图标方案

275 阅读3分钟

方案一 字体图标

web常用的图标方案是字体图标(iconfont),常使用www.iconfont.cn/ 等网站来将多个 svg图标转换为字体文件。

使用方式

<i class="iconfont add"></i>

优缺点

  • 生成字体图标简单,没有开发成本 缺点
  • 依赖外部系统,不方便项目交接
    项目换人后,希望再增加或删除图标就有些麻烦。而且如果同事离职了还可能找不到原来的iconfont图标。 如果将原来的svg图标保存一份到项目文件中,能解决一部分问题。
  • 一次加载所有的图标
    • 有的图标可能只在某几个页面中使用,但会一起加载(一般不会分多个iconfont,比较麻烦)。
    • (公司)组件库提供了图标,可能只用到其中几个,却要加载整个的iconfont。

方案二 svg icon

  1. 此次依赖icon库,如何开发会在后面讲到
  2. 此方案的代码都放到了github.com/xiangweiweb…

使用方式

1)引入icon组件

import Icon from '@xiangwei/icon';

2)添加项目图标

  1. @/icon是在打包配置中定义的目录别名
  2. 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>

Xnip2022-05-12_08-52-33.jpg

图标处理

这一步是将svg图片转换成开发使用的js文件

1)准备svg图片

Xnip2022-05-12_08-54-03.jpg

2)使用依赖icon提供的命令行转换

  "scripts": {
    "serve": "vue-cli-service serve",
     ...
    "build:icon": "icon build svg --source svg --output src/icon"
  },

Xnip2022-05-12_09-00-12.jpg

组件设计

1、希望能通过 <icon name="alipay" fill="red"></icon> 这种方式使用图标
那么就需要name和svg图标就需要一一对应。
采用将svg转为js的方式,因为svg是xml的一种,能很容易的转换为js对象。
转化结果(icon属性值就是svg的json结构):

Xnip2022-05-12_09-10-13.jpg

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文件体积如果有的图标只在某些页面用到了,可以在组件内添加图标,动态组件打包,可减少一次引入的图标数量)