实战系列 | 手把手带你打造一个专属图标库

1,455 阅读11分钟

前言

同学,你好!我是 嘟老板。在开发前端应用时,图标 是一个不可或缺的视觉元素。它们不仅能够提升界面的美观度,还能以简洁直观的方式传达功能和信息,增强用户体验。很多时候,一个 合适的图标 甚至可以胜过长篇累牍的文字描述。今天,我们就来打造一款使用高效的图标库 —— SVG 图标库

SVG(Scalable Vector Graphics) 图标库因其可伸缩性、清晰度和灵活性,成为了现代前端开发中的优选方案。

SVG 格式的图标有以下优势:

  • 无限缩放SVG 图标可以在不同分辨率下保持清晰,适合多种设备和屏幕尺寸。
  • 文件大小:通常比位图图标(如 PNGJPEG)小,有助于减少加载时间和带宽消耗。
  • 样式定制SVG 可以通过 CSSJavaScript 进行样式和行为的定制,提供更高的灵活性。
  • 可访问性SVG 图标可以包含标签和描述,提高网站的可访问性。

目前比较常用的 UI 框架都配有图标库,比如 ElementPlus@element-plus/icons-vueAnt Design@ant-design/icons,它们的存在为我们的开发工作提供了不少便利。

阅读本文您将获得:

  1. 梳理 svg 图标库用到的库。
  2. vue 框架图标库设计思路及完整实现过程。
  3. 图标库打包过程及 playground 应用图标。

前期准备

技术准备

以下是图标库主要应用的依赖:

peerDependencies

  • vue: 图标库是基于 Vue 框架的,不用多说。

构建类

命令行类

  • tsx: 用于在 node 环境执行 typescript 脚本。
  • vue-tsc: Vue3 命令行类型检查工具,可用于为 Vue 组件生成类型文件(.d.ts)。
  • npm-run-all: 并行或顺序执行多个 npm script 命令的工具。

素材准备

素材很简单,准备需要封装的 svg 图标即可。

我预先准备了一个图标,没有素材的话可以直接 copy 到 .svg 文件中:

