前言
最近在公司里从0到1的搭建了一套SVG图标上传及图标组件发布系统,目的是让图标上传
到发布NPM包
之间的全部流程实现自动化。
主要的流程为:

现在社区上利用SVG用作图标组件的UI库其实非常多,例如Antd
,MUI
等等,但是如何自动化的将一个单色SVG或者多色SVG的文件转化为React组件的文章却属实不多。在上述的整个系统实现的过程中,其实有许多的功能点,或者踩过的坑都蛮有实战价值,所以简单的通过这篇文章来详细的记录和总结一下,希望能够帮助到相关的同学。
获取SVG文件内容
作为整个系统的起点,当用户在客户端将SVG图标文件上传后,我们在服务端需要正确的接收文件并解析出文件的内容。 这里,我使用了流的形式,去接收并获取传输过来的文件内容。
js
const onReadFile = (file) => {
return new Promise((resolve, reject) => {
const fileStream = fs.createReadStream(file.filepath);
let fileValue = "";
fileStream.on("data", function (chunk) {
// 读取内容
fileValue += chunk;
});
fileStream.on("end", function (chunk) {
// 读取内容
resolve(fileValue);
});
});
};
解析SVG文件内容
利用Node原生提供的流的方法,我们可以很简单的获取到上传文件的内容。我们先来简单看下SVG内容的一些特点。
以一个单色SVG图标为例,我们可以看到在SVG的标签里面,除了我们熟悉的width
、height
、id
之外,还有很多一些额外的属性,例如version
,xmlns
等等,通常来说这些属性对我们的元素构成并没有太大的影响。接着,我们还可以发现,SVG里其实是利用了path
或者rect
等线性或者图案元素去构成最终的图形,并利用fill
或者stroke
来填充颜色。还需要注意的是,SVG图形还会携带一个viewBox
属性,这个属性实则是去控制该SVG图形的可视区域,对于SVG图形的展示至关重要,通常来说都需要保留。
html
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Delete</title>
<g id="图层1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="图层2" transform="translate(-564.000000, -283.000000)" fill-rule="nonzero">
<g id="Delete" transform="translate(588.000000, 307.000002) rotate(0.010000) translate(-588.000000, -307.000002) translate(564.004188, 283.004189)">
<rect id="矩形" fill-opacity="0" fill="#000000" x="0" y="0" width="47.9916246" height="47.9916261"></rect>
<path d="M33.9971919,11.9979065 L43.9985715,11.9979065 L43.9985715,16.0003331 L39.996145,16.0003331 L39.996145,43.9985728 L7.99547965,43.9985728 L7.99547965,16.0003331 L4.00242651,16.0003331 L4.00242651,11.9979065 L14.0038061,11.9979065 L14.0038061,4.00242663 L33.9971919,4.00242663 L33.9971919,11.9979065 Z M35.9937185,16.0003331 L11.9979062,16.0003331 L11.9979062,39.9961462 L35.9937185,39.9961462 L35.9937185,16.0003331 Z M17.9968592,21.9992864 L21.9992857,21.9992864 L21.9992857,33.9971929 L17.9968592,33.9971929 L17.9968592,21.9992864 Z M25.9923389,21.9992864 L29.9947654,21.9992864 L29.9947654,33.9971929 L25.9923389,33.9971929 L25.9923389,21.9992864 Z M17.9968592,7.99547989 L17.9968592,11.9979065 L29.9947654,11.9979065 L29.9947654,7.99547989 L17.9968592,7.99547989 Z" id="形状" fill="#5C7080"></path>
</g>
</g>
</g>
</svg>
我们再来看下多色图标,可以看到多色图标里,可能会含有更多我们不常见的标签属性,如title
标签,和style
的样式标签,另外,也会有defs
标签和use
属性,我们可以在defs标签内去定义可能需要多次用到的图形,然后再利用use
其ID来做到图形的复用。
html
<?xml version="1.0" encoding="UTF-8"?>
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Title</title>
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#0C40A8;}
</style>
<defs>
<pattern id="pattern-1" width="100%" height="100%" patternUnits="objectBoundingBox">
<use xlink:href="#image-2"></use>
</pattern>
<image id="image-2" width="40" height="40" xlink:href="data:image/png;base64,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"></image>
</defs>
<g id="Icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-232, -936)" id="编组-8">
<g transform="translate(228, 932)">
<rect id="矩形" x="0" y="0" width="48" height="48"></rect>
<g id="Kirby-Yicon-glo_collected备份-3" transform="translate(4, 4)" fill="url(#pattern-1)" fill-rule="nonzero">
<g id="加载4_loading-four">
<path class="st0" d="M20,0 C31.0456996,0 40,8.95430038 40,20 C40,31.0456996 31.0456996,40 20,40 C8.95430038,40 0,31.0456996 0,20 C0,19.1715729 0.671572875,18.5 1.5,18.5 C2.32842712,18.5 3,19.1715729 3,20 C3,29.3888454 10.6111546,37 20,37 C29.3888454,37 37,29.3888454 37,20 C37,10.6111546 29.3888454,3 20,3 C19.1715729,3 18.5,2.32842712 18.5,1.5 C18.5,0.671572875 19.1715729,0 20,0 Z" id="路径"></path>
</g>
</g>
</g>
</g>
</g>
</svg>
完成React组件的转化
在了解完SVG的一些属性特点之后,我们便开始利用Node脚本来完成SVG文件到React组件的转化。
但是在此之前,我们还得清晰的知道,我们最终需要得到的是一个怎样的React
组件,也就是我们需要去理清楚这个组件需要具有怎样的功能点,作为一个图标组件,我们需要做到:
- 动态控制大小,或者宽高
- 动态控制颜色
- 支持传入常见一些属性和事件
所以,我们的模版代码大概如下:
typescript
import React from "react";
export interface IconProps extends React.SVGProps<SVGSVGElement> {
fontSize: string;
color?: string;
}
export const DeleteIcon: React.FC<IconProps> = (props) => {
const { fontSize, color, width, height, ...rest } = props;
return (
<svg
fill={color}
width={fontSize || width}
height={fontSize || height}
{...rest}
>
<path
d="M33.9971919,11.9979065 L43.9985715,11.9979065 L43.9985715,16.0003331 L39.996145,16.0003331 L39.996145,43.9985728 L7.99547965,43.9985728"
></path>
</svg>
);
};
针对单色图标,我们可以简单的增加fontSize
和color
来让调用方去控制图标的大小和颜色。需要注意的是,由于SVG内会存在多个path
或者其他的一些图形,这些图形可能在设计的时候就已经被定义好了颜色,所以我们需要在转化组件之前,将图形里的fill
或者stroke
属性全部去除掉,这样才能统一的在顶层通过fill
属性来控制图标的颜,另外,为了更好的控制图形的大小,我们也需要把图形原来的width
和height
给去掉,最后再在顶层注入我们模版代码里的fontSize
变量。还需要注意的时候,SVG图形里的诸多属性,如果直接代入到React组件里面是会报错的,例如xmlns:link
等等,我们也需要将这些属性给去除掉。
怎么去掉呢?我们可以利用一个强大的SVG工具,SVGO(github.com/svg/svgo),他...%25EF%25BC%258C%25E4%25BB%2596%25E9%2587%258C%25E9%259D%25A2%25E5%258C%2585%25E5%2590%25AB%25E4%25BA%2586%25E5%25BC%25BA%25E5%25A4%25A7%25E7%259A%2584%25E5%25A4%2584%25E7%2590%2586SVG%25E5%2590%2584%25E7%25A7%258D%25E5%25B1%259E%25E6%2580%25A7%25E7%259A%2584%25E9%259C%2580%25E6%25B1%2582%25EF%25BC%258C%25E5%2585%25B6%25E4%25B8%25AD%25E5%25B0%25B1%25E5%258C%2585%25E5%2590%25AB%25E4%25BA%2586%25E6%2588%2591%25E4%25BB%25AC%25E4%25B8%258A%25E8%25BF%25B0%25E7%259A%2584%25E5%258A%259F%25E8%2583%25BD%25EF%25BC%259A "https://github.com/svg/svgo)%EF%BC%8C%E4%BB%96%E9%87%8C%E9%9D%A2%E5%8C%85%E5%90%AB%E4%BA%86%E5%BC%BA%E5%A4%A7%E7%9A%84%E5%A4%84%E7%90%86SVG%E5%90%84%E7%A7%8D%E5%B1%9E%E6%80%A7%E7%9A%84%E9%9C%80%E6%B1%82%EF%BC%8C%E5%85%B6%E4%B8%AD%E5%B0%B1%E5%8C%85%E5%90%AB%E4%BA%86%E6%88%91%E4%BB%AC%E4%B8%8A%E8%BF%B0%E7%9A%84%E5%8A%9F%E8%83%BD%EF%BC%9A")
js
const result = optimize(fileValue, {
plugins: [
"cleanupAttrs",
"removeComments",
"removeXMLProcInst",
"removeDoctype",
"removeUselessStrokeAndFill",
"removeStyleElement",
"removeTitle",
{
name: "removeAttrs",
params: {
attrs:
"(width|height|xmlns|stroke|fill|class|version|fill-rule|xmlns.*|xml.*)",
},
},
{
name: "mergePaths",
params: {
force: true,
},
},
],
});
上述代码中,我们就可以快速的清除掉众多我们并不需要的属性。在拿到我们一个相对干净的SVG代码之后,我们再把利用正则把核心的SVG代码和本身的viewBox
给提取出来。
js
// 利用正则提取viewBox
const viewBox = svgValue.match(/(?<=viewBox=").*?(?=\")/).length
? res.match(/(?<=viewBox=").*?(?=\")/)[0]
: "0 0 1024 1024";
const createComponent = (name, viewBox, svg) => {
const _svg = svg.match(/<svg.*?<\/svg>/gi)
// 提取svg标签包裹的内容。
const svgContent = _svg[0].substring(_svg[0].indexOf('>') + 1).split('</svg>')[0]
const component = `
export const ${name} = (props: IconProps) => {
const { fontSize, color, width, height, ...rest } = props;
return (
<svg
viewBox="${viewBox}"
fill={color}
width={fontSize || width}
height={fontSize || height}
{...rest}
>
${svgContent}
</svg>
)
}
`
return component;
};
这样,一个简单的单色SVG组就转化完成了。
完成多色图标的换色功能
但是,如果我们把功能点再扩展一下,假设UI同学上传了一个双色图标或者一个多色图标,我们还能不能利用类似的代码去实现一个多色图标组件,并且支持多色图标的换色呢? 其实也是可以的,而且实现起来也不难。 以一个双色图标为例:
html
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path fill="#D9D9D9" d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zm0-448H184V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136z"/>
<path fill="#D9D9D9" d="M712 304c0 4.4-3.6 8-8 8h-56c-4.4 0-8-3.6-8-8v-48H384v48c0 4.4-3.6 8-8 8h-56c-4.4 0-8-3.6-8-8v-48H184v136h656V256H712v48z"/>
<path fill="#cccccc" d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zm0-448H184V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136z"/>
</svg>
我们可以看到,这里存在三个path
路径,并且都填充了两种颜色,分别是#D9D9D9
和#cccccc
,那么,我们可以非常容易的想到,只要把将用到这两个颜色的地方全部都用变量去代替,让调用方用入参的形式去控制他们的值不就可以做到双色图标的换色了吗,换言之,任意一个多色图标都可以用这种方法去做到换色,区别只在于控制颜色的多少罢了。
js
const transFormSvgColor = (svg, mainColor, subColor) => {
if (mainColor) {
svg = svg.replace(
new RegExp(`"${mainColor}"`, "g"),
`{mainColor || '${mainColor}'}`
); // 带入主色
}
if (subColor) {
svg = svg.replace(
new RegExp(`"${subColor}"`, "g"),
`{subColor || '${subColor}'}`
); // 带入副色 //
}
return svg;
};
代码其实非常容易理解,以双色图标为例,我们在组件模版代码上新增两个mainColor
和subColor
的入参,这两个入参的默认值就是该双色图标原来的颜色,如上述例子里的#D9D9D9
和#cccccc
,然后将svg里面用到这两个颜色的地方,全部换成变量。 所以,针对双色图标,我们的模版代码则是:
ini
const createTwoToneComponent = (name, viewBox, svg) => {
const _svg = svg.match(/<svg.*?<\/svg>/gi)
// 提取svg标签包裹的内容。
const svgContent = _svg[0].substring(_svg[0].indexOf('>') + 1).split('</svg>')[0]
const component = `
export const ${name} = (props: IconProps) => {
const { fontSize, color, width, height, mainColor, subColor ...rest } = props;
return (
<svg
viewBox="${viewBox}"
width={fontSize || width}
height={fontSize || height}
{...rest}
>
${svgContent}
</svg>
)
}
`
return component;
};
这样,我们就可以完成双色图标的换色功能了。
React属性处理
在完成组件的转化之后,当我们在开发环境去调用这个组件的时候,会发现控制台可能会报出许多警告,有时候甚至会弹出许多报错。为什么呢?主要是因为:
- React里的标签属性需要变成小驼峰命名,而Svg本身的属性名并不是。
- 原生标签里的class需要转化为className
- React组件不支持内置标签。
针对第一和第二点,我们仅需要利用正则去全局替换掉即可:
js
/**
* 适配React 将参数转为驼峰命名
* @param {*} svgString
* @returns
*/
const parseReactApi = async (svgString) => {
const reactApiConfig = {
"xlink:href": "href",
"stroke-width": "strokeWidth",
"fill-rule": "fillRule",
"fill-opacity": "fillOpacity",
"stop-color": "stopColor",
"class": "className"
}
if (reactApiConfig) {
Object.keys(reactApiConfig).forEach((key) => {
res = res.replace(new RegExp(`${key}`, "g"), `${reactApiConfig[key]}`);
});
}
return res;
};
而对于第三点,我们并不能简单粗暴的直接将给删除掉,因为style标签里的内容是则是对某些图层的样式定义,如果直接去掉的话,图层的样式就会发生改变。怎么解决呢?其实很简单,我们只需要将style标签的样式转化为行内样式就可以了,利用SVGO就可以很简单的做到。
js
const result = optimize(fileValue, {
plugins: [
"inlineStyles",
],
});
避免图层彼此影响
如果经常使用复杂的SVG多色图标的话,很容易注意到一个问题,就是图形之间的复用,在带来便利的同时,也会带来诸多问题。 我们来看个例子:
html
<svg width="56px" height="56px" viewBox="0 0 56 56" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient x1="11.4130931%" y1="18.2153362%" x2="79.6703028%" y2="79.9212994%" id="custom-1">
<stop stop-color="#FFBC00" offset="0%"></stop>
<stop stop-color="#FF9400" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-435.000000, -626.000000)">
<g transform="translate(435.000000, 626.000000)">
<use id="形状结合" fill="url(#custom-1)"></use>
</g>
</g>
</g>
</svg>
<svg width="56px" height="56px" viewBox="0 0 56 56" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="custom-1">
<stop stop-color="#FFFFFF" stop-opacity="0.561325393" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-435.000000, -626.000000)">
<g transform="translate(435.000000, 626.000000)">
<use id="形状结合" fill="url(#custom-1)"></use>
</g>
</g>
</g>
</svg>
可以看到,上述的两个SVG元素,都在defs
里定义了一个可复用的图形,并且ID都是custom-1
,这样的话,如果他们在不同的页面单独使用的时候,其实不会有任何问题。但是如果他们同时出现在一个页面内,后出现的SVG在寻找图层的时候,就会找到先出现的SVG里的ID为custom-1
的图层。换句话来说,图层的寻找是整个页面内从上到下的,如果两个SVG元素的内定义的图形ID一致,就会导致图形复用时出现偏差,随之出现问题。
怎么解决呢?其实很简单,我们只需要保证在生成组件时,组件内的图层ID都是唯一的就好了,我们还是可以利用SVGO去实现这个功能。
js
// 生成随机id
const randomId = () => {
return Math.random().toString(36).substr(2, 8);
};
const result = optimize(svg, {
plugins: [
{
name: "prefixIds",
params: {
prefix: randomId() + SvgComponentName,
},
},
],
});
我们可以生成一个随机ID,拼接上我们的图标名称,再作为前缀拼接到到每个图层ID的前面就好了。