需求与目标
不知道大家平时在开发时候遇到 SVG 图标是怎么处理的,在我们项目之前是这样处理的:
第一步: 将 svg 原文件通过 这个网站工具压缩下代码
第二步: 将压缩后的代码粘贴到组件中,使用 Antd 自定义 Icon 的形式得到可用组件。
此时代码里有一些不符合 JSX 风格的属性,则需要手动改正。
为什么不使用 svgr 的方式呢?比如:
import { ReactComponent as SafeIcon } from "./safe.svg"
主要是我们需要考虑更多的灵活性,对 svg 的大小、颜色等有动态变化的需求。
所以目前采用了第一种方式。可能最开始将 svg 加入到组件时感觉还好,但是随着 svg 数量越来越多后,觉得这个工作十分繁琐,所以需要找一个方法是否能够优化这个流程,最好是自动化的。
选取工具:XML-JS
在我广泛收集资料、查看文档后,发现了一个宝贝库,那就是xml-js。这个库的介绍是这样的:
将 XML 格式转换成 对象或者 JSON 格式,那这不就好解决了吗。我们可以写一个 node 脚本,读取 svg 文件,再生成模板代码写入文件,这不就省事了嘛。
自动转化流程实现
先来看下实现最终效果后的项目目录结构:
让我来解释一个这个是什么意思:
scripts/generateIcon.js:这就是我们将要完成的脚本。src/assets/autoSyncSvgs:此目录包含我们所有的需要自动转成组件Icon的 svg。src/components/Icon/autoSyncSvgs:此目录将包含我们所有自动生成的所有组件Icon文件。第一步:清空目标文件夹
关键代码:
function clearTargetDir() {
if (fs.existsSync(autoSyncTargetUrl)) {
fs.rmSync(autoSyncTargetUrl, { recursive: true, force: true })
fs.mkdirSync(autoSyncTargetUrl)
}
}
为什么清空,因为脚本会读取所有 svg 文件,再生成到这个目录,避免有不必要的影响,所以清空。
第二步:生成组件 Icon 代码
关键代码:
function generateComponent() {
const svgs = fs.readdirSync(autoSyncOriginUrl)
svgs.forEach(svg => {
const svgSourceContent = fs.readFileSync(
path.join(autoSyncOriginUrl, svg),
{ encoding: "utf-8" }
)
const svgTargetCode = xml2js(svgSourceContent)
const svgTargetDom = transformSvgDom(svgTargetCode.elements)
const { componentName, codeFileName } = getName(svg)
const code = `import Icon from '@ant-design/icons';
import type { GetProps } from 'antd';
type CustomIconComponentProps = GetProps;
const SVGComponent = () => ${svgTargetDom};
export const ${componentName} = (props: Partial) => ;
`
fs.writeFileSync(path.join(autoSyncTargetUrl, codeFileName + ".tsx"), code)
})
}
function getName(fileName) {
const securityName = fileName.replace(/[-_]/g, "").replace(".svg", "")
const componentName =
securityName[0].toUpperCase() + securityName.substring(1)
const codeFileName = securityName[0].toLowerCase() + securityName.substring(1)
return {
componentName: `${componentName}Icon`,
codeFileName
}
}
第三步:生成入口文件
关键代码:
function generateEntry() {
const svgs = fs.readdirSync(autoSyncOriginUrl)
const exports = []
svgs.forEach(svg => {
const { codeFileName, componentName } = getName(svg)
exports.push(`export { ${componentName} } from './${codeFileName}';`)
})
fs.writeFileSync(
path.join(autoSyncTargetUrl, "index.tsx"),
exports.join("n")
)
}
这步执行完成后,会在src/components/Icon/autoSyncSvgs中生成一个index.tsx。
其里面代码(自动生成的)是:
最后,我们只需加上这一句:
这里需要手动写,是因为后面这行代码在后续中基本不会改变了。
完整代码
// scripts/generateIcon.js
const path = require("node:path")
const fs = require("node:fs")
const { xml2js } = require("xml-js")
const autoSyncOriginUrl = path.resolve(__dirname, "../src/assets/autoSyncSvgs")
const autoSyncTargetUrl = path.resolve(
__dirname,
"../src/components/Icon/autoSyncSvgs"
)
const tasks = [
{
desc: "清除 autoSyncSvgs 里所有文件",
task: clearTargetDir
},
{
desc: "在 autoSyncSvgs 生成新的 svg 组件",
task: generateComponent
},
{
desc: "生成入口文件",
task: generateEntry
}
]
tasks.forEach(item => {
item.task()
console.log("√ ", item.desc)
})
function clearTargetDir() {
if (fs.existsSync(autoSyncTargetUrl)) {
fs.rmSync(autoSyncTargetUrl, { recursive: true, force: true })
fs.mkdirSync(autoSyncTargetUrl)
}
}
function generateComponent() {
const svgs = fs.readdirSync(autoSyncOriginUrl)
svgs.forEach(svg => {
const svgSourceContent = fs.readFileSync(
path.join(autoSyncOriginUrl, svg),
{ encoding: "utf-8" }
)
const svgTargetCode = xml2js(svgSourceContent)
const svgTargetDom = transformSvgDom(svgTargetCode.elements)
const { componentName, codeFileName } = getName(svg)
const code = `import Icon from '@ant-design/icons';
import type { GetProps } from 'antd';
type CustomIconComponentProps = GetProps;
const SVGComponent = () => ${svgTargetDom};
export const ${componentName} = (props: Partial) => ;
`
fs.writeFileSync(path.join(autoSyncTargetUrl, codeFileName + ".tsx"), code)
})
}
function getName(fileName) {
const securityName = fileName.replace(/[-_]/g, "").replace(".svg", "")
const componentName =
securityName[0].toUpperCase() + securityName.substring(1)
const codeFileName = securityName[0].toLowerCase() + securityName.substring(1)
return {
componentName: `${componentName}Icon`,
codeFileName
}
}
function transformSvgDom(elements) {
if (!Array.isArray(elements)) return ""
return elements
.map(({ type, name, attributes, elements, text }) => {
if (type === "text") return text
if (type === "element") {
const attr = attributes
? Object.keys(attributes)
.map(key => {
const isInValidKey = [
"xmlns",
"xmlns:xlink",
"line-spacing",
"t",
"p-id",
"class"
].includes(key)
if (isInValidKey) return ""
// 转成大驼峰
const camelcaseKey = key
.split("-")
.map((k, index) =>
index === 0 ? k : k[0].toUpperCase() + k.substring(1)
)
.join("")
.split(":")
.map((k, index) =>
index === 0 ? k : k[0].toUpperCase() + k.substring(1)
)
.join("")
return `${camelcaseKey}="${attributes[key]}"`
})
.join(" ")
: ""
return `${name} ${attr}>${transformSvgDom(elements)}${name}>`
}
return ""
})
.join("")
}
function generateEntry() {
const svgs = fs.readdirSync(autoSyncOriginUrl)
const exports = []
svgs.forEach(svg => {
const { codeFileName, componentName } = getName(svg)
exports.push(`export { ${componentName} } from './${codeFileName}';`)
})
fs.writeFileSync(
path.join(autoSyncTargetUrl, "index.tsx"),
exports.join("n")
)
}
让我们来到终端执行以下:
OK,执行成功,让我们看看生成的结果:
那我们再引入一下,再来看看显示效果:
Nice,完美收工!
后续优化
可以在package.json中加入 script 脚本命令:
后续只需要执行 pnpm icon:sync 就可以了,或者在其他地方寻找更好的时机执行。