续集:Vite 字体插件重构之路 —— 从“能用”到“生产级稳定”

续集:Vite 字体插件重构之路 ------ 从"能用"到"生产级稳定"

前言 : 在上一篇文章《把 16MB 中文字体压到 400KB:我写了一个 Vite 字体子集插件》中,我分享了如何通过自研插件 @fe-fast/vite-plugin-font-subset 解决中文字体体积过大的问题。

文章发出后,不少朋友试用并反馈了意见。更重要的是,在实际生产环境的复杂构建链路中,我发现 v0.1 版本存在一些设计上的"硬伤"。

开源不仅仅是发布代码,更是一个持续迭代的过程。今天这篇"续集",就来聊聊这次 v0.3.x 重构背后的故事:如何解决路径 404 问题、生命周期竞态,以及如何更优雅地接入 Vite 构建流。

一、遇到的问题:本地跑得欢,上线 404

在 v0.1 版本中,我的实现逻辑非常简单粗暴:

  1. 插件运行,调用 subset-font 生成 .woff2 文件。
  2. 使用 fs.writeFileSync 直接把文件写到 src/assets/fonts/subset 目录下。
  3. 生成一个 font.css,里面写着 src: url('./subset/xxx.woff2')
  4. 用户手动引入这个 CSS。

看起来没问题?但在真实项目中,这个方案有两个致命缺陷:

1. 路径引用的"相对论"陷阱

当我们在开发环境(vite dev)时,文件都在本地磁盘上,相对路径 url('./subset/...') 能正常工作。 但在 vite build 生产构建时,Vite 会把 CSS 内联到 HTML 中,或者打包成独立的 CSS 文件放在 assets 目录深处。 一旦目录结构发生变化(例如设置了 base: '/app/'),原本写死的相对路径就会失效,导致浏览器报 404 Not Found

2. "脏"文件与构建副作用

直接用 fs 模块往 src 目录写文件,是一种"副作用"很强的做法:

  • 这些生成的临时文件会被 Git 识别,污染工作区。
  • 它们没有经过 Vite 的资源处理管道(Asset Pipeline),导致文件名没有 Hash(缓存问题),也不会出现在 manifest.json 中。

二、重构核心:拥抱 Vite/Rollup 标准流

为了解决上述问题,我决定对插件进行彻底重构。核心目标是:不再手动写磁盘,而是把资源"交给" Vite 处理。

1. 弃用 fs.write,改用 emitFile

Vite(基于 Rollup)提供了一个标准的 API this.emitFile,专门用于在构建过程中发射文件。

Before (v0.1):

javascript 复制代码
// ❌ 坏味道:直接操作磁盘,脱离构建流
fs.writeFileSync(outputPath, subsetBuffer);

After (v0.3):

javascript 复制代码
// ✅ 最佳实践:告诉构建工具"我有一个文件要打包"
this.emitFile({
  type: 'asset',
  fileName: 'assets/fonts/my-font.woff2',
  source: subsetBuffer
});

这样做的好处是,Vite 会自动处理这些文件,把它们放到正确的 dist 目录,并且我们可以利用 Vite 的机制来处理路径。

2. 解决生命周期的"竞态问题"

在重构过程中,我踩了一个深坑:生命周期执行顺序

我最初想在 transformIndexHtml 钩子(处理 HTML 时)去注入 CSS 标签。但是,字体子集化是一个耗时操作(CPU 密集型),如果放在错误的钩子执行,会导致 HTML 已经处理完了,字体还没生成好。

最终的架构方案:

  • buildStart 阶段 : 这是构建开始的最早阶段。我在这里执行最耗时的"字体子集化"和"字符扫描"工作。计算出所有的 Hash 文件名,准备好要发射的数据。 为什么在这里? 确保后续任何钩子执行时,数据都已经准备就绪。

  • generateBundle 阶段 : 在这里统一调用 emitFile 发射所有字体文件和生成的 CSS 文件。

  • transformIndexHtml 阶段 : 因为在 buildStart 阶段我们已经计算好了最终的文件名(包含 Hash),所以在这里可以直接生成 <link rel="stylesheet"> 标签并注入到 HTML 的 <head> 中。

3. 自动注入:从"手动挡"变"自动挡"

