方案一 字体图标
web常用的图标方案是字体图标(iconfont),常使用www.iconfont.cn/ 等网站来将多个 svg图标转换为字体文件。
使用方式
<i class="iconfont add"></i>
优缺点
优
- 生成字体图标简单,没有开发成本 缺点
- 依赖外部系统,不方便项目交接
项目换人后,希望再增加或删除图标就有些麻烦。而且如果同事离职了还可能找不到原来的iconfont图标。 如果将原来的svg图标保存一份到项目文件中,能解决一部分问题。 - 一次加载所有的图标
- 有的图标可能只在某几个页面中使用,但会一起加载(一般不会分多个iconfont,比较麻烦)。
- (公司)组件库提供了图标,可能只用到其中几个,却要加载整个的iconfont。
方案二 svg icon
- 此次依赖icon库,如何开发会在后面讲到
- 此方案的代码都放到了github.com/xiangweiweb…
使用方式
1)引入icon组件
import Icon from '@xiangwei/icon';
2)添加项目图标
- @/icon是在打包配置中定义的目录别名
- js图标文件是如何来的见后面的【图标处理】
import alipayIcon from '@/icon/alipay.js';
import accountBookIcon from '@/icon/account-book.js';
import shebaoIcon from '@/icon/医疗_电子社保卡.js';
import tingzhenIcon from '@/icon/医疗_听诊.js';
Icon.add([alipayIcon, accountBookIcon, shebaoIcon, tingzhenIcon]);
或者可以使用require.context,将icon目录下所有的图标都引入进来
const icons = require.context('@/icon', false, /\.js$/);
icons.keys().forEach(key => {
const iconInstance = icons(key).default;
Icon.add(iconInstance);
});
3)将Icon组件注册为全局组件(组件中定义了install方法)
Vue.use(Icon);
4)在组件中使用
<div id="app">
<icon name="alipay" fill="red"></icon>
<icon name="account-book"></icon>
<icon name="医疗_电子社保卡" size="30px"></icon>
<icon name="医疗_听诊" class="hello-world"></icon>
</div>
图标处理
这一步是将svg图片转换成开发使用的js文件
1)准备svg图片
2)使用依赖icon提供的命令行转换
"scripts": {
"serve": "vue-cli-service serve",
...
"build:icon": "icon build svg --source svg --output src/icon"
},
组件设计
1、希望能通过 <icon name="alipay" fill="red"></icon> 这种方式使用图标
那么就需要name和svg图标就需要一一对应。
采用将svg转为js的方式,因为svg是xml的一种,能很容易的转换为js对象。
转化结果(icon属性值就是svg的json结构):
2、想能灵活添加项目图标
第一 组件需要提供方法方便添加图标
第二 组件需要提供命令行方便将项目的svg转换为js(icon组件是通过依赖的方式添加到项目中时需要)
3、可以顺带优化一下svg
组件开发
1、定义组件数据和方法
const Icon = {
name: 'icon',
props: {
// 要渲染的组件唯一标识
name: String,
// 图标宽高:长度相同
size: {
type: String,
default: '20px'
},
// 图标宽高:优先级高于size,两个都设置才会生效
width: String,
height: String,
},
// 项目可使用图标集合(图标:svg打包成js后的对象)
icons: [],
/**
* 给图标组件添加可使用的图标
* @param {*} newIcons 参数可以是单个图标对象,也可以是图标数组
*/
add(newIcons){
const expectAddIcons = [].concat(newIcons);
expectAddIcons.forEach(icon => {
// 防重复添加图标
if(this.icons.indexOf(icon) === -1){
this.icons.push(icon);
}
});
},
}
2、当前渲染的组件&宽高
computed: {
/**
* 当前要渲染的icon
* {name: '', icon: svgJSONObject}
*/
renderIcon(){
// 如果图标没找到,会在渲染的时候处理
return this.$options.icons.find((item) => item.name === this.name);
},
/**
* 当前要渲染的icon的宽高
* 1)宽高默认使用size
* 2) 如果组件设置了宽高则用此数据覆盖size
*/
renderSize(){
const size = {
width: this.size,
height: this.size
}
if(typeof this.width === 'string' && typeof this.height === 'string'){
size.width = this.width;
size.height = this.height;
}
return size;
}
},
3、渲染图标
/**
* SVG渲染
* @param {Function} h vue 渲染函数
* @param {Object} node icon js对象 {name: xx, icon: svgJSONObj}或svgJSONObj中的元素
* @param {Object} props 渲染标签的属性
* @returns
*/
function renderSvg(h, node, props){
let nodeProps = {
attrs: {
...node.attributes
}
};
// 根元素,绑定事件,设置组件的宽高颜色
if(node.name === 'svg'){
nodeProps.on = props.listeners;
Object.assign(nodeProps.attrs, props.attrs);
}
const children = node.children || [];
return h(
node.name,
nodeProps,
children.map((childNode) => {
return renderSvg(h, childNode, props);
})
)
}
const Icon = {
name: 'icon',
render(h){
console.log('render icon is ', this.renderIcon ? this.renderIcon.name : null);
if(!this.renderIcon) return null;
const props = {
listeners: this.$listeners,
attrs: {
name: this.renderIcon.name,
width: this.renderSize.width,
height: this.renderSize.height,
fill: 'currentColor'
}
};
return renderSvg(h, this.renderIcon.icon, props);
},
}
4、svg转换-模板template.ejs
const <%= name%> = <%- data%>;
export default <%= name%>;
5、svg转换-处理函数
const path = require('path');
const fs = require('fs');
const ejs = require('ejs');
const rimraf = require('rimraf');
const parseXml = require('@rgrove/parse-xml');
const { optimize } = require('svgo');
const colors = require('colors');
const iconTemplatePath = path.resolve(__dirname, '../template.ejs');
const iconTemplate = fs.readFileSync(iconTemplatePath, 'utf8');
const compiler = ejs.compile(iconTemplate);
/**
* 获取图标对应的变量名
* 图片会被打包成js文件,此时需要使用到
* @param {String} sourceName
* @returns {String}
*/
function getIconVarName(sourceName){
sourceName = sourceName.replace('-', '_');
const reg = /^([^\x00-\xff]|[a-zA-Z_$])([^\x00-\xff]|[a-zA-Z0-9_$])*$/;
if(reg.test(sourceName)){
return sourceName + 'Icon';
}else{
// 如果名称不满足变量名规则,则随机生成4位字符
const getRandom = () => {
// 0-25
return Math.floor(Math.random() * 26);
}
const chars = 'abcdefghijklmnopqrsjuvwxyz';
const newName = `${chars[getRandom()]}${chars[getRandom()]}${chars[getRandom()]}${chars[getRandom()]}`;
console.log(colors.blue(`文件名[${sourceName}]不满足变量命名规则,使用随机名称[${newName}]`));
return newName + 'Icon';
}
}
function getResolvePath(dir){
let resolvePath = null;
if(path.isAbsolute(dir)){
resolvePath = dir;
}else{
resolvePath = path.resolve(process.cwd(), dir);
}
return resolvePath;
}
/**
* 将svg格式打包成js(组件渲染使用),并做svg图标优化
* @param {*} sourceDir svg图标的文件路径(绝对路径)
* @param {*} targetDir 打包后svg图标输出的文件路径(绝对路径)
*/
async function handler(sourceDir, targetDir){
const fileList = fs.readdirSync(sourceDir, 'utf8');
for(const filename of fileList){
console.log(colors.blue('filename is ' + filename));
if(!(/\.svg$/.test(filename))){
console.log(colors.blue('not svg'));
continue;
}
// 1. 优化svg
const filePath = path.resolve(sourceDir, filename);
const data = fs.readFileSync(filePath, 'utf8');
const result = await optimize(data);
// 2. 组装组件需要的数据
const iconJSONObj = parseXml(result.data).children[0];
const iconName = path.basename(filename, '.svg');
const icon = {
// 用文件名来做组件调用时的名称
name: iconName,
icon: iconJSONObj
};
const componentStr = compiler({
// 文件名称可能不符合变量命名方式,所以需要处理一下
name: getIconVarName(iconName),
data: JSON.stringify(icon, null, 4)
});
// 3. 将数据输出到文件中
const targetFilePath = path.resolve(targetDir, iconName +'.js');
fs.writeFileSync(targetFilePath, componentStr);
}
};
module.exports = function build(source, output){
const sourceDir = getResolvePath(source);
console.log(colors.yellow('source dir is ' + sourceDir));
if(!fs.existsSync(sourceDir)){
console.log(colors.red('source dir is not exist'));
return;
}
const targetDir = getResolvePath(output);
console.log(colors.yellow('output dir is ' + targetDir));
rimraf(
targetDir,
async function success(){
fs.mkdirSync(targetDir, {recursive: true});
await handler(sourceDir, targetDir);
console.log(colors.green('build successful!!!'));
}
);
}
6、svg转换-命令行
#!/usr/bin/env node
const { program } = require('commander');
const build = require('./build');
const package = require('../../package.json');
program
.version(package.version);
program
.command('build <name>')
.requiredOption('-s, --source <path>', 'svg 文件夹路径')
.option('-o, --output <path>', 'icon图标输出文件夹路径', 'icons')
.action((name, options) => {
if(name !== 'svg'){
console.log('只支持svg图片打包');
return;
}
build(options.source, options.output);
});
program.parse();
优缺点
优
- 不依赖外部系统(开发者使用方便)
- 添加图标十分方便且能按需引入(公司的图标库,项目也能按需引入)
- 可以通过打包配置svg优化方案
缺点
- 图标会被打包到JS中,增加js文件体积如果有的图标只在某些页面用到了,可以在组件内添加图标,动态组件打包,可减少一次引入的图标数量)