背景
实现一个通过页面批量上传svg源文件,进行转换后发布到npm仓库,前端按需引用,可以作为团队内部的一个icon库使用,ui在界面上上传,各个项目可以更新使用。
思路
使用nodejs做服务端,主要功能是把上传的svg源文件保存到服务器中,然后批量读取源文件转换成js文件,再发布到npm,下面主要介绍的是svg->js的转换
目标是前端按需引用就需要每个icon有独立的文件,并且export一个Vue.extend构造器,组件选项就是svg的一些信息,所以我们主要工作就是
1.编写一个模板文件
2.批量读取svg源文件,写一个svg标签拆分为createElement的方法
3.生成各个js文件,
4.最后生成一个index.js用于引用所有的js。
效果
执行generator.js,生成component文件夹、index.js文件,编写package.json文件,提交package.json和生成的文件到npm仓库,在项目中引用。
实现
mock形成组件的js文件:我们的模版js也是基于这个构造去生成的,取svg源文件的文件名作为组件的名字
template.js:模版文件 ,icon支持的属性有size、color、click事件,有$$ComponentName$$、$$ComponentCamelName$$、$$SVGContent$$三个变量,分别表示组件名、驼峰组件名、svg标签中的内容转换成的createElement数组,执行脚本的时候会进行变量替换。
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
var Vue = require('vue');
function _interopDefaultLegacy(e) {
return e && typeof e === 'object' && 'default' in e ? e : {
'default': e
};
}
var Vue__default = _interopDefaultLegacy(Vue);
var $$ComponentCamelName$$ = Vue__default["default"].extend({
name: '$$ComponentCamelName$$',
functional: true,
props: {
size: String,
color: String,
onClick: Function
},
render: function render(createElement, context) {
var props = context.props,
data = context.data;
return createElement('svg', {
class: 'ahc-$$ComponentName$$',
style: {
color: props.color,
'font-size': props.size,
},
attrs: {
width: '1em',
height: '1em',
fill: 'none',
viewBox: '0 0 16 16',
},
on: {
click: function (e) {
if (data.on && data.on.click) {
data.on.click(e);
}
}
}
}, $$SVGContent$$)
}
});
exports["default"] = $$ComponentCamelName$$;
generator.js:生成js执行的脚本,主要是遍历svg文件夹,生成一个component文件夹,放置所有生成的js文件,最后生成一个index.js,主要内容如下示例,generator.js中调用了util.js,封装了两个方法,一个是字符串连字符转驼峰的方法,还有一个是svg标签内容转成createElement的方法,这个写的比较粗糙,后面根据实际情况可以进行调整。
// index.js
var AddCircle = require('./component/add-circle.js');
exports.AddCircle = AddCircle["default"];
var ArrowDownRectangle = require('./component/arrow-down-rectangle.js');
exports.ArrowDownRectangle = ArrowDownRectangle["default"];
// generator.js
const fs = require('fs');
const path = require('path');
const util = require('./util');
const templateContent = fs.readFileSync(path.join(__dirname, 'template.js'), 'utf-8');
const svgFilePath = path.join(__dirname, 'svg');
const filePaths = fs.readdirSync(svgFilePath);
for (let i = 0; i < filePaths.length; i++) {
const svgName = filePaths[i].replace('.svg', '');
const svgPath = path.join(__dirname, 'svg', filePaths[i]);
const file = fs.readFileSync(svgPath, {
encoding: 'utf-8'
});
const children = util.getSvgChildren(file);
if (children) {
const newPage = templateContent.replace('$$ComponentName$$', svgName)
.replace(/\$\$ComponentCamelName\$\$/g, util.toHump(svgName))
.replace('$$SVGContent$$', `[${children}]`);
const outputFoldPath = path.join(__dirname, 'component');
if (!fs.existsSync(outputFoldPath)) {
fs.mkdirSync(outputFoldPath);
}
fs.writeFileSync(path.join(__dirname, 'component', svgName + '.js'), newPage);
}
}
const componentFiles = fs.readdirSync(path.join(__dirname, 'component'), {
encoding: 'utf-8'
});
let dataStr = '';
componentFiles.forEach(file => {
const varName = util.toHump(file.replace('.js', ''));
dataStr += `var ${varName} = require('./component/${file}');exports.${varName} = ${varName}["default"];`
})
const componentIndexPath = path.join(__dirname, 'index.js');
fs.writeFileSync(componentIndexPath, dataStr);
// util.js
function toHump(str) {
str = str.replace(/\-(\w)/g, function (_, letter) {
return letter.toUpperCase();
})
const firstCase = str[0].toUpperCase();
return firstCase + str.slice(1);
}
function getSvgChildren(str) {
// 先去掉svg标签
const deleteSvgTag = str.replace(/<svg(S*?)[^>]*>/, '').replace(/<\/svg>/, '');
// 获取中间的内容列表
const contentList = deleteSvgTag.match(/<(S*?)[^>]*\/>/g);
if (contentList) {
const attrList = [];
contentList.forEach(content => {
if (content) {
const tag = content.match(/<(\S*)\s/g)[0].slice(1).trim();
const attrStr = content.replace('<' + tag, '').replace('/>', '');
const keys = attrStr.match(/(\w+|\w+\-\w+)=/g).map(v => v.replace('=', ''));
const attrMap = {};
keys.forEach(key => {
const reg = new RegExp(key + '="(.*?)"');
let value = content.match(reg)[1];
if (key === 'fill') {
value = 'currentColor';
}
attrMap[key] = value;
})
attrList.push(`createElement("${tag}", {attrs: ${JSON.stringify(attrMap)}})`)
}
})
return attrList.toString();
}
}
module.exports = {
toHump,
getSvgChildren
}
改进
因为解析svg的方法写的比较粗糙,所以后续将util中的getSvgChildren方法去掉,改为使用htmlparser去解析,修改的地方涉及generator.js的for循环处理方法。
for (let i = 0; i < filePaths.length; i++) {
const svgName = filePaths[i].replace('.svg', '');
const svgPath = path.join(__dirname, 'svg', filePaths[i]);
const file = fs.readFileSync(svgPath, {
encoding: 'utf-8'
});
const handler = new htmlparser.DefaultHandler(function (error, dom) {
if (error) {
console.log('解析失败', error);
} else {
const attrList = [];
const content = dom[0].children || [];
content.forEach(ele => {
if (ele.type === 'tag') {
const attrs = ele.attribs;
if (attrs['fill']) {
attrs['fill'] = 'currentColor';
}
attrList.push(`createElement("${ele.name}", {attrs: ${JSON.stringify(attrs)}})`)
}
});
if (attrList.length) {
const newPage = templateContent.replace('$$ComponentName$$', svgName)
.replace(/\$\$ComponentCamelName\$\$/g, util.toHump(svgName))
.replace('$$SVGContent$$', `[${attrList.toString()}]`);
const outputFoldPath = path.join(__dirname, 'component');
if (!fs.existsSync(outputFoldPath)) {
fs.mkdirSync(outputFoldPath);
}
fs.writeFileSync(path.join(__dirname, 'component', svgName + '.js'), newPage);
}
}
});
const parser = new htmlparser.Parser(handler);
parser.parseComplete(file);
}