超实用教程 - 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。
搭配webpack
的file-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文件中需要继承的属性不能显示的申明在元素上!!!!
图中的fill
,因为我们一般改颜色就是指定fill
的。所以这里的fill
统统全部做掉!!!
最后的效果:
我们甚至害能用stroke给它描个边!
Icon预览
对于重复Icon,首先如果我们命名规范,就能一定程度上能防范。比如,根据图标的形状命名而不是具体业务,使用arrow_left
而不是back
更能体现其通用性。其次呢,我们得有一个能预览所有Icon的页面。它能帮助我们快速搜索以及发现重复Icon,快速复制Icon的symbolId。
所以我们可以建一个预览页面,将所有Icon聚合展示,可供搜索,复制。
到这里,咱们的Icon管理已经很OK了。如何做到Icon展示的统一,还需要和设计师进行沟通统一。那就不在我们讨论的范畴里了。