背景
我们使用iconfont-阿里巴巴矢量图标库来管理自己的一套图标,并且基于它的js资源,封装了自己的icons图标组件。封装的方法是使用了antd提供的createFromIconfontCN方法
但随着图标库越来越大,JS资源文件也变得越来越大。在业务中,哪怕你只需要一个图标,却也要引入整个的JS资源。这使得我们必须考虑对他进行优化。
解决思路
我们参考@ant-design/icons的设计模式,计划将它的图标拆分成了一个一个组件,在业务中按需引入来达到减少资源浪费的目的。
我们阅读了@ant-design/icons的源码,发现他的图标对象其实都放在了一个叫做@ant-design/icons-svg的包里,里面放了每一个图标的属性。@ant-design/icons引用了这些图标并再次封装了一些属性和方法。
然而我们并不能这样做,我们没有深究他的这些图标对象是如何自动产生的。 我们只有在iconfont-阿里巴巴矢量图标库上下载的JS资源文件。
好在这个JS文件非常的有规律,他其实就是每一个svg图标的path路径。这使得我们可以简单的使用这个js文件来创建我们的图标组件,而不需要在独立创建一个包来存放图标的属性。
实现步骤
- 我们需要使用脚本读取这个js文件,输出一个数组,数组里面是每个svg图标的path
- 创建一个组件,用来接受svg的path内容
- 创建一个模版,循环数组的内容并生成相应的文件
- 脚本生成tree shake目录
- 执行脚本,生成对应文件
一、读取文件,输出数组
const fs = require('fs');// 读取文件后整理成后的数据源
const fileContent = [];function handleFormatData(content) {const symbolStr = content.split('<svg>')[1].split('</svg>')[0].replace(/<\/symbol>/g, '</symbol>\n');// 先根据 回车 拆成数组,每一项都包含了一个图标的信息const IconsStringArr = symbolStr.split('\n').filter(Boolean);const fileNameRegExp = /id="你的js的svg的id前缀-([^]*?)"/g;const viewBoxRegExp = /viewBox="([^]*?)"/g;const pathRegExp = /<path([^]*?)path>/g;IconsStringArr.forEach((element) => {const fileName = element.match(fileNameRegExp)[0].split('id="你的js的svg的id前缀-')[1].split('"')[0];const viewBox = element.match(viewBoxRegExp)[0].split('viewBox="')[1].split('"')[0];const path = element.match(pathRegExp).join('');fileContent.push({fileName: `P${fileName}`,viewBox: viewBox,svgPath: path,});});
}fs.readFile('./iconfont.js', (err, data) => {// 将文件文本处理成想要的数据源handleFormatData(data.toString());});
二、组件
import React, { PropsWithChildren } from 'react';export interface LeeIconProps {spanProps?: React.HTMLProps<HTMLSpanElement>;svgProps?: React.SVGProps<SVGSVGElement>;
}const LeeIcon = (props: PropsWithChildren<LeeIconProps>) => {return (<span role="img" {...props.spanProps} ><svg width="1em" fill="currentColor" aria-hidden="true" {...props.svgProps}>{props.children}</svg></span>);
};export default LeeIcon;
三、模版、生产文件
const lodash = require('lodash');function generateFile() {const render = lodash.template(`
/* eslint-disable react/self-closing-comp */
// GENERATE BY ../../scripts/generate.ts
// DON NOT EDIT IT MANUALLYimport LeeIcon, { LeeIconProps } from '../components/icon';const <%= fileName %> = (props: LeeIconProps) => (<LeeIcon{...props}svgProps={{viewBox: '<%= viewBox %>',...props.svgProps,}}><%= svgPath %></LeeIcon>
);export default <%= fileName %>;`.trim(),);console.log('generateFileLoading...');fileContent.forEach(({ fileName, viewBox, svgPath }) => {// 写入文件内容fs.writeFileSync(`../src/leeIcons/${fileName}.tsx`, render({ fileName, viewBox, svgPath }), () => {});console.log(`generateFile${fileName}Down`);});
}
// 生成文件
generateFile();
四、生产Tree Shake目录
function generateTreeShake() {console.log('generateTreeShakeLoading...');const render = lodash.template(`
export { default as <%= fileName %> } from './<%= fileName %>';`,);fs.writeFileSync(`../src/leeIcons/index.ts`,`
// GENERATE BY ../../scripts/generate.ts
// DON NOT EDIT IT MANUALLY`.trim(),() => {},);fileContent.forEach(({ fileName }) => {fs.appendFileSync(`../src/leeIcons/index.ts`, render({ fileName }), () => {});});console.log('generateTreeShakeSuccess');
}
// 生成摇树首页
generateTreeShake();
五、执行脚本
node ./generate.js
完整代码
/* eslint-disable @typescript-eslint/no-require-imports */
/** @Author: atwLee* @Date: 2023-04-07 15:48:20* @LastEditors: atwLee* @LastEditTime: 2023-04-12 10:18:40* @Description:* @FilePath: /panui/packages/Icons/script/generate.js*/
const fs = require('fs');
const lodash = require('lodash');
const rimraf = require('rimraf');// 读取文件后整理成后的数据源
const fileContent = [];function handleFormatData(content) {const symbolStr = content.split('<svg>')[1].split('</svg>')[0].replace(/<\/symbol>/g, '</symbol>\n');// 先根据 回车 拆成数组,每一项都包含了一个图标的信息const IconsStringArr = symbolStr.split('\n').filter(Boolean);const fileNameRegExp = /id="你的js的svg的id前缀-([^]*?)"/g;const viewBoxRegExp = /viewBox="([^]*?)"/g;const pathRegExp = /<path([^]*?)path>/g;IconsStringArr.forEach((element) => {const fileName = element.match(fileNameRegExp)[0].split('id="你的js的svg的id前缀-')[1].split('"')[0];const viewBox = element.match(viewBoxRegExp)[0].split('viewBox="')[1].split('"')[0];const path = element.match(pathRegExp).join('');fileContent.push({fileName: `P${fileName}`,viewBox: viewBox,svgPath: path,});});
}function generateFile() {const render = lodash.template(`
/* eslint-disable react/self-closing-comp */
// GENERATE BY ../../scripts/generate.ts
// DON NOT EDIT IT MANUALLYimport LeeIcon, { LeeIconProps } from '../components/icon';const <%= fileName %> = (props: LeeIconProps) => (<LeeIcon{...props}svgProps={{viewBox: '<%= viewBox %>',...props.svgProps,}}><%= svgPath %></LeeIcon>
);export default <%= fileName %>;`.trim(),);console.log('generateFileLoading...');fileContent.forEach(({ fileName, viewBox, svgPath }) => {// 写入文件内容fs.writeFileSync(`../src/leeIcons/${fileName}.tsx`, render({ fileName, viewBox, svgPath }), () => {});console.log(`generateFile${fileName}Down`);});
}function generateTreeShake() {console.log('generateTreeShakeLoading...');const render = lodash.template(`
export { default as <%= fileName %> } from './<%= fileName %>';`,);fs.writeFileSync(`../src/leeIcons/index.ts`,`
// GENERATE BY ../../scripts/generate.ts
// DON NOT EDIT IT MANUALLY`.trim(),() => {},);fileContent.forEach(({ fileName }) => {fs.appendFileSync(`../src/leeIcons/index.ts`, render({ fileName }), () => {});});console.log('generateTreeShakeSuccess');
}function generateIcons() {// 删除目录rimraf('../src/leeIcons', () => {// 增加目录fs.mkdirSync('../src/leeIcons', () => {});// 读取icons-js文件,获取需要的数据fs.readFile('./iconfont.js', (err, data) => {// 将文件文本处理成想要的数据源handleFormatData(data.toString());// 生成文件generateFile();// 生成摇树首页generateTreeShake();});});
}generateIcons();