v0.1 版本需要用户手动在 main.tsimport './subset/font.css'。 现在,得益于对 transformIndexHtml 的利用,插件默认开启 injectCss: true

用户什么都不用做 ,构建完成后,index.html 里会自动多出一行:

html 复制代码
<link rel="stylesheet" href="/assets/fonts/font-a1b2c3d4.css">

这不仅省事,更重要的是路径绝对正确 。插件会根据 Vite 配置的 base 自动拼接路径,无论你部署在根目录还是子目录,都能完美加载。

三、代码细节:一个小 Bug 的教训

在 v0.3.1 的迭代中,我还修复了一个变量作用域的小 Bug。

Bug 现场:

javascript 复制代码
// ❌ 错误代码
const result = await processFont(...)
// 这里试图解构 fontPath,但 processFont 返回值里漏传了这个字段
const { fontPath } = result 
console.log(path.basename(fontPath)) // -> undefined

修复与优化: 在 v0.3.3 中,我不仅修复了变量传递,还反思了一下:为什么需要传递这个变量? 其实 cssEntry 对象里已经包含了 relativePath,完全可以通过它推导出文件名。于是我删除了冗余的 fontPath 返回值,让数据流更清晰。

javascript 复制代码
// ✅ 优化后:减少冗余数据传递
const { cssEntry } = result
// 直接从已有数据推导
const fileName = path.basename(cssEntry.relativePath)

这提醒我们:重构不仅是改架构,更是清理冗余逻辑的好时机。

四、v0.3.x 版本的新特性总结

经过这次"伤筋动骨"的重构,@fe-fast/vite-plugin-font-subset 现在具备了生产级的能力:

  1. 零配置自动注入:默认自动生成 CSS 并注入 HTML,无需手动 import。
  2. 构建产物纯净 :不再污染 src 源码目录,所有产物直接进入 dist
  3. 路径安全 :完美支持 Vite 的 base 配置,支持 CDN 部署路径。
  4. Hash 缓存友好:生成的文件名带有内容 Hash,利于浏览器长效缓存。

五、写在最后

从 v0.1 到 v0.3,代码量增加了不少,但使用者的心智负担却降低了。

做开源项目往往就是这样:把复杂留给自己,把简单留给用户。 最初只是为了解决自己项目的 16MB 字体问题,现在它已经变成了一个更加健壮的通用解决方案。

如果你也在使用 Vite 开发中文项目,深受字体体积困扰,欢迎尝试一下这个插件,也欢迎在 GitHub 上提 Issue 或 PR,我们一起把它打磨得更好。

GitHub : github.com/william-xue... NPM : npm install @fe-fast/vite-plugin-font-subset -D


下一阶段计划: 目前插件主要针对 Build 阶段优化。接下来我可能会探索一下如何在 Dev 开发阶段提供更好的体验(比如利用缓存避免重复构建),敬请期待!

相关推荐
Never_Satisfied1 小时前
在JavaScript / 微信小程序中,动态修改页面元素的方法
开发语言·javascript·微信小程序
王大宇_1 小时前
虚拟列表从入门到出门
前端·javascript
Coder-coco2 小时前
个人健康系统|健康管理|基于java+Android+微信小程序的个人健康系统设计与实现(源码+数据库+文档)
android·java·vue.js·spring boot·微信小程序·论文·个人健康系统
一 乐3 小时前
英语学习激励|基于java+vue的英语学习交流平台系统小程序(源码+数据库+文档)
java·前端·数据库·vue.js·学习·小程序
淡淡蓝蓝3 小时前
uni.uploadFile使用PUT方法上传图片
开发语言·前端·javascript
老华带你飞3 小时前
个人健康系统|健康管理|基于java+Android+微信小程序的个人健康系统设计与实现(源码+数据库+文档)
android·java·vue.js·微信小程序·论文·毕设·个人健康系统
JIngJaneIL3 小时前
停车场管理|停车预约管理|基于Springboot+的停车场管理系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·notepad++·停车场管理|
零一科技3 小时前
Vue3学习第七课:(Vuex 替代方案)Pinia 状态管理 5 分钟上手
前端·vue.js
Achieve前端实验室3 小时前
深度解析 JavaScript 作用域与作用域链
前端·javascript·面试