<svg t="1710472412593" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2183" width="200" height="200"><path d="M930.8 768.3c-19.5-24.8-53.1-28.4-70.9-24.8-29.8 6-59.3 28.8-71.8 39.4h-14.8c7.8-8.5 15.1-17 21.8-25.6 19.6-25.2 33-49.2 42-71.5 2.7 0.3 5.4 0.5 8.2 0.5 45.7 0 82.9-40.3 82.9-89.8s-37.2-89.8-82.9-89.8c-7.8 0-15.5 1.2-23 3.5-19.3-28.7-46.8-51.8-79-66.6 14.6-31.5 22.3-66 22.3-100.9 0-27.3-4.6-53.6-13.2-78.2 40.4-14.3 68.7-52.7 68.7-97.5 0-57.1-46.4-103.5-103.5-103.5-40.4 0-76.5 23.7-93.4 59-33.6-16-71.3-25.1-111.2-25.1-39.9 0-77.6 9-111.2 25.1-16.9-35.2-53-59-93.4-59-57.1 0-103.5 46.4-103.5 103.5 0 44.1 28.1 83 68.7 97.5-8.5 24.6-13.2 50.9-13.2 78.3 0 34.9 7.7 69.3 22.3 100.9-32.2 14.8-59.7 37.9-79 66.7-7.4-2.3-15.1-3.6-23-3.6-45.7 0-82.9 40.3-82.9 89.8s37.2 89.8 82.9 89.8c2.8 0 5.5-0.2 8.2-0.5 8.9 22.3 22.4 46.3 42 71.5 6.7 8.6 14 17.2 21.8 25.6h-14.8c-12.5-10.6-41.9-33.4-71.8-39.4-17.8-3.6-51.4 0.1-70.9 24.8-13.3 16.9-23.8 48.8 2.2 104.4 42.4 90.8 158.5 87.3 169.4 86.7 0 0 496 0.2 499.6 0.2 28.6 0 124.4-6.2 162.1-86.9 26-55.7 15.6-87.5 2.3-104.5z" fill="#E66978" p-id="2184"></path><path d="M810.9 579.4c-14.4-65.9-79.4-115.6-151.3-115.6-51.5 0-98 25.2-131 71-3.6 5-9.4 8-15.6 8-6.2 0-12-3-15.6-8-33-45.8-79.5-71-131-71-71.9 0-137 49.7-151.3 115.6-24.2 110.9 87.1 235.2 297.9 333.4 210.9-98.2 322.1-222.5 297.9-333.4z" fill="#FFAEBF" p-id="2185"></path><path d="M845.3 545.2c24.5 0 44.4 23 44.4 51.3 0 27-18.2 49.1-41.1 51.1 6.1-30.2 4.3-56.2-0.1-76.4-1.9-8.8-4.6-17.4-7.8-25.7 1.5-0.2 3.1-0.3 4.6-0.3zM177.5 571.2c-4.4 20.2-6.2 46.2-0.1 76.4-23-2-41.1-24.1-41.1-51.1 0-28.3 19.9-51.3 44.4-51.3 1.5 0 3.1 0.1 4.6 0.3-3.2 8.3-5.8 16.9-7.8 25.7z" fill="#FFD976" p-id="2186"></path><path d="M215.1 579.4c14.4-65.9 79.4-115.6 151.3-115.6 51.5 0 98 25.2 131 71 3.6 5 9.4 8 15.6 8 6.2 0 12-3 15.6-8 33-45.8 79.5-71 131-71 71.9 0 136.9 49.7 151.3 115.6 24.2 110.9-87.1 235.2-297.9 333.4-210.8-98.2-322.1-222.5-297.9-333.4z" fill="#FFAEBF" p-id="2187"></path><path d="M717.6 102c35.9 0 65.1 29.2 65.1 65.1 0 29.1-19 54-45.8 62.2-18.9-34.9-46.1-65-79.2-87.5 10-23.6 33.5-39.8 59.9-39.8zM513 135.9c118.1 0 214.2 92.8 214.2 206.9 0 30.5-7 60.8-20.4 88.2-15.2-3.7-31-5.6-47.1-5.6-56.3 0-107.6 23.8-146.6 67.5-39-43.7-90.3-67.5-146.6-67.5-16.2 0-32 2-47.2 5.6-13.4-27.4-20.4-57.6-20.4-88.2 0-114.1 96-206.9 214.1-206.9zM243.4 167.1c0-35.9 29.2-65.1 65.1-65.1 26.4 0 49.9 16.1 59.9 39.8-33.1 22.5-60.3 52.5-79.2 87.4-26.9-8.3-45.8-33.4-45.8-62.1zM265.2 921c-1 0.1-100.2 5.3-132.9-64.6-13.8-29.6-16.2-52.4-6.8-64.4 8.4-10.7 25.7-12.4 33.1-10.9 22.4 4.5 50.1 26.8 58.9 35 3.6 3.3 8.3 5.2 13.1 5.2h62c40.4 35 90.7 68.3 150.3 99.7H265.2zM893.7 856.5c-32.4 69.5-131.9 64.6-132.9 64.6H583.1c59.6-31.4 109.9-64.7 150.3-99.7h62c4.9 0 9.6-1.9 13.1-5.2 8.8-8.2 36.6-30.5 59-35 7.4-1.5 24.6 0.2 33.1 10.9 9.3 11.9 6.9 34.8-6.9 64.4z" fill="#FFD976" p-id="2188"></path><path d="M513 442c60.3 0 109.3-41.9 109.3-93.3 0-51.5-49-93.3-109.3-93.3s-109.3 41.9-109.3 93.3S452.7 442 513 442z" fill="#E66978" p-id="2189"></path><path d="M513 293.8c39.1 0 70.9 24.6 70.9 54.9 0 30.3-31.8 54.9-70.9 54.9-39.1 0-70.9-24.6-70.9-54.9 0.1-30.3 31.8-54.9 70.9-54.9z" fill="#FFF4E5" p-id="2190"></path><path d="M427.8 272.6c5.1 0 10-2.1 13.6-5.6 3.6-3.6 5.6-8.5 5.6-13.6s-2.1-10-5.6-13.6c-3.6-3.6-8.5-5.6-13.6-5.6s-10 2-13.6 5.6c-3.6 3.6-5.6 8.5-5.6 13.6s2.1 10 5.6 13.6c3.6 3.6 8.5 5.6 13.6 5.6zM603.1 272.6c5.1 0 10-2.1 13.6-5.6 3.6-3.6 5.6-8.5 5.6-13.6s-2.1-10-5.6-13.6c-3.6-3.6-8.5-5.6-13.6-5.6-5 0-10 2-13.6 5.6-3.6 3.6-5.6 8.5-5.6 13.6s2.1 10 5.6 13.6c3.6 3.6 8.5 5.6 13.6 5.6z" fill="#E66978" p-id="2191"></path></svg>

