还在为 SVG 烦恼?我写了个 CLI 工具,一键打包,性能拉满!(已开源)

179 阅读7分钟

还在为 SVG 烦恼?我写了个 CLI 工具,一键打包,性能拉满!(已开源)

作者:[King-GD]

GitHub: github.com/King-GD/svg…

NPM: www.npmjs.com/package/svg…

哈喽,各位掘友们好!作为一名前端开发者,和 SVG 打交道是家常便饭。它轻量、可缩放、还能用 CSS 控制样式,简直是图标方案的完美选择。但随着项目越来越大,SVG 图标一多,管理和使用上的“痛点”也随之而来。

最近,我就被这些痛点折磨得不轻,索性一怒之下,自己动手写了个 CLI 工具来彻底解决它们。没想到,从一个几十行的脚本开始,我最终把它打磨成了一个可以发布到 NPM 的完整开源项目:svg-quick

今天,我想和大家分享的,不仅仅是这个工具,更是从一个想法到一个开源产品的完整心路历程。

🤔 我们究竟在烦恼 SVG 的什么?

在使用 SVG 图标时,你是否也遇到过这些问题?

1.  性能问题:如果每个图标都作为独立的 .svg 文件请求,上百个图标就会导致上百个 HTTP 请求,这对性能是灾难性的。

2.  打包效率:在大型项目中,Webpack/Vite 每次构建都要实时去处理成百上千个 SVG 文件(比如用 svgr 转换),这会明显拖慢本就“弥足珍贵”的构建速度。

3.  最终体积:将 SVG 作为 React/Vue 组件直接导入,每个图标都会被包裹一层组件的额外代码,积少成多,最终产物体积会悄悄膨胀。

4.  灵活性差:有时候我想在 <img> 里用,有时候想在 CSS background 里用,有时候又想给 ECharts 这类图表库用。不同的场景需要不同格式的图标,来回转换非常繁琐。

这些问题,每一个都像一根小刺,扎在日常开发的幸福感上。

💡 解决方案的进化之路

V1.0: 一个满足自己的脚本

一切始于一个简单的 Node.js 脚本。思路很直接:

  • 扫描一个指定文件夹里的所有 .svg 文件。

  • 读取每个文件的内容。

  • svgo 压缩优化。

  • 把所有优化后的 SVG 字符串,塞到一个 JS 对象里。

  • 最后生成一个 index.js 文件,导出这个大对象和几个辅助函数。

这个脚本解决了“多文件请求”和“重复优化”的问题,让我可以很方便地在项目里使用。但它的问题也很明显:路径硬编码、无法复用、每次换项目都得复制粘贴改代码。

V2.0: 走向 CLI 工具

为了让这个脚本能在任何项目中开箱即用,我决定把它改造成一个真正的命令行工具。

我引入了 yargs 来解析命令行参数,核心命令很快就成型了:


svg-quick --input ./icons --output ./dist

这比之前的脚本强大多了!我可以指定任意的输入和输出目录,它变成了一个可复用的工具。我当时觉得已经很完美了,直到我遇到了现代前端工程化的“灵魂拷问”—— Tree-shaking

V3.0: 拥抱 Tree-shaking 和专业化

我意识到,之前把所有图标都打包到一个大对象里的做法,有一个致命缺陷:


// 之前产物的核心

export const icons = {

  user: '<svg>...',

  setting: '<svg>...',

  // ... 500 个图标

};

当我在代码里只 import 并使用 user 图标时,打包工具(Webpack/Vite)无法判断 icons 对象里其他 499 个图标是无用的,最终会导致所有图标都被打包进产物

这绝对不能忍!于是,我重构了整个工具的输出结构。

新的产物变成了这样:


// Tree-shakeable 的产物

export const user = '<svg>...</svg>';

export const setting = '<svg>...</svg>';

// ... 每个图标都是一个独立的 const 导出

这样一来,import { user } from '...'; 就能让打包工具精确地“摇掉”所有未使用的图标,完美实现了按需打包

同时,我还增加了更多专业化的功能:

  • 多种输出模式 (--mode): 用户可以选择只生成按需打包的版本 (treeshakeable),或者只生成全量版本 (full),甚至两者都要 (all)。

  • 类型定义: 自动生成 .d.ts 文件,在 TypeScript 项目中提供完美的类型提示和自动补全。

  • 代码美化: 内置 Prettier,确保生成的代码风格统一且美观。

至此,svg-quick 已经从一个自用的“小作坊”脚本,进化成了一个功能完备、设计考究的“正规军”。

🚀 正式介绍:svg-quick

现在,你可以通过 npx 轻松使用它,无需安装:


npx svg-quick --input <源目录> --output <目标目录> --mode <输出模式> 

