经济日报电子版手机版
65.71MB · 2025-11-24
在 v0.1 版本中,我的实现逻辑非常简单粗暴:
subset-font 生成 .woff2 文件。fs.writeFileSync 直接把文件写到 src/assets/fonts/subset 目录下。font.css,里面写着 src: url('./subset/xxx.woff2')。看起来没问题?但在真实项目中,这个方案有两个致命缺陷:
当我们在开发环境(vite dev)时,文件都在本地磁盘上,相对路径 url('./subset/...') 能正常工作。
但在 vite build 生产构建时,Vite 会把 CSS 内联到 HTML 中,或者打包成独立的 CSS 文件放在 assets 目录深处。
一旦目录结构发生变化(例如设置了 base: '/app/'),原本写死的相对路径就会失效,导致浏览器报 404 Not Found。
直接用 fs 模块往 src 目录写文件,是一种“副作用”很强的做法:
manifest.json 中。为了解决上述问题,我决定对插件进行彻底重构。核心目标是:不再手动写磁盘,而是把资源“交给” Vite 处理。
fs.write,改用 emitFileVite(基于 Rollup)提供了一个标准的 API this.emitFile,专门用于在构建过程中发射文件。
Before (v0.1):
// 坏味道:直接操作磁盘,脱离构建流
fs.writeFileSync(outputPath, subsetBuffer);
After (v0.3):
// 最佳实践:告诉构建工具“我有一个文件要打包”
this.emitFile({
type: 'asset',
fileName: 'assets/fonts/my-font.woff2',
source: subsetBuffer
});
这样做的好处是,Vite 会自动处理这些文件,把它们放到正确的 dist 目录,并且我们可以利用 Vite 的机制来处理路径。
在重构过程中,我踩了一个深坑:生命周期执行顺序。
我最初想在 transformIndexHtml 钩子(处理 HTML 时)去注入 CSS 标签。但是,字体子集化是一个耗时操作(CPU 密集型),如果放在错误的钩子执行,会导致 HTML 已经处理完了,字体还没生成好。
最终的架构方案:
buildStart 阶段:
这是构建开始的最早阶段。我在这里执行最耗时的“字体子集化”和“字符扫描”工作。计算出所有的 Hash 文件名,准备好要发射的数据。
为什么在这里? 确保后续任何钩子执行时,数据都已经准备就绪。
generateBundle 阶段:
在这里统一调用 emitFile 发射所有字体文件和生成的 CSS 文件。
transformIndexHtml 阶段:
因为在 buildStart 阶段我们已经计算好了最终的文件名(包含 Hash),所以在这里可以直接生成 <link rel="stylesheet"> 标签并注入到 HTML 的 <head> 中。
v0.1 版本需要用户手动在 main.ts 里 import './subset/font.css'。
现在,得益于对 transformIndexHtml 的利用,插件默认开启 injectCss: true。
用户什么都不用做,构建完成后,index.html 里会自动多出一行:
<link rel="stylesheet" href="/assets/fonts/font-a1b2c3d4.css">
这不仅省事,更重要的是路径绝对正确。插件会根据 Vite 配置的 base 自动拼接路径,无论你部署在根目录还是子目录,都能完美加载。
在 v0.3.1 的迭代中,我还修复了一个变量作用域的小 Bug。
Bug 现场:
// 错误代码
const result = await processFont(...)
// 这里试图解构 fontPath,但 processFont 返回值里漏传了这个字段
const { fontPath } = result
console.log(path.basename(fontPath)) // -> undefined
修复与优化:
在 v0.3.3 中,我不仅修复了变量传递,还反思了一下:为什么需要传递这个变量?
其实 cssEntry 对象里已经包含了 relativePath,完全可以通过它推导出文件名。于是我删除了冗余的 fontPath 返回值,让数据流更清晰。
// 优化后:减少冗余数据传递
const { cssEntry } = result
// 直接从已有数据推导
const fileName = path.basename(cssEntry.relativePath)
这提醒我们:重构不仅是改架构,更是清理冗余逻辑的好时机。
经过这次“伤筋动骨”的重构,@fe-fast/vite-plugin-font-subset 现在具备了生产级的能力:
src 源码目录,所有产物直接进入 dist。base 配置,支持 CDN 部署路径。从 v0.1 到 v0.3,代码量增加了不少,但使用者的心智负担却降低了。
做开源项目往往就是这样:把复杂留给自己,把简单留给用户。 最初只是为了解决自己项目的 16MB 字体问题,现在它已经变成了一个更加健壮的通用解决方案。
如果你也在使用 Vite 开发中文项目,深受字体体积困扰,欢迎尝试一下这个插件,也欢迎在 GitHub 上提 Issue 或 PR,我们一起把它打磨得更好。
下一阶段计划: 目前插件主要针对 Build 阶段优化。接下来我可能会探索一下如何在 Dev 开发阶段提供更好的体验(比如利用缓存避免重复构建),敬请期待!