是一个粉色的爱心小熊。

image.png

设计思路

现在做个实验,我们直接将 svg 源代码写到 .vue 文件的 template 标签下,能否正常渲染?

<template>
  <svg
    t="1710472412593"
    class="icon"
    viewBox="0 0 1024 1024"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    p-id="2183"
    width="200"
    height="200"
  >
    <path
      d="M930.8 768.3c-19.5-24.8-53.1-28.4-70.9-24.8-29.8 6-59.3 28.8-71.8 39.4h-14.8c7.8-8.5 15.1-17 21.8-25.6 19.6-25.2 33-49.2 42-71.5 2.7 0.3 5.4 0.5 8.2 0.5 45.7 0 82.9-40.3 82.9-89.8s-37.2-89.8-82.9-89.8c-7.8 0-15.5 1.2-23 3.5-19.3-28.7-46.8-51.8-79-66.6 14.6-31.5 22.3-66 22.3-100.9 0-27.3-4.6-53.6-13.2-78.2 40.4-14.3 68.7-52.7 68.7-97.5 0-57.1-46.4-103.5-103.5-103.5-40.4 0-76.5 23.7-93.4 59-33.6-16-71.3-25.1-111.2-25.1-39.9 0-77.6 9-111.2 25.1-16.9-35.2-53-59-93.4-59-57.1 0-103.5 46.4-103.5 103.5 0 44.1 28.1 83 68.7 97.5-8.5 24.6-13.2 50.9-13.2 78.3 0 34.9 7.7 69.3 22.3 100.9-32.2 14.8-59.7 37.9-79 66.7-7.4-2.3-15.1-3.6-23-3.6-45.7 0-82.9 40.3-82.9 89.8s37.2 89.8 82.9 89.8c2.8 0 5.5-0.2 8.2-0.5 8.9 22.3 22.4 46.3 42 71.5 6.7 8.6 14 17.2 21.8 25.6h-14.8c-12.5-10.6-41.9-33.4-71.8-39.4-17.8-3.6-51.4 0.1-70.9 24.8-13.3 16.9-23.8 48.8 2.2 104.4 42.4 90.8 158.5 87.3 169.4 86.7 0 0 496 0.2 499.6 0.2 28.6 0 124.4-6.2 162.1-86.9 26-55.7 15.6-87.5 2.3-104.5z"
      fill="#E66978"
      p-id="2184"
    ></path>
    <path
      d="M810.9 579.4c-14.4-65.9-79.4-115.6-151.3-115.6-51.5 0-98 25.2-131 71-3.6 5-9.4 8-15.6 8-6.2 0-12-3-15.6-8-33-45.8-79.5-71-131-71-71.9 0-137 49.7-151.3 115.6-24.2 110.9 87.1 235.2 297.9 333.4 210.9-98.2 322.1-222.5 297.9-333.4z"
      fill="#FFAEBF"
      p-id="2185"
    ></path>
    <path
      d="M845.3 545.2c24.5 0 44.4 23 44.4 51.3 0 27-18.2 49.1-41.1 51.1 6.1-30.2 4.3-56.2-0.1-76.4-1.9-8.8-4.6-17.4-7.8-25.7 1.5-0.2 3.1-0.3 4.6-0.3zM177.5 571.2c-4.4 20.2-6.2 46.2-0.1 76.4-23-2-41.1-24.1-41.1-51.1 0-28.3 19.9-51.3 44.4-51.3 1.5 0 3.1 0.1 4.6 0.3-3.2 8.3-5.8 16.9-7.8 25.7z"
      fill="#FFD976"
      p-id="2186"
    ></path>
    <path
      d="M215.1 579.4c14.4-65.9 79.4-115.6 151.3-115.6 51.5 0 98 25.2 131 71 3.6 5 9.4 8 15.6 8 6.2 0 12-3 15.6-8 33-45.8 79.5-71 131-71 71.9 0 136.9 49.7 151.3 115.6 24.2 110.9-87.1 235.2-297.9 333.4-210.8-98.2-322.1-222.5-297.9-333.4z"
      fill="#FFAEBF"
      p-id="2187"
    ></path>
    <path
      d="M717.6 102c35.9 0 65.1 29.2 65.1 65.1 0 29.1-19 54-45.8 62.2-18.9-34.9-46.1-65-79.2-87.5 10-23.6 33.5-39.8 59.9-39.8zM513 135.9c118.1 0 214.2 92.8 214.2 206.9 0 30.5-7 60.8-20.4 88.2-15.2-3.7-31-5.6-47.1-5.6-56.3 0-107.6 23.8-146.6 67.5-39-43.7-90.3-67.5-146.6-67.5-16.2 0-32 2-47.2 5.6-13.4-27.4-20.4-57.6-20.4-88.2 0-114.1 96-206.9 214.1-206.9zM243.4 167.1c0-35.9 29.2-65.1 65.1-65.1 26.4 0 49.9 16.1 59.9 39.8-33.1 22.5-60.3 52.5-79.2 87.4-26.9-8.3-45.8-33.4-45.8-62.1zM265.2 921c-1 0.1-100.2 5.3-132.9-64.6-13.8-29.6-16.2-52.4-6.8-64.4 8.4-10.7 25.7-12.4 33.1-10.9 22.4 4.5 50.1 26.8 58.9 35 3.6 3.3 8.3 5.2 13.1 5.2h62c40.4 35 90.7 68.3 150.3 99.7H265.2zM893.7 856.5c-32.4 69.5-131.9 64.6-132.9 64.6H583.1c59.6-31.4 109.9-64.7 150.3-99.7h62c4.9 0 9.6-1.9 13.1-5.2 8.8-8.2 36.6-30.5 59-35 7.4-1.5 24.6 0.2 33.1 10.9 9.3 11.9 6.9 34.8-6.9 64.4z"
      fill="#FFD976"
      p-id="2188"
    ></path>
    <path
      d="M513 442c60.3 0 109.3-41.9 109.3-93.3 0-51.5-49-93.3-109.3-93.3s-109.3 41.9-109.3 93.3S452.7 442 513 442z"
      fill="#E66978"
      p-id="2189"
    ></path>
    <path
      d="M513 293.8c39.1 0 70.9 24.6 70.9 54.9 0 30.3-31.8 54.9-70.9 54.9-39.1 0-70.9-24.6-70.9-54.9 0.1-30.3 31.8-54.9 70.9-54.9z"
      fill="#FFF4E5"
      p-id="2190"
    ></path>
    <path
      d="M427.8 272.6c5.1 0 10-2.1 13.6-5.6 3.6-3.6 5.6-8.5 5.6-13.6s-2.1-10-5.6-13.6c-3.6-3.6-8.5-5.6-13.6-5.6s-10 2-13.6 5.6c-3.6 3.6-5.6 8.5-5.6 13.6s2.1 10 5.6 13.6c3.6 3.6 8.5 5.6 13.6 5.6zM603.1 272.6c5.1 0 10-2.1 13.6-5.6 3.6-3.6 5.6-8.5 5.6-13.6s-2.1-10-5.6-13.6c-3.6-3.6-8.5-5.6-13.6-5.6-5 0-10 2-13.6 5.6-3.6 3.6-5.6 8.5-5.6 13.6s2.1 10 5.6 13.6c3.6 3.6 8.5 5.6 13.6 5.6z"
      fill="#E66978"
      p-id="2191"
    ></path>
  </svg>
