likes
comments
collection
share

Vue3 + Svg 一步步实现自己的图标库(提供源码)

作者站长头像
站长
· 阅读数 14

前言

在看 antdvnaive-ui 等组件库源码的时候,会发现它们都将组件库的图标单独抽成了一个 npm 包,并且可以通过组件的形式来引入,由于是通过组件的形式来引入, svg 以 dom 的形式渲染在浏览器上,相对于字体图标来说可以减少 http 请求,在低端设备上 svg 有更好的清晰度等优点。那么这些 svg 图标库是怎么创建的呢?我们一起往下看吧。

实战操作

一些准备

首先使用 vite 来搭建一个工程,包管理器选择了 pnpm 。然后在工程目录的 src 文件夹下创建一个 svg 文件夹,用于存放设计师给的 svg 文件,当然,为了学习,大家也可以到 iconfont 上面下载免费的 svg 图标。

Vue3 + Svg 一步步实现自己的图标库(提供源码)

接下来就要下载一些关键依赖,主要是以下依赖:

  • css-render ,一个实现 CSS-in-JS 的库,挺好用的。

  • execa,增强了 nodejs 子进程 child_process 的方法,相对于 nodejs 原生的子进程,该 API 使用起来更友好。

  • fs-extra,添加了原生 fs 模块中未包含的文件系统方法,并为 fs 模块的方法添加了 Promise 支持,使用起来更加友好。

  • v2s,将 Vue 转换成 script 。以可摇树优化的方式,将 vue 文件的 template 与 script 转换为 ts 、js 文件,也是很好用的。

项目中其它的依赖就略过了,相对比较简单,大家可自身在 github 或 npm 中搜索学习。

安装完项目的依赖后,就可以编写生成 svg 图标的脚本了,该脚本的实现也是本项目的核心。

脚本实现

首先在 package.json 文件的 scripts 实现两个脚本命令

{
  "scripts": {
    "clean": "rimraf lib es dist",
    "build:icons": "pnpm run clean && node ./scripts/build.js"
  },  
}

rimraf 是跨平台实现删除文件、文件夹的库,用于代替 linux 中的 rm -rf 命令。在执行构建图标的命令前将之前的构建结果删掉,可以保证每次的构建结果都是干净的,不受上次构建结果的影响。

接下来项目根目录下创建 scripts 文件夹,然后在 scripts 文件夹下创建 build.js 文件,用于编写构建脚本 。

由于 v2s 库依赖了 vue-template-compiler

Vue3 + Svg 一步步实现自己的图标库(提供源码)

而我们的工程中依赖了 Vue3 ,Vue3 与 vue-template-compiler 同时在依赖列表中会报版本冲突的错误:

Vue3 + Svg 一步步实现自己的图标库(提供源码)

会报错的原因在于 vue-template-compiler 中的一段代码,它会取当前自己的版本与 Vue 的版本对比,如果不一致,则会报版本不一致的错误:

Vue3 + Svg 一步步实现自己的图标库(提供源码)

所以在 scripts\build.js 中开头有这么一段代码:

// make sure vue template compiler do not throw error
require('vue').version = null

将 Vue 的版本号抹去,从而避免了这个问题。

然后,我们编写读取工程中 svg 文件的源码的函数。通常设计师给到我们的 svg 文件会包含很多没用的信息,比如编辑器元数据、注释、隐藏元素、默认值等,这些信息可以删去,而不影响 SVG 呈现结果的内容,所以我们得到工程中 svg 文件的源码后,还要对其进行一些清理,从而保证 svg 代码的干净。

读取工程中 svg 文件的源码,并对 svg 源码进行清理的函数实现如下:

function createSvgSanitizer(src) {
  this.removeAttr = (...attrs) => {
    src = removeAttr(src, ...attrs)
    return this
  }
  this.removeSvgAttr = (...attrs) => {
    src = removeSvgAttr(src, ...attrs)
    return this
  }
  this.removeComment = () => {
    src = removeComment(src)
    return this
  }
  this.removeUselessTags = () => {
    src = removeUselessTags(src)
    return this
  }
  this.refill = () => {
    src = refill(src)
    return this
  }
  this.svg = () => src
  return this
}

