记录一次webpack升级vite的过程

83 阅读3分钟

SVG Sprite 插件问题排查与修复总结

🔄 原理对比

Webpack 方式(原有方案)

const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)

工作流程:

  1. require.context() - webpack 编译时扫描 svg 目录
  2. 配合 svg-sprite-loader 插件
  3. loader 自动将 SVG 转换为 <symbol> 并注入页面
  4. 简洁优雅,但依赖 webpack 生态

优点:

  • 代码简洁(3行)
  • webpack 生态成熟
  • 自动化处理

缺点:

  • 仅支持 webpack
  • 不兼容 Vite

首先要明确:require.context 是 Webpack 编译时注入的静态 API(不是原生 JavaScript 或 CommonJS 语法),作用是 “批量获取某个目录下符合规则的文件路径,并生成对应的模块加载函数”。

1. const req = require.context('./svg', false, /.svg$/)

这行代码是 “批量加载的入口”,通过 require.context 定义了要加载的文件规则,三个参数分别对应:

  • 第 1 个参数 './svg':指定要扫描的目录(相对当前文件的 svg 文件夹);
  • 第 2 个参数 false:是否递归扫描子目录(这里为 false,只扫 svg 根目录,不扫子文件夹);
  • 第 3 个参数 /.svg$/:匹配文件的正则表达式(只找后缀为 .svg 的文件)。

执行后,req 会变成一个特殊的加载函数,它有两个关键能力:

  • req.keys():返回所有匹配文件的路径数组(比如 ['./icon-home.svg', './icon-user.svg']);
  • req(路径):加载指定路径的文件(类似 require('./svg/icon-home.svg'),但由 Webpack 统一管理)。
2. const requireAll = requireContext => requireContext.keys().map(requireContext)

这行定义了一个批量加载工具函数,作用是 “自动遍历所有匹配的 SVG 文件,并逐个加载”:

  • 输入 requireContext:就是前面的 req(Webpack 生成的加载函数);
  • requireContext.keys():先获取所有 SVG 文件的路径数组;
  • .map(requireContext):遍历路径数组,用 req 加载每个路径对应的 SVG 文件(相当于执行 req('./icon-home.svg')req('./icon-user.svg') 等)。
3. requireAll(req)

执行工具函数,触发 “批量加载所有 SVG 文件” 的逻辑 ——Webpack 编译时会根据这个调用,自动把 svg 目录下的所有 .svg 文件纳入打包流程,并交给后续的 svg-sprite-loader 处理。 代码能实现 “SVG 自动转 Sprite”,核心是 Webpack 编译时能力 + svg-sprite-loader 插件 的配合,四步流程的本质是 “从文件扫描到页面注入的全自动化”:

1. Webpack 编译时扫描 SVG 目录
  • 当 Webpack 编译到 require.context('./svg', ...) 时,会静态分析这个 API 的参数,在编译阶段就去扫描 ./svg 目录下所有符合 /.svg$/ 的文件(不是运行时动态扫描);
  • 这一步的关键是 “编译时提前知晓要加载的文件”,为后续批量处理打下基础(原生 JS 做不到这一点,必须依赖 Webpack 的静态分析能力)。
2. 配合 svg-sprite-loader 插件处理 SVG
  • svg-sprite-loader 是 Webpack 的专属 Loader,作用是 “把单个 SVG 文件转换成 SVG Sprite 中的 <symbol> 标签”;
  • 比如一个 icon-home.svg 文件(内容是 <svg><path d="..."/></svg>),经过这个 Loader 处理后,会变成:
<symbol id="icon-home" viewBox="0 0 24 24">
  <path d="..."/>
</symbol>
3. Loader 自动将 <symbol> 注入页面
  • svg-sprite-loader 处理完所有 SVG 后,会自动在页面的 DOM 中注入一个隐藏的 <svg> 容器,里面包含所有 SVG 对应的 <symbol> 标签;
  • 最终页面 DOM 中会多一段这样的代码(通常在 <body> 或 <head> 里):
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="icon-home" viewBox="0 0 24 24"><path d="..."/></symbol>
  <symbol id="icon-user" viewBox="0 0 24 24"><path d="..."/></symbol>
  <!-- 所有 SVG 对应的 symbol 都在这里 -->