</template>

<script setup></script>

答案是可以,运行项目小熊图标正常渲染。

image.png

这便是图标库的基础原理,借助 Vue 单文件组件(SFC) 可以直接渲染 svg 的特性。

以下是图标库将 svg 转换为 vue 组件的大致流程:

  1. 新建 svg 目录,专门放置 svg 图标素材。
  2. 读取 svg 目录下所有文件,分别将 svg 源代码插入到特定的 vue 模板代码中,写入同名的 .vue 文件并放置到 components 目录下。
  3. 生成 components/index.ts 文件,作为图标组件的入口文件,导出 components 文件夹下的所有 vue 组件。
  4. 在图标库入口文件中(通常 src/index.ts),默认导出 install 函数,用于使用 app.use() 安装;同时导出所有图标组件,供按需导入。

搭建过程

大致步骤清楚了,现在我们开始逐步实现。

项目结构

我们先创建一个名为 zimu-icons 的项目,目录结构如下:

|-- build - 用来放置组件生成及图标库构建相关文件;
|-- src - 方式所有源代码,包括图标库入口 index.ts 及根据 svg 生成的 components 目录及 Vue 组件。
|-- |-- index.ts - 图标库入口文件,主要导出 install 函数及组件。
|-- svg - 放置 svg 素材。
|-- package.json - npm 配置文件。
|-- tsconfig.json - typescript 配置文件
|-- tsconfig.build.json - 可选,用于约束 build 目录文件的 ts 配置项。

