编写自己的 SVG 图标库

8 天前

在做 one-react 组件库时,思考如何对组件库各组件中用到的图标作统一管理。在这之前,都是直接 「hard code」 写入 SVG code,一旦出现同一个图标在不同组件中引入时,本质上是在反复复制一段代码,后续图标更新时会产生不小的工作量,独立的图标库势在必行。

调研了几种社区常见方案:

iconfont

  • 这种方案在 SVG 图标文件的基础上,利用脚本生成几种不同格式的字体文件(ttf/woff/eot),亦可通过 iconfont.cn 管理图标文件,然后通过 @font-face 加载字体文件,最后在伪元素的 content 中填写对应图标的编码渲染出图标。
  • 兼容性好,理论上能兼容到 IE6。
  • 繁琐之处在于处理 SVG 生成多个字体文件,编写生成多个图标字体编码的 className 样式表,字体文件要按需加载会比较麻烦。

SVG Sprite

  • 通过将 SVG 文件合并成一个 SVG sprite 并加载到一个 display: none 的元素中,使用时则通过 SVG 的 use 传入图标 id 后渲染出图标。
  • 在 webpack 配置中引入 svg-sprite-loader 即可,配置简单,可按需加载和打包。
  • 若在组件库中使用该方案,组件库的用户在使用时,需自行在 webpack 配置中引入对应 loader,处理对应 node_modules 下组件库里的 SVG 文件。
  • svg-sprite-loader 有自己的 runtime(svg-baker-runtime)。

SVG to React

  • 可使用如 svg-react-loader,直接 require 一个 SVG 转换成 React 组件。
  • 同上一个方案,组件库若引入,用户也得额外配置 svg-react-loader
  • 可按需引入。

按需加载,不引入 runtime 和额外配置,从一个组件库来说,关乎其易用性,这样下来上述三种方案都各有优缺点。思来想去,最好是每个组件都是一个纯粹的 React 组件,其 render 返回 SVG 的 XML,也就是把 SVG icon 改写成一个 React 组件。类似这种:

carbon.89195b0ced56.png

重新梳理下思路,明确要做的事情:

  • 从 Sketch 等工具中导出绘制好的 SVG 文件。
  • 将 SVG 的 XML 写入对应的 React 组件的 render 函数中。
  • 将所有 component 通过 index.ts 统一 export 出来。
  • 通过标准的 Semantic Release 流程进行测试、打包和发布。

将 SVG 转化成 React 组件

上面提到了一个步骤「将 SVG 的 XML 写入对应 React 组件的 render 函数中」,最初验证整体方案可行性时,是手动复制粘贴的,但是我们现在要把这一步变成自动化的,即每次增加一个新图标,跑一个脚本即可生成对应的 React 组件。

刚好之前 star 过 svgr 这个项目,这个项目能基于 h2x 将 HTML 编译成 JSX,支持 custom template,支持 svgo 压缩 SVG,支持 Prettier,有个在线的 REPL 可以体验。

注意:本文初次整理时 svgr@v4 尚未发布,因此下方提到的 svgr 相关都是基于 svgr@v3 的,后续笔者考虑将本文的内容和关联的项目升级到最新版本的 svgr。

将从 Sketch 中导出的 SVG 文件放到 src/assets 目录下,通过 svgr 将 SVG 编译成 tsx 文件,并输出到 src/icons 目录中。

carbon2.4922058b713e.png

不得不提,svgr 的配置参数十分神奇,比如它支持 config 文件,但是 out-dir, ext 等参数必须通过命令行参数传进去。

其中用到的 custom template 如下:

carbon.c256ecd5d968.png

svgr.config.js 文件中定义好 SVG 标签上的 props,包括默认样式、sizefill 等。如下所示:

carbon3.8cf37d91b9ca.png

执行对应的 npm scripts,将 SVG 编译成 React 组件。如下示例:

carbon4.c2b73d016d83.png

将编译好的 React 组件统一 export

通过脚本,读取 src/icons 目录下的所有 tsx 文件,以 export { SvgAbc } from './icons/abc' 的形式写入 src/index.ts 中。

脚本代码如下:

carbon5.d68ed45c9e40.png

在 CI 阶段执行编译

我们期望的是每次新增或修改了 SVG 文件,无需本地手动执行脚本编译成 React 组件并 export,直接 commit 并 push,让 CI 把编译好的结果提交回来并执行发布流程。

  • build 之前先执行 npm run getIcons命令,将 SVG 文件转化成 React 组件并统一导出。
  • 使用 @semantic-release/git 自动提交在 CI 上编译生成的 .ts/.tsx 文件。
  • 发布到 NPM。

对应的 .travis.yml 配置如下:

carbon6.10638761aa75.png

通过 release.config.js 文件配置 semantic-release

carbon7.1fdda2fe7e93.png

semantic-release 默认的 plugins 有四个:

  • @semantic-release/commit-analyzer:基于 conventional-changelog 分析 commit 信息。
  • @semantic-release/release-notes-generator:基于 conventional-changelog 生成 release 日志的内容。
  • @semantic-release/npm:发布 NPM 包。
  • @semantic-release/github: 发布 GitHub release 并且写入 release 日志。

要注意的是需要将 @semantic-release/git 插件放在 @semantic-release/npm 之前执行,因为要保证 release 的版本对应的是已完成编译的代码,即基于 @semantic-release/git 自动 commit 后的最新版本。

配置好的 semantic-release 流程如下:

sr-flow-with-git.c9cdd397749f.png

完成上述配置后,@semantic-release/git 会在 CI 流程中,将生成新的组件代码 commit 回来:

semantic_release_git.3d317563974e.png

在 GitHub 上也能看到 bot 自动提交回来的 commit:

semantic_release_bot_commit.a129578a5d54.png

上图中合并 mr 时会触发一次 Travis CI build,而这个 build 中,若 bot 提交一个新的 commit 回来的话,会再产生一个新的 build,但这个 build 并不会发布代码,因为上一个 build 中的 release 已经是基于最新代码(包含这个 commit的),这个 build 看起来有点「浪费」。

至此,我们达到了目的,只需要将 SVG 文件放到 src/assets 目录下并提交到远程,其他的事就交给 CI 处理即可。

使用示例

carbon8.0fd0184fa214.png

完整的项目代码请见我的 GitHub Repo

0
推荐阅读