超实用教程 - SVG Icon封装

超实用教程 - SVG Icon封装

你是否也有业务繁杂,Icon多难管理,并且很多重复,新增Icon时要手动引用的问题。并且SVG通过img标签使用并不能使用CSS修改其颜色的烦恼。

那么接下来我们将来一步一步解决这些问题。
由于笔者使用的是Vue开发,所以下面的封装皆为Vue代码,其他框架可以以此类推。

统一的Icon管理

为什么需要统一的Icon管理?

  • 如果Icon零零散散的放在项目的各个角落,我们很难去抉择新的Icon应该放在哪
  • 有很多Icon其实是很通用的,并不属于具体的某个模块
  • 没有统一的Icon管理,我们很难实现自动生成index导入Icon

为了让我们的项目良性的发展下去,统一的Icon管理是一个很好地决定。(这里的Icon可以是SVG,图片,Iconfont等等)

所以第一步,就是规范我们的目录结构

├─ src/assets/
         ├─ svgs/
         │    ├─ xxx.svg
         │    ├─ ...
         │    └─ index.js
         │
         └─ imgs/
              ├─ xxx.png...
              ├─ ...
              └─ index.js

我们在asset中建了两个文件夹,一个用来存SVG,一个用来存其他图片。为啥?因为SVG可以特殊处理!这个我们后面再说。

有了目录结构之后,我们就可以按部就班的将图片放进来。

到这里,我们的图片Icon不再流离失所,它们,有家了!接着就是索引文件index.js了。

import remove from './remove.svg'
....

export default {
  remove,
  ...
}

我们每加一个图片,都需要手动的index中将其引用进来,这很麻烦,特别是一次新增很多图片的时候。那么有没有比较便捷的做法呢。答案就是我们写一个脚本自动生成index.js

// /scripts/buildAssetsIndex.js

const fs = require('fs');
const path = require('path');
const _ = require('lodash');

const imgExt = ['svg', 'jpg', 'jpeg', 'png', 'gif'];

const iconPaths = new Set();
const duplicatedList = [];

function readDir(dirPath) {
  let dirs = [];
  try {
    dirs = fs.readdirSync(dirPath);
  } catch (error) {
    return;
  }

  dirs.forEach((dir) => {
    if (imgExt.includes(path.extname(dir).substring(1))) {
      const iconPath = path.join(dirPath, dir);

      if (iconPaths.has(iconPath)) {
        duplicatedList.push(iconPath);
      }
      iconPaths.add(iconPath);
      return;
    }

    readDir(path.join(dirPath, dir));
  });
}

function createImportScript(storePath) {
  let importContent = '';
  let exportContent = 'export default {\n';
  const icons = [...iconPaths];
  icons.forEach((iconPath, i) => {
    let iconName = iconPath.replace(storePath, '');
    iconName = iconName.replace(/\//g, '_');
    iconName = iconName.substring(0, iconName.indexOf('.'));
    iconName = _.snakeCase(iconName);

    const importPath = iconPath.replace(storePath, '.');

    importContent += `import ${iconName} from '${importPath}';\n`;
    exportContent += `  ${iconName}${i === icons.length - 1 ? '' : ','}\n`;
  });
  importContent += '\n';
  exportContent += '};\n';

  fs.writeFileSync(path.join(storePath, 'index.js'), importContent + exportContent);
}

const iconPath = path.join(__dirname, '../src/assets/icons');
readDir(iconPath);
createImportScript(iconPath);
iconPaths.clear();

const imgPath = path.join(__dirname, '../src/assets/imgs');
readDir(imgPath);
createImportScript(imgPath);

if (duplicatedList.length) {
  console.error('there are some duplicated icon paths: ');
  console.log(duplicatedList);
}

在每次新增一个Icon之后,我们只需要执行一下这个脚本,index.js便更新好了。

统一的Icon组件

我们在使用这些图片Icon的时候,一般是使用img标签来加载的,那就不免要传入资源的src。
搭配webpackfile-loader之后,我们index.js就是一个name: url的对应表,所以原理上我们只需要知道Icon的name即可定位到具体的某个src。

import remove from './remove.svg'

export default { remove }

// {
//   remove: '/publicDir/remove.svg'
// }

我们通过封装一个组件来实现传入name即可渲染Icon,会大大的提高我们的开发效率。
这个组件也很简单:

<template>
  <img class="img-icon" :style="style" :src="source" />
</template>

<script>
import IMGS from '@/assets/imgs';

export default {
  name: 'ImgIcon',

  props: {
    size: {
      type: String,
      default: '16px'
    },
    name: {
      type: String,
      default: ''
    },
    mr5: {
      type: Boolean,
      default: false
    }
  },

  computed: {
    source() {
      return IMGS[this.name] || this.name;
    },
    style() {
      return {
        height: this.size,
        width: this.size,
        marginRight: this.mr5 ? '5px' : undefined
      };
    }
  }
};
</script>

<style scoped>
.img-icon {
  display: inline-block;
}
</style>

SVG!不止这样

有了以上组件,我们在使用的过程中以及很统一并且方便了,但是有个痛点,这也是很多人喜欢Iconfont的原因。怎么用CSS修改Icon的颜色啊!!!!我不想每个Icon都下两份555…

这个时候我们恍然大悟,咱的Icon的是SVG啊,SVG不是支持CSS吗!说干就干,怎么将SVG直接嵌到DOM里呢?一开始我们试用了embed,发现它把SVG加载到了另一个Document下面?这咋选…

我们想要的是SVG直接嵌到DOM里,但是没有元素能直接的这样将SVG文件嵌到页面上。那好吧,难道我们要手动的创建SVG元素插进DOM?这也太粗暴了吧。我们继续搜索解决方案,发现了其实早就有这种解决方案了。

新的解决方案:svg-sprite-loader

这项目16年就开始了…,但是感觉没有怎么被推广

8错,这个webpack loader就能解决我们的痛点,他的原理是通过将svg读取后全部预加载到DOM中,每一个SVG都会生成一个symbolId和我们上面的name一个意思。

然后就是使用:

<svg class="svg-icon">
  <use xlink:href="#remove" />
</svg>

use元素,SVG中重用定义好的元素的方法,但是通过这种方法并不能将我们的的SVG Icon完全替换掉use,也就是说use只是一个链接。但是!现在我们的SVG Icon可以继承样式了!

use的帮助下,我们SVG Icon现在能继承.svg-icon上的属性!也就是说我们直接用CSS修改.svg-icon就能反应到use链接的SVG上。

但是!!!!

我们的SVG文件中需要继承的属性不能显示的申明在元素上!!!!

code.png

图中的fill,因为我们一般改颜色就是指定fill的。所以这里的fill统统全部做掉!!!

最后的效果:
image.png
我们甚至害能用stroke给它描个边!

Icon预览

对于重复Icon,首先如果我们命名规范,就能一定程度上能防范。比如,根据图标的形状命名而不是具体业务,使用arrow_left而不是back更能体现其通用性。其次呢,我们得有一个能预览所有Icon的页面。它能帮助我们快速搜索以及发现重复Icon,快速复制Icon的symbolId。

image.png

所以我们可以建一个预览页面,将所有Icon聚合展示,可供搜索,复制。

到这里,咱们的Icon管理已经很OK了。如何做到Icon展示的统一,还需要和设计师进行沟通统一。那就不在我们讨论的范畴里了。

上一篇 堆的基础知识和相关算法
下一篇 Rollup plugin 推荐