它能为你带来什么?

  • ⚡️ 极致性能: 预构建模式,提升构建速度,减小产物体积。

  • 🌳 完美按需打包: 确保最终产物只包含你用到的图标。

  • ✨ 高度灵活性: 多种输出模式,满足任何使用场景。

  • 💻 优秀的开发体验: 统一的 API 和完整的 TypeScript 支持。

  • 🔧 强大的优化: 内置 svgo,自动压缩优化。

📖 产物使用示例 (Using the Output)

根据你选择的 --mode,产物的使用方式也不同,这体现了性能与灵活性的权衡。

模式一:按需打包 (treeshakeable 模式)

这是追求极致性能的推荐用法。此模式下生成的模块只包含按图标名导出的 const 变量。

📖 产物使用示例 (Using the Output)

Treeshakeable 模式 (推荐)

mode 设置为 treeshakeable 时,你可以这样使用:

// 在你的 React / Vue / Svelte 组件中
import { user, arrowLeft } from '../generated-icons';

// 'user' 和 'arrowLeft' 都是优化后的 SVG 字符串
// 你可以轻松地封装成自己的 <Icon> 组件

// React 示例:
const Icon = ({ svgString, ...props }) => (
  <span {...props} dangerouslySetInnerHTML={{ __html: svgString }} />
);

const App = () => (
  <div>
    <Icon svgString={user} style={{ color: 'blue' }} />
    <Icon svgString={arrowLeft} style={{ color: 'red' }} />
  </div>
);

优势: 打包工具构建你的应用时,只有被 importuserarrowLeft 图标会被包含进来,实现完美的按需打包。

Full 模式

mode 设置为 full 时,你可以使用实用工具函数:

// 导入实用工具函数,支持动态获取图标
import { getIconData, getIconSrc } from '../generated-icons';

// 获取 SVG 字符串
const userIconSvg = getIconData('user');

// 获取 base64 编码的 data URL,可直接用于 <img> 标签或 CSS background
const userIconSrc = getIconSrc('user');

// 在组件中使用
const DynamicIcon = ({ iconName }) => {
  const iconSrc = getIconSrc(iconName);
  return iconSrc ? <img src={iconSrc} alt={iconName} /> : null;
};

// 或者在 CSS 中使用
const iconUrl = getIconSrc('user');
// background-image: url(data:image/svg+xml;base64,...)

在 ECharts 中使用图标 (需要 Full 模式)

// 导入 ECharts 和图标工具函数 (需要使用 full 模式生成)
import * as echarts from 'echarts';
import { getIconSrc } from '../generated-icons';

// 在 ECharts 配置中使用图标
const option = {
  // 1. 在系列数据中使用图标作为 symbol
  series: [
    {
      type: 'scatter',
      data: [
        { value: [10, 20], symbol: getIconSrc('user') },
        { value: [30, 40], symbol: getIconSrc('star') },
      ],
      symbolSize: 30,
    },
  ],

  // 2. 在图例中使用自定义图标
  legend: {
    data: [
      {
        name: '用户数据',
        icon: getIconSrc('user'),
      },
      {
        name: '收藏数据',
        icon: getIconSrc('star'),
      },
    ],
  },

  // 3. 在工具箱中使用自定义图标
  toolbox: {
    feature: {
      myTool: {
        show: true,
        title: '自定义工具',
        icon: getIconSrc('settings'),
        onclick: function () {
          console.log('自定义工具被点击');
        },
      },
    },
  },

  // 4. 在标记点中使用图标
  series: [
    {
      type: 'line',
      data: [120, 200, 150, 80, 70, 110, 130],
      markPoint: {
        data: [
          {
            type: 'max',
            symbol: getIconSrc('arrowUp'),
            symbolSize: 25,
          },
          {
            type: 'min',
            symbol: getIconSrc('arrowDown'),
            symbolSize: 25,
          },
        ],
      },
    },
  ],
};

// 初始化图标
const chart = echarts.init(document.getElementById('chart'));
chart.setOption(option);

注意:使用此模式会生成一个包含所有图标的全量 icons 对象和辅助函数。因此,无论你实际用了几个,所有图标都会被打包进最终产物。

🌟 写在最后

从一个解决自己痛点的小脚本,到一个功能完善并发布到 NPM 的开源工具,这个过程让我收获良多。最大的感悟是,“轮子”不一定要惊天动地,能解决一个真实、具体、哪怕很小的问题,它就是有价值的

svg-quick 现在已经非常稳定,如果你也曾为 SVG 的管理而烦恼,不妨试一试它。

如果你觉得这个小工具对你有帮助,欢迎给我的 GitHub 仓库点个 Star 🌟!如果你有任何想法或建议,也非常欢迎提 Issue 或 PR。

感谢阅读!

项目链接