function readSvgs() {
  const svgFiles = readSvgDirectory(ICONS_DIR)
  return svgFiles.map(svgFile => {
    const name = path.basename(svgFile, '.svg')
    const normalizedName = normalizeName(name)
    const contents = readSvg(svgFile, ICONS_DIR).trim()
    const svgSanitizer = createSvgSanitizer(contents)
    svgSanitizer
      .removeComment()
      .removeUselessTags()
      .removeAttr('id')
      .removeSvgAttr('width', 'height')
      .refill()
    
    const svg = svgSanitizer.svg()
    return {
      name: normalizedName,
      svg
    }
  })
}

为了阅读的体验,这里就不贴完整的代码实现了,因为太长了,完整的代码可在后面提供的源码中查看。

获取到工程中干净的 svg 源码后,将每个 svg 文件的源码添加到 Vue 的 Template 模板中,从而将 svg 文件构造成 Vue 组件,然后放入临时文件目录中。然后借助 v2s 的能力将 Vue 组件转换成 ts 文件,就可以使用 TypeScript 提供的 tsc 命令将相关的 ts 文件都编译成 js ,最后将临时文件删掉,就完成了整个 svg 图标库的构建流程。构建的入口函数在 generateVue3

async function generateVue3(icons, basePath) {
  // 省略一些非核心代码
  for (const { name, svg } of icons) {
    await fse.writeFile(
      path.resolve(tempPath, `${name}.vue`),
      '<template>\n' +
        svg +
        '\n' +
      '</template>\n' +
      '<script lang="ts">\n' +
      `import { defineComponent } from 'vue'\n` +
      'export default defineComponent({\n' +
      `  name: '${name}'\n` +
      '})\n' +
      '</script>'
    )
  }
  await generateIndex(names, '.ts', '.vue', tempPath)
  await generateAsyncIndex(names, '.ts', '.vue', tempPath)
  const dir = await fse.readdir(tempPath)
  const paths = dir.map((fileName) => path.resolve(tempPath, fileName))
  await v2s(paths, {
    deleteSource: true,
    refactorVueImport: true
  })
  const compilerOptionsBase = {
    forceConsistentCasingInFileNames: true,
    moduleResolution: 'node',
    target: 'ES6',
    lib: ['ESNext', 'DOM'],
    types: [], // ignore @types/react, which causes error
    declaration: true
  }
  console.log('  tsc to vue3 (cjs)')
  await tsc(
    {
      include: ['_vue3/**/*'],
      compilerOptions: {
        ...compilerOptionsBase,
        outDir: 'vue3/lib',
        module: 'CommonJS'
      }
    },
    basePath
  )
  console.log('  copy cjs output to root')
  const cjsDir = await fse.readdir(path.resolve(basePath, 'vue3/lib'))
  for (const file of cjsDir) {
    await fse.copy(
      path.resolve(basePath, 'vue3/lib', file),
      path.resolve(basePath, 'vue3', file)
    )
  }
  console.log('  tsc to vue3 (esm)')
  await tsc(
    {
      include: ['_vue3/**/*'],
      compilerOptions: {
        ...compilerOptionsBase,
        outDir: 'vue3/es',
        module: 'ESNext'
      }
    },
    basePath
  )
  // remove _vue3
  console.log('  remove _vue3')
  await fse.remove(tempPath)
}

上面的 generateVue3 函数也不是完整的实现,这里主要讲述 svg 图标库的构建流程,完整的实现可在后面提供的源码中查看。

构建流程总结

Vue3 + Svg 一步步实现自己的图标库(提供源码)

整个流程还是比较简单的。

最后

以上就是本篇文章的全部内容,工程的源码可以在这里 zentvicons 查看。“纸上得来终觉浅,绝知此事要躬行”,大家赶紧自己尝试构建一个自己的图标库吧 ~