</svg>

image.png

4. 依赖 Webpack 生态的核心原因
  • 整个方案的 “批量扫描(require.context)”“SVG 转 Sprite(svg-sprite-loader)”“自动注入 DOM”,每一步都依赖 Webpack 的编译时机制和 Loader 生态;

  • 脱离 Webpack 后(比如在 Vite 或原生项目中):

    • 没有 require.context 这个 API,无法批量扫描文件;
    • svg-sprite-loader 是 Webpack Loader,不能在其他构建工具中使用;
    • 因此这套方案只能在 Webpack 项目中生效。

Vite 方式(新方案)

// src/icons/index.js
const modules = import.meta.glob('./svg/*.svg', { eager: true })

// vite-plugin-svg-sprite.js (自定义插件)
export default function viteSvgSpritePlugin() {
  // 拦截 SVG 文件加载
  // 解析并生成 sprite
  // 注入到 HTML
}

工作流程:

  1. import.meta.glob() - Vite 编译时批量导入
  2. 自定义插件 vite-plugin-svg-sprite 处理
  3. 插件拦截 SVG 文件,提取内容和 viewBox
  4. 在 HTML 转换阶段注入 sprite 到页面

优点:

  • 原生支持 Vite
  • 性能好(编译时处理)
  • 灵活可控

缺点:

  • 需要自定义插件
  • 代码相对复杂

src/icons/index.js(仅22行)

const isVite = typeof import.meta !== 'undefined' && import.meta.env

if (isVite) {
  // Vite:import.meta.glob + 插件自动处理
  const modules = import.meta.glob('./svg/*.svg', { eager: true })
  console.log(`[Vite] 已加载 ${Object.keys(modules).length} 个 SVG 图标`)
} else {
  // Webpack:require.context + svg-sprite-loader
  const req = require.context('./svg', false, /\.svg$/)
  requireAll(req.keys().map(req))
  console.log(`[Webpack] 已加载 ${req.keys().length} 个 SVG 图标`)
}

vite-plugin-svg-sprite.js(插件负责"脏活")

  • 拦截并解析 SVG 文件
  • 提取 symbol 内容和 viewBox
  • 自动注入到 HTML 的 <body> 开头

📊 方案对比

特性Webpack 方案Vite 方案兼容方案
代码简洁度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
维护成本
兼容性Webpack onlyVite only双构建工具
自动化程度

下面介绍一下vite-plugin-svg-sprite.js插件

核心思路:不在构建时注入 HTML,而是在浏览器运行时动态创建 sprite。

一开始的时候,在 transformIndexHtml 中尝试注入,发现transformIndexHtml 钩子执行时,SVG 文件还没有被 load 钩子处理,<svg-icon icon-class="back-arrow" /> 在页面上不显示,页面中也没有看到注入的 SVG sprite