初始化方法:

  1. 项目根目录下,执行 npm init,生成默认的 package.json 文件。
  2. 同根目录下,执行 tsc -init,自动创建 tsconfig.json 文件。前提需要全局安装 tsc 包,若没有安装过,执行时会提示安装。
  3. tsconfig.build.json 看情况拆分,通过 include 配置项限定生效范围,如 { "include": ["build", "package.json"] }
  4. 将上面的小熊素材加入到 src/svg 目录下。

到这,项目结构就算基本成型了。

生成图标组件

我们将组件生成的逻辑,单独放到 build/generate 文件中。

想一想,我们需要生成哪些东西?

  • 每个图标对应的 Vue 组件
  • components 目录的入口文件 index.ts,并在该文件中导出所有 Vue 组件

以上就是 generate 要生成的全部内容,我们挨个实现:

生成 Vue 组件

生成 Vue 组件可以拆解成两步:

1. 首先获取 svg 目录下所有的图标文件

import glob from 'fast-glob'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

// 当前程序执行的目录,即 build
const dir = dirname(fileURLToPath(import.meta.url))
// 根目录
export const pathRoot = resolve(dir, '..')
// src 目录
export const pathSrc = resolve(pathRoot, 'src')
// svg 资源目录
export const pathSvg = resolve(pathRoot, 'svg')

/**
 * 获取 svg 文件
 */
function getSvgFiles() {
  return glob('*.svg', { cwd: pathSvg, absolute: true })
}

