在中大型前端项目中,图标库(Icon Library)经常是一个被忽视的"隐形大头":
- 设计同学给了一整套 SVG 图标,全部集成进组件库;
- 实际业务只用到其中一小部分;
- 结果是:没怎么用,但每个页面都在为这堆"沉睡图标"付出体积成本。
这篇文章分享的是我在项目里做的一次 Icon 优化实践:通过一段 Node 脚本,自动收集项目中实际使用到的图标组件 ,生成统一入口文件,再在 Vue 应用中批量注册。
同时兼顾了两类使用方式:
- 组件模板中直接写
<ZeroIconXXX /> - 菜单、配置中用字符串
"ZeroIconXXX"表示 icon
文中的组件前缀统一用
ZeroIconXXX,可以理解为公司内部约定的一套 Icon 命名规范。
一、背景与问题
项目中有一套统一的 SVG Icon 组件库(假设包名为):
js
import { ZeroIconHome, ZeroIconSetting, ... } from "@company/svg-icon"
公司有统一约定:所有图标组件名都以 ZeroIcon 开头,例如:
ZeroIconHomeZeroIconAdvertisingZeroIconAnalysis
在业务中,图标主要有两种使用方式:
-
模板/组件中直接使用组件:
vue<ZeroIconHome /> -
配置中通过字符串标记 icon,例如菜单配置:
tsconst menu = [ { name: '首页', icon: 'ZeroIconHome', }, ]
如果我们简单粗暴地:
js
import * as Icons from "@company/svg-icon"
或者手写一大坨:
js
import {
ZeroIconHome,
ZeroIconAdvertising,
ZeroIconAnalysis,
// ...
} from "@company/svg-icon"
会出现几个问题:
- 打包会把整个 Icon 库搬进来,几乎没有体积控制;
- 手动维护这些 import 很容易出错:
- 配置里新增了菜单 icon,忘记加 import;
- 某些 icon 已经没人用了,但 import 一直留在那儿。
于是我希望有这样一个能力:
自动分析项目代码,找出所有实际出现的
ZeroIconXXX,生成一份svgList.js,只从 Icon 库中导入这些组件。
二、Icon 优化方案:自动收集 + 生成入口
整体思路是写一个构建前执行的 Node 脚本,主要承担四件事:
- 扫描业务代码,找出所有出现过的
ZeroIconXXX; - 扫描 Icon 库中真实存在的组件名,做一次交集过滤;
- 生成统一的入口文件
svgList.js; - 如果一个图标都没扫到,直接阻断部署,起到兜底作用。
1. 扫描项目代码中的 ZeroIcon 使用
核心脚本示例:
js
const glob = require("glob")
const fs = require("fs")
const { EOL } = require("os")
const path = require("path")
try {
// 1)扫描 icon 组件库里的所有 .vue 组件
const svgFiles = glob.sync(\`node_modules/@company/svg-icon/components/**/*.vue\`)
const allSVG = svgFiles.map((file) => path.parse(file).name)
// 2)扫描业务代码(根据你的项目实际目录调整)
const files = glob.sync(\`packages/**/*.{js,jsx,ts,tsx,vue}\`, {
ignore: [\`**/node_modules/**\`, \`**/dist/**\`],
})
// 收集所有出现过的 ZeroIcon 名称
const icons = []
files.forEach((file) => {
const content = fs.readFileSync(file, "utf8")
// 通过正则匹配所有 ZeroIconXXX
const matches = content.match(/ZeroIcon[a-zA-Z0-9]+/g)
if (matches) {
matches.forEach((e) => icons.push(e))
}
})
// 去重
const uniqueIcons = Array.from(new Set(icons))
// 和实际存在的 SVG 组件求交集,避免引用不存在的名字
const filteredIcons = uniqueIcons.filter((icon) => allSVG.includes(icon))
// 稳定排序,方便 diff 和 review
filteredIcons.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
到这里为止,filteredIcons 数组里就是:
「当前项目中真实用到,且图标库里确实存在的 ZeroIcon 组件名」。
2. 没有任何匹配时,直接阻止部署
js
// 如果没有匹配的图标,抛出异常阻塞部署进程
if (filteredIcons.length === 0) {
throw new Error("No matching icons found in the SVG files.")
}
这一点很实用:
- 如果路径写错、Icon 库不可用、脚本出 bug 等导致匹配结果为空,
- 在 CI / 部署阶段就会失败,
- 避免线上出现"所有图标都不见了"这种事故。
3. 自动生成 svgList.js 入口文件
下面这段代码,将 filteredIcons 拼接为一个 import + export 文件:
js
let listStatement = ""
let index = 1
filteredIcons.forEach((e) => {
if (index === filteredIcons.length) {
listStatement += \` \${e}\`
} else {
listStatement += \` \${e},\${EOL}\`
}
index++
})
const statement =
\`import {\${EOL}\${listStatement}\${EOL}} from "@company/svg-icon"\` +
\`\${EOL}export default {\${EOL}\${listStatement}\${EOL}}\${EOL}\`
fs.writeFileSync("packages/prism/src/components/SvgIcon/svgList.js", statement)
} catch (e) {
// 任何异常直接退出,CI 中会失败
process.exit(1)
}
生成的 svgList.js 大致长这样:
js
import {
ZeroIconHome,
ZeroIconAdvertising,
ZeroIconAnalysis,
// ...
} from "@company/svg-icon"
export default {
ZeroIconHome,
ZeroIconAdvertising,
ZeroIconAnalysis,
// ...
}
到此我们就得到了一个**"只包含当前项目实际使用图标"的统一入口文件**。
三、如何在项目中使用生成的 svgList.js
有了 svgList.js 之后,接下来就是在应用启动的时候,把这些 Icon 组件做一次全局注册。
下面是一个在 Vue 3 项目中的示例(路径根据你项目调整):
js
// packages/pacvue/src/frame.js
// 1. 导入 svgList.js
import SvgList from "@/components/SvgIcon/svgList.js"
import { createApp } from "vue"
import App from "./App.vue"
class InitAppClass {
constructor() {
this.app = createApp(App)
this.registerComponent(this.app) // 调用注册方法
}
// 2. 统一注册组件
registerComponent(app) {
// 遍历 svgList.js 导出的所有图标组件
for (const [key, component] of Object.entries(SvgList)) {
// 全局注册每个图标组件
app.component(key, component)
}
}
mount(selector) {
this.app.mount(selector)
}
}
export default InitAppClass
在业务代码中,使用体验是完全原来的写法:
vue
<template>
<ZeroIconHome />
<ZeroIconAdvertising />
</template>
你并不需要在每个组件里重复写:
js
import { ZeroIconHome } from "@company/svg-icon"
所有 Icon 组件的注册统一收敛在 frame.js 中,既保持了代码干净,又利用 svgList.js 做到了"只注册真正用到的那一批"。
四、特殊处理:菜单 / 配置中的 icon(menuIconList)
上面的脚本有一个很好的特性:
我们是直接对文件内容字符串 做正则匹配,凡是出现 ZeroIconXXX 的地方都会被识别------不仅是模板里的 <ZeroIconXXX />,字符串 "ZeroIconXXX" 一样能匹配。
这就刚好覆盖了菜单/配置中使用 icon 的场景。
假设我们单独维护了一个 static-icons.js:
js
// static-icons.js
const menuIconList = [
"ZeroIconHome",
"ZeroIconAdvertising",
"ZeroIconReport",
"ZeroIconOptimization",
"ZeroIconCompetitorAnalysis",
// ...
]
module.exports = {
menuIconList,
}
只要满足两点:
static-icons.js位于packages目录(或者被 glob 扫描到的目录);menuIconList中存的是完整的组件名字符串,如"ZeroIconHome";
那么在脚本扫描时:
js
const content = fs.readFileSync(file, "utf8")
const matches = content.match(/ZeroIcon[a-zA-Z0-9]+/g)
就会把这些菜单 icon 一并识别出来:
- 不需要在菜单配置文件里额外 import 图标;
- 不需要对
menuIconList做特殊逻辑处理; - 只要遵守统一命名规范(
ZeroIconXXX),就能自动被收集进 svgList。
这也是"统一命名规范"非常重要的一点:
它让工具脚本只用一条简单的正则,就能覆盖所有 icon 使用场景。
五、这种方案带来的好处
1. 包体积可控:只导入真正用到的 Icon
和全量导入 Icon 库相比,这种方式相当于构建了一份白名单:
- 项目中没出现过的
ZeroIconXXX,不会被 import; - 新增业务用到了新 icon,脚本下一次扫描就会自动加进来;
- 删除一些功能时,对应 icon 只要不再出现在代码里,也会自动从
svgList.js中消失。
整个过程不需要人工维护一份"icon 白名单"文件。
2. 对业务代码零侵入
对业务开发同学来说,只需要遵守两点简单约定:
- 使用组件时统一写成
<ZeroIconXXX />; - 在配置中表示 icon 时统一写
"ZeroIconXXX"。
你不需要:
- 在每个组件里手工 import icon 组件;
- 在任何地方维护一个"手写的 icon 清单"。
所有优化都通过这段脚本 + 应用启动时的统一注册来完成。
3. 可视化、可审核
生成的 svgList.js 本身就是一份"当前项目实际使用 icon 清单":
- 在 PR 中可以直观看到本次改动引入了哪些新 icon;
- 长期来看,可以统计:
- 哪些 icon 被频繁使用;
- 哪些 icon 长期无人引用,可以考虑在图标库层面做清理。
4. 构建阶段兜底,降低线上风险
通过:
js
if (filteredIcons.length === 0) {
throw new Error("No matching icons found in the SVG files.")
}
配合 process.exit(1),可以保证:
- 一旦 Icon 库路径出问题 / 脚本失效;
- 或者误改了目录结构导致没匹配到任何 icon;
CI / 部署流程会直接失败,避免"上线之后才发现所有图标全挂了"的情况。
六、与 import Tree Shaking 的区别与优势
Tree Shaking 能不能解决这个问题?
答案是:理论上能帮助一部分,但不足以完全覆盖。
1. Tree Shaking 的适用场景与局限
Tree Shaking 的基本前提是:
- 分析静态 ES Module 依赖(`import { A, B } from 'xxx'`);
- 找出"从未被使用的导出",在打包时剔除。
它在以下场景表现不错:
- 工具函数库;
- UI 组件库(每个组件明确按需 import)。
但对本文的场景,它存在几类天然盲区:
-
配置中用字符串标记 icon:
tsconst menu = [{ icon: 'ZeroIconHome' }]Tree Shaking 无法从字符串反推出这是一个组件依赖。
-
库本身导出方式不够"纯":
比如存在 CommonJS 导出、`export *` 等,Tree Shaking 效果会明显打折扣。
-
动态 import / 间接引用等复杂用法,也会增加 Tree Shaking 分析难度。
2. 当前方案的特点
我们的方案从另外一个角度切入:
不去"猜哪些没用",而是 明确列出"所有用到的 ZeroIconXXX",只导入这些。
具体特点是:
- 使用正则在代码文本层面识别 icon,而不仅仅是 import 语句;
- 能覆盖:
- `` 这样的模板用法;
- 代码中的变量引用;
- 菜单/配置中的 `"ZeroIconHome"` 字符串;
- 不依赖于 Icon 库内部是 ESM 还是 CJS。
3. 二者的关系
- Tree Shaking 更像一个通用的"编译级优化器";
- 本文这个脚本方案更像是一个为 Icon 这种高碎片化资源定制的专项优化。
两者不冲突:
- 你完全可以继续依赖 Tree Shaking 做其它库的优化;
- 在 Icon 这种资源上,再叠加一层"代码扫描 + 白名单导入",效果更确定、更可控。
总体来说,这套基于 ZeroIconXXX 命名规范的 Icon 优化方案,实现了:
- 减小包体积:只打包项目实际用到的图标;
- 无侵入接入:业务代码几乎不需要改动;
- 可维护性更好:统一入口 + 自动生成 + 支持菜单配置场景。
如果你们项目也有类似:
- 统一前缀的 Icon 组件;
- 菜单 / 配置中用字符串标记图标;
不妨也尝试用一段简单的脚本,把 Icon 这块的"隐形体积"收拾一下。