transformIndexHtml(html) {
  // 问题:此时 spriteSymbols 还是空的
  spriteSymbols.forEach((data, name) => { ... })
}
vite钩子整体阶段划分(从早到晚)
阶段核心钩子作用执行时机
1. 配置修改阶段config → configResolved修改 Vite 配置(如 alias、plugins)Vite 初始化时,最早执行(仅一次)
2. 模块处理阶段resolveId → load → transform处理单个模块(JS/SVG/CSS 等)模块被引用时按需触发(每个模块一次)
3. HTML 处理阶段transformIndexHtml处理入口 HTML 文件(如注入脚本、样式)解析入口 HTML 时执行(仅一次,早于模块处理)
关键钩子的执行逻辑
  • config/configResolved:最早执行,用于修改 Vite 配置(比如添加别名、调整插件),此时还未处理任何模块或 HTML。

  • resolveId → load → transform:这是 单个模块的处理流程(针对每一个被引用的模块,如 icon.svgindex.js):

    1. resolveId:解析模块路径(比如将 @/icon.svg 解析为绝对路径);
    2. load:读取模块内容(比如读取 icon.svg 的原始 XML 代码);
    3. transform:修改模块内容(比如将 SVG 转为 <symbol> 标签,或注入额外代码)。注意:这三个钩子是按需触发的 —— 只有当模块被其他文件引用时(如 JS 导入 import './icon.svg'、HTML 中 <img src="./icon.svg">),才会执行这个流程。
  • transformIndexHtml:专门处理入口 HTML 文件(如 index.html),作用是修改 HTML 内容(比如注入 <script> 标签、添加 CSS 链接)。它的执行时机是 Vite 解析入口 HTML 时,此时 HTML 中的模块引用(如 <script src="./main.js"><img src="./icon.svg">)还只是 “字符串路径”,尚未触发模块的 resolveId/load 流程。

HTML 处理时机” 早于 “模块处理时机” ,且模块处理是 “按需触发” 的,具体分两种场景说明:

场景 1:在 transformIndexHtml 中注入 SVG 的 “模块引用”

比如在 HTML 中注入 <img src="./icon.svg"> 或 <script>import './icon.svg'</script>

// 某 Vite 插件的 transformIndexHtml 钩子
function transformIndexHtml(html) {
  // 注入引用 SVG 的 img 标签
  return html.replace('</body>', '<img src="./icon.svg"></body>');
}
  • 此时 transformIndexHtml 执行时,./icon.svg 还只是一个字符串路径,Vite 尚未处理这个 SVG 模块 —— 因为 “引用 SVG 的 img 标签” 是刚注入的,Vite 还没解析到这个引用,自然不会触发 resolveId/load 流程。
  • 只有当后续 Vite 处理这个 img 标签的 src 时(即解析 “如何加载 ./icon.svg”),才会触发 SVG 模块的 resolveId → load → transform
场景 2:在 transformIndexHtml 中直接注入 SVG 的 “原始内容”

比如直接把 SVG 代码写入 HTML(如 <svg><path d="..."/></svg>):

javascript

运行

function transformIndexHtml(html) {
  // 直接注入 SVG 原始内容
  const svgContent = '<svg width="24" height="24"><path d="M12 2..."/></svg>';
  return html.replace('</body>', `${svgContent}</body>`);
}
  • 这种情况下,SVG 内容是直接写死在 HTML 中的,没有经过 “模块引用” 流程,因此不会触发 Vite 的 resolveId/load 钩子 —— 也就不存在 “是否被 load 处理” 的问题,SVG 会被浏览器直接渲染。
  • 但如果需要 SVG 经过插件处理(比如用 vite-plugin-svg-icons 转为 Sprite),这种直接注入原始内容的方式会跳过处理,不符合预期。
场景 3:在 transformIndexHtml 中主动加载并处理 SVG

如果想在 transformIndexHtml 中注入 “已被 load/transform 处理后的 SVG 内容”,需要主动触发模块加载流程(通过 Vite 提供的 this.loadModule API):