要想获取 svg 文件,首先要确定 svg 目录的位置。通过当前文件的位置,向上找到 根目录,然后找到根目录下的 svg 目录

获取到 svg 目录 的位置,就可以借助 fast-glob 的能力,获取目录下的所有 svg 文件

2. 获取 svg 图标文件的内容

import { readFile, writeFile } from 'node:fs/promises'

/**
 * 将 file 转换为 vue 组件
 * @param file 待转换的 file 路径
 */
async function transformToVueComponent(file: string) {
    // 读取 svg 图标文件内容
    const content = await readFile(file)
}

借助 node 内置文件模块 fs 提供的 readFile api,读取 svg 文件的内容。

3. 截取 svg 图标文件的名称,作为 Vue 组件的名称

import path from 'node:path'
import camelcase from 'camelcase'

/**
 * 从文件路径中获取文件名及组件名
 * @param file 文件路径
 * @returns
 */
function getName(file: string) {
  const fileName = path.basename(file).replace('.svg', '')
  const componentName = camelcase(fileName, { pascalCase: true })
  return {
    fileName,
    componentName
  }
}

借助 node 内置 path 模块提供的 basename api,获取 svg 完整文件名,然后将 .svg 后缀替换为 空字符串,仅保留名称,作为 Vue 组件的文件名称。

然后使用 camelcase 库,将文件名称转换为大写驼峰格式,用于作为组件名。

注意:
文件名称 是指 vue 文件,即以 .vue 为后缀的文件名;
组件名称 是指显示为组件定义的名称,即 option api 中的 name 选项。

4. 组合 Vue 组件代码

import { format } from 'prettier'

/**
 * 按照给定解析器格式化代码
 * @param code 待格式化代码
 * @param parser 解析器类型
 * @returns 格式化后的代码
 */
function formatCode(code: string, parser: BuiltInParserName = 'typescript') {
  return format(code, {
    parser,
    semi: false,
    trailingComma: 'none',
    singleQuote: true
  })
}

/**
 * 将 svg file 转换为 vue 组件
 * @param file 待转换的 file 路径
 */
async function transformToVueComponent(file: string) {
  const content = await readFile(file)
  const { fileName, componentName } = getName(file)

  const vue = formatCode(
    `<template> ${content} </template>
    <script lang="ts">
      import { defineComponent } from 'vue'
      export default defineComponent({
        name: '${componentName}'
      })
    </script>`,
    'vue'
  )
}

首先通过模板字符串,将 svg 图标内容 和固定的 vue 组件模板 整合成一个字符串;然后借助 prettier 提供的 format api,进行代码格式优化。

ps:
vue 默认的模板非固定,可灵活制定;使用 setup 模式,通过 defineOptions 定义组件名称也是可以的。

5. 将结果代码写入到 .vue 文件中

借助 node 内置的 fs 模块 提供的 writeFile api,将第 4 步处理好的 Vue 组件内容,写入到 .vue 文件中。

import { readFile, writeFile } from 'node:fs/promises'

/**
 * 将 file 转换为 vue 组件
 * @param file 待转换的 file 路径
 */
async function transformToVueComponent(file: string) {
  const content = await readFile(file, 'utf-8')
  const { fileName, componentName } = getName(file)

  const vue = formatCode(
    `<template> ${content} </template>
    <script lang="ts">
      import { defineComponent } from 'vue'
      export default defineComponent({
        name: '${componentName}'
      })
    </script>`,
    'vue'
  )

  writeFile(path.resolve(pathComponents, `${fileName}.vue`), vue, 'utf-8')
}

到这,一个完整的 svg 图标Vue 组件 的过程就完成了。

生成入口文件 index.ts

入口文件用来干什么的呢?只做一件事,那就是将 components 内的所有 Vue 组件,统一导出

/**
 * 生成 components 入口文件
 */
