需求与目标

不知道大家平时在开发时候遇到 SVG 图标是怎么处理的,在我们项目之前是这样处理的:

第一步: 将 svg 原文件通过 这个网站工具压缩下代码

image.png

第二步: 将压缩后的代码粘贴到组件中,使用 Antd 自定义 Icon 的形式得到可用组件。

image.png

此时代码里有一些不符合 JSX 风格的属性,则需要手动改正。

为什么不使用 svgr 的方式呢?比如:

import { ReactComponent as SafeIcon } from "./safe.svg"

主要是我们需要考虑更多的灵活性,对 svg 的大小、颜色等有动态变化的需求。

所以目前采用了第一种方式。可能最开始将 svg 加入到组件时感觉还好,但是随着 svg 数量越来越多后,觉得这个工作十分繁琐,所以需要找一个方法是否能够优化这个流程,最好是自动化的。

选取工具:XML-JS

在我广泛收集资料、查看文档后,发现了一个宝贝库,那就是xml-js。这个库的介绍是这样的:

image.png

将 XML 格式转换成 对象或者 JSON 格式,那这不就好解决了吗。我们可以写一个 node 脚本,读取 svg 文件,再生成模板代码写入文件,这不就省事了嘛。

自动转化流程实现

先来看下实现最终效果后的项目目录结构

image.png

让我来解释一个这个是什么意思:

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。

image.png

其里面代码(自动生成的)是:

image.png

最后,我们只需加上这一句:

image.png

这里需要手动写,是因为后面这行代码在后续中基本不会改变了。

完整代码

// 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") ) }

让我们来到终端执行以下:

image.png

OK,执行成功,让我们看看生成的结果:

image.png

那我们再引入一下,再来看看显示效果:

image.png

image.png

Nice,完美收工!

后续优化

可以在package.json中加入 script 脚本命令:

image.png

后续只需要执行 pnpm icon:sync 就可以了,或者在其他地方寻找更好的时机执行。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。