async function transformIndexHtml(html) {
  // 主动加载并处理 SVG 模块(触发 resolveId → load → transform)
  const svgModule = await this.loadModule('./icon.svg', { 
    eager: true // 强制同步加载模块
  });
  // 假设处理后的 SVG 内容在 svgModule.default 中
  const processedSvg = svgModule.default;
  // 注入处理后的 SVG
  return html.replace('</body>', `${processedSvg}</body>`);
}
  • 这种情况下,通过 this.loadModule 主动触发了 SVG 模块的 resolveId → load → transform 流程,因此注入的是 “已处理后的 SVG 内容”,此时 SVG 已被 load 处理。
  • 注意:this.loadModule 是 Vite 插件上下文提供的 API,需在异步钩子中使用(transformIndexHtml 支持异步)
  1. 钩子顺序关键点config → configResolved → transformIndexHtml(处理 HTML)  → (模块被引用时)resolveId → load → transform(处理 SVG 等模块)。即 transformIndexHtml 执行时,默认还未触发任何模块的 load 流程。

  2. SVG 是否被 load 处理,取决于注入方式

    • 注入 “模块引用”(如 <img src="./icon.svg">):SVG 未被 load,后续按需处理;
    • 注入 “原始内容”(如直接写 <svg>):SVG 不经过 load,跳过插件处理;
    • 主动 loadModule 加载:SVG 会被 load 处理,可注入处理后的内容。
  3. 实际开发建议:若需在 HTML 中注入 “经过插件处理的 SVG”(如 Sprite 图标),推荐使用专门的 SVG 插件(如 vite-plugin-svg-icons),这类插件会在模块处理阶段自动处理 SVG,并在 transformIndexHtml 中注入 Sprite 容器,无需手动管理钩子顺序。

load(id) {
  // 1. 读取 SVG 文件
  const svgContent = readFileSync(id, 'utf-8')
  
  // 2. 解析内容
  const symbolMatch = svgContent.match(/<svg[^>]*>([\s\S]*)<\/svg>/)
  
  // 3. 返回一个模块,包含客户端注册代码
  return `
    const iconData = { id, content, viewBox };
    
    // 在浏览器中执行
    if (typeof window !== 'undefined') {
      const registerIcon = () => {
        // 创建/获取 sprite 容器
        let svg = document.getElementById('__svg__icons__dom__');
        if (!svg) { /* 创建容器 */ }
        
        // 创建 symbol 元素
        const symbol = document.createElementNS(...);
        symbol.innerHTML = iconData.content;
        svg.appendChild(symbol);
      };
      
      registerIcon();
    }
  `
}

🔧 插件工作原理

数据流程图

┌─────────────────────────────────────────────────────────────────┐
│                      Vite 构建流程                                │
└─────────────────────────────────────────────────────────────────┘

1. 代码分析阶段
   ┌──────────────────────────────────────┐
   │ src/icons/index.js                   │
   │ const modules = import.meta.glob()   │  ← 扫描匹配的文件
   └──────────────────────────────────────┘
                    ↓
   ┌──────────────────────────────────────┐
   │ 发现: ./svg/back-arrow.svg           │
   │ 发现: ./svg/user.svg                 │
   │ ...                                  │
   └──────────────────────────────────────┘

2. 模块加载阶段 (load 钩子)
   ┌──────────────────────────────────────┐
   │ viteSvgSpritePlugin.load(id)         │
   │ ✓ 读取 SVG 文件内容                  │
   │ ✓ 解析 <svg>, viewBox                │
   │ ✓ 生成客户端注册代码                 │
   └──────────────────────────────────────┘
                    ↓
   ┌──────────────────────────────────────┐
   │ 返回 JS 模块代码:                     │
   │ - iconData 对象                      │
   │ - registerIcon() 函数                │
   │ - DOM 操作代码                       │
   └──────────────────────────────────────┘

3. 打包阶段
   ┌──────────────────────────────────────┐
   │ 将所有 SVG 模块打包到 bundle.js      │
   │ 每个 SVG = 一个独立的注册函数        │
   └──────────────────────────────────────┘

4. 浏览器运行时
   ┌──────────────────────────────────────┐
   │ 加载 bundle.js                        │
   │   ↓                                   │
   │ 执行每个 SVG 的注册函数               │
   │   ↓                                   │
   │ 创建 <svg id="__svg__icons__dom__">  │
   │   ↓                                   │
   │ 添加 <symbol id="icon-back-arrow">   │
   │ 添加 <symbol id="icon-user">         │
   │ ...                                  │
   └──────────────────────────────────────┘
                    ↓
   ┌──────────────────────────────────────┐
   │ <svg-icon icon-class="back-arrow" /> │
   │   ↓ 查找                              │
   │ <use xlink:href="#icon-back-arrow"/> │
   │   ↓ 找到并渲染                        │
   │ ✓ 图标显示                            │
   └──────────────────────────────────────┘