const generateEntry = async (files: string[]) => {
  const code = formatCode(
    files
      .map(file => {
        const { fileName, componentName } = getName(file)
        return `export { default as ${componentName} } from './${fileName}.vue'`
      })
      .join('\n'),
      
  )
  await writeFile(path.resolve(pathComponents, 'index.ts'), code, 'utf-8')
}

内容很简单,参数 filessvg 文件数组,遍历 svg 文件,循环生成以下语句:export { default as ${componentName} } from './${fileName}.vue',即将对应的 Vue 组件 导出。

到这,生成组件的处理方法就全部写完了,接下来我们顺序调用即可。

import { emptyDir, ensureDir } from 'fs-extra'

console.log(chalk.blue('开始生成 Vue 图标组件................................'))
await ensureDir(pathComponents)
await emptyDir(pathComponents)
const files = await getSvgFiles()

console.log(chalk.blue('开始生成 Vue 文件................................'))
await Promise.all(files.map((file: string) => transformToVueComponent(file)))

console.log(
  chalk.blue('开始生成 Vue 组件入口文件................................')
)
await generateEntry(files)
console.log(chalk.green('Vue 图标组件已生成'))

细心的同学可能发现,在获取 svg 文件之前,加了两行代码,这属于程序的小优化,借助 fs-extraensureDiremptyDir api,在每次重新生成组件之前,验证路径是否存在及清空已有文件。

生成 Vue 组件的脚本代码已经搞定,添加一个 npm script,即可在命令行直接执行生成组件了。

{
  "scripts": {
     "build:generate": "tsx build/generate.ts",
  },
}

打包及应用

我们借助 esbuild 进行打包操作。在开始打包之前,我们先想想,为什么打包?我们现在图标库生成的 Vue 组件,只能在本项目使用,但这并不是封装图标库的目的,我们的目的是给到更多的项目用,这就需要 打包 的参与。

通过打包,可以将 图标库的组件转换成 JavaScript,供其他项目引入。在打包过程中,还可以通过 压缩tree-shaking 等手段对打包产物进行优化。

打包过程

我们将打包的逻辑,单独放到 build/build 文件中。

不需要过多的准备工作,直接创建一个打包函数,duBuild。函数中,借助 esbuildbuild api,执行打包工作。我们先以打包 esm(es module) 格式为例。除此之外,还可以打包 iife(立即执行函数)cjs(commonjs) 格式。

import { build } from 'esbuild'

/**
 * 执行构建
 * @param minify 是否需要压缩
 */
const doBuild = async (minify: boolean = true) => {
  await build({
    ...getBuildOptions('esm'),
    entryNames: `[name]${minify ? '.min' : ''}`,
    minify
  }) 
}

其中 getBuildOptions 函数,参数 format打包格式,即 esm,iife,cjs 三选一,主要用来获取相应的打包配置。

import type { BuildOptions, Format } from 'esbuild'
import vue from 'unplugin-vue/esbuild'

// 编译输出目录
export const pathOutput = resolve(pathRoot, 'dist')

/**
 * 获取 esbuild 构建配置项
 * @param format 打包格式,分为 esm,iife,cjs
 */
const getBuildOptions = (format: Format) => {
  const options: BuildOptions = {
    entryPoints: [path.resolve(pathSrc, 'index.ts')],
    target: 'es2018',
    platform: 'neutral',
    plugins: [
      vue({
        isProduction: true,
        sourceMap: false
      })
    ],
    bundle: true,
    format,
    minifySyntax: true,
    banner: {
      js: `/*! ZIMU Icons v${version} */\n`
    },
    outdir: pathOutput
  }
  if (format === 'iife') {
    options.plugins!.push(
      GlobalsPlugin({
        vue: 'Vue'
      })
    )
    options.globalName = 'ZIMUIcons'
  } else {
    options.external = ['vue']
  }

  return options
}

内容比较简单,就是给 esbuild 的部分构建配置预设值。包括构建入口插件输出目录等,其中针对 format 配置项做了特殊处理,若 formatiife 时,添加定义全局变量插件,将 Vue 设为全局变量并设置图标库的全局名称;否则将 vue 从构建中排除。

这是由于不同的打包结果,用法不同。其中 iife 需要通过全局变量使用,而另外两种格式,则是在安装了 vue 的环境中导入使用。

esbuild 全部配置详见 官网,可根据具体需求增减。

接下来,直接执行即可:

console.log(chalk.blue('开始编译................................'))
console.log(chalk.blue('清空 dist 目录................................'))
await emptyDir(pathOutput)
console.log(chalk.blue('构建中................................'))
await doBuild()
console.log(chalk.green('构建完成。'))

generate 一样,将执行语句添加到 npm script 中,以命令行执行:

{
  "scripts": {
     "build:build": "tsx build/build.ts",
  },
}

到这里就完了吗?nonono,我们既然用 ts 实现,当然要为图标组件生成类型文件啦。借助 vue-tsc 可以为 vue 组件直接生成对应的 .d.ts 类型文件。

方便起见,我们将生成组件类型的语句添加到 npm script 中,并将三个命令进行整合,仅通过 pnpm run build 便可一次性执行三个任务。

"scripts": {
    "build": "pnpm run build:generate && run-p build:build build:types",
    "build:generate": "tsx build/generate.ts",
    "build:build": "tsx build/build.ts",
    "build:types": "vue-tsc --declaration --emitDeclarationOnly"
  },

build 命令中,用到的 run-p 命令,实际上是 npm-run-all 包提供的能力。

现在执行 pnpm run build,运行成功后,便可在 输出目录(dist) 看到 类型目录(types) (类型输出路径在 tsconfig.json 中的 declarationDir 配置项定义)和 打包后的 js 文件 了。

image.png

image.png

ok,我们的图标库已经成型。接下来,我们创建个 playground 项目,来应用一下吧。

playground 项目

playground 项目主要用来验证图标可用性,简单为主。用 vite 创建一个初始工程即可。

图标库根目录下执行 pnpm create vite playground --template vue,按照提示操作,完成项目创建。

以下是我创建的 playground 项目,已删除多余的文件。

image.png

安装图标库,控制台执行命令:pnpm add @zimu/icons,安装后的 package.json 如下:

image.png

为什么图标库的依赖名是 @zimu/icons,这是在图标库 package.json 中的 name 属性配置的。 image.png

依赖安装后,在 src/main.js 中引入图标库。

import { createApp } from 'vue'
import ZimuIcons from '@zimu/icons'
import App from './App.vue'

createApp(App).use(ZimuIcons).mount('#app')

最后在 App.vue 中使用图标。

<template>
  <h1>我是粉色小熊:</h1>
  <pink-bear />
  <h1>我是所有图标:</h1>
  <component v-for="icon in icons" :key="icon.name" :is="icon"></component>
</template>

<script setup>
import { icons } from '@zimu/icons'
</script>

<style scoped></style>

OK,测试代码编写完毕,执行 pnpm dev 启动本地服务,浏览器打开 http://localhost:5173/ 看看效果:

image.png

完活,收工!!!

本来还想写写个人的有感而发,写了一大段最后还是删了,因为越写越偏离本文的主题,觉得没有存在的必要。

意思大致就是图标库这么简单的小项目,也涉及到诸如 esbuild,tsx,node 等多种技术,不懂就要各种看,各种学,属实艰难。只能说 前端之路,任重道远,共勉吧!!!

结语

好啦,今天的内容到这就结束了。本文从 前期准备搭建过程,再到 打包应用,比较详尽的讲述了图标库的整个搭建过程,希望对你有所帮助,相关代码已上传至 GitHub。感兴趣的同学快去行动起来,相信你能打造出更优质的图标库项目。

如果您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。

技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。


往期推荐