🎯 插件各部分作用

1. config 钩子

config(config, { command }) {
  isBuild = command === 'build'
}

作用:检测是开发模式还是构建模式,决定注入策略

2. load 钩子(核心)

load(id) {
  if (!svgRegex.test(id)) return null
  
  const svgContent = readFileSync(id, 'utf-8')
  // 解析并返回客户端注册代码
}

作用

  • 拦截 .svg 文件的加载
  • 读取原始 SVG 内容
  • 解析 <svg> 标签和 viewBox 属性
  • 生成包含客户端注册逻辑的 JS 模块
  • 将 SVG 转换为可执行的 JavaScript

关键点:返回的是 JavaScript 代码字符串,会被 Vite 当作模块处理

3. transformIndexHtml 钩子(构建时)

transformIndexHtml: {
  order: 'post',
  handler(html) {
    if (!isBuild) return html
    // 构建时直接在 HTML 中注入完整 sprite
  }
}

作用:仅在生产构建时将所有 sprite 直接注入 HTML,优化首屏加载


📊 数据链路详解

开发模式数据流

SVG 文件 (磁盘)
    ↓ readFileSync()
原始 SVG 字符串
    ↓ 正则匹配
{ id: 'back-arrow', content: '<path...>', viewBox: '0 0 24 24' }
    ↓ JSON.stringify() + 模板字符串
JavaScript 模块代码
    ↓ Vite 打包
浏览器 bundle.js
    ↓ 执行
DOM: <svg id="__svg__icons__dom__">
       <symbol id="icon-back-arrow">...</symbol>
     </svg>
    ↓ SvgIcon 组件引用
<use xlink:href="#icon-back-arrow" />
    ↓ 浏览器渲染
✓ 图标显示

生产构建数据流

所有 SVG 文件
    ↓ load 钩子收集
spriteSymbols Map
    ↓ transformIndexHtml
完整的 <svg> sprite 字符串
    ↓ HTML 替换
index.html (包含 sprite)
    ↓ 部署
用户浏览器
    ↓ 直接可用
✓ 图标显示(无需 JS 注册)

💡 关键技术点

1. import.meta.glob 的工作机制

const modules = import.meta.glob('./svg/*.svg', { eager: true })
  • 编译时:Vite 扫描并生成导入列表
  • 运行时:返回 { path: module } 对象
  • eager: true:立即加载所有模块(非懒加载)

2. Vite 插件钩子顺序

config → resolveId → load → transform → transformIndexHtml
  • load:最早拦截文件内容
  • transformIndexHtml:最后修改 HTML
  • 问题根源:最初在 transformIndexHtml 中读取数据,但数据在 load 中才准备

3. 客户端代码注入技巧

return `
  const iconData = ${JSON.stringify(data)};
  
  if (typeof window !== 'undefined') {
    // 浏览器中执行的代码
    registerIcon();
  }
`
  • 插件返回的字符串会被作为 JS 模块执行
  • 可以混合数据和逻辑
  • typeof window 判断避免 SSR 错误

4. DOM 操作的时机控制

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', registerIcon);
} else {
  registerIcon();
}
  • 确保 DOM 准备好再操作
  • 兼容不同的加载时机

✅ 最终效果

代码简洁度

// src/icons/index.js 
const isVite = typeof import.meta !== 'undefined' && import.meta.env

if (isVite) {
  const modules = import.meta.glob('./svg/*.svg', { eager: true })
  console.log(`[Vite] 已加载 ${Object.keys(modules).length} 个 SVG 图标`)
} else {
  const req = require.context('./svg', false, /\.svg$/)
  requireAll(req.keys().map(req))
  console.log(`[Webpack] 已加载 ${req.keys().length} 个 SVG 图标`)
}

使用体验

<!-- 业务代码无需修改 -->
<svg-icon icon-class="back-arrow" style="font-size: 36px" />