本文使用“署名-非商业性使用-相同方式共享 4.0 国际(CC BY-NC-SA 4.0)”进行许可。
商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接。 如果您再混合、转换或者基于本作品进行创作,您必须基于相同的协议分发您贡献的作品。
这些天正在折腾自动生成 OG Image,会需要把字体作为 ArrayBuffer
(或者 Node.js 的 Buffer
)导入的情况。尝试了一番,在 Vite 中终于找到了一个我认为还不错的解决方案。
TL;DR 写个小 Vite 插件实现导入文件的绝对路径,然后用 fs 读入。
虽然很久之前就知道 vercel/satori: Enlightened library to convert HTML and CSS to SVG 这个库了,但是一直拖着没做自动生成 OG Image 的功能,这两天下定决心按照 liruifengv 的文章 Astro 自动生成 Open Graph & Twitter card 图片😄 | liruifengv 把这个功能做出来。
首先,我的需求是在构建时生成 Image,没在实时 SSR 上试过(但感觉应该没问题?);同时,需要是个 ESM 的解决方案,不要用到 require
。在这之中遇到的最重要的问题就是 satori 需要一个字体的 ArrayBuffer 来做渲染,也是我踩了不少坑的地方。
import
语句的方法,让 Vite 寻找对应的文件fs
模块亲自读文件首先,当然是看看参考的文章里是怎么干的。复制一小段:
const isDev = import.meta.env.DEV;
const website = isDev ? "http://localhost:4321/" : SITE.website;
const fetchFonts = async () => {
const fontFileRegular = await fetch(
`${website}fonts/ZCOOL_KuaiLe/ZCOOLKuaiLe-Regular.ttf`
);
const fontRegular: ArrayBuffer = await fontFileRegular.arrayBuffer();
return { fontRegular };
};
const { fontRegular } = await fetchFonts();
呃 🤔……感觉有点黑魔法,有硬编码的 dev server 路径,感觉不太对;而且如果是 production build 的时候会保证是从 public 文件夹里读取而不是上一次部署的网站里吗?看了一圈 Astro 的文档,也没找到像这样的用法。所以打算找个更“一般”(至少 Vite 项目通用)的办法。
下一步,我知道 Vite 可以做到直接 import 一些二进制文件,这一般是通过某些插件实现的。因此,打算使用一个 Vite 插件可以直接将 ttf 文件导入成一个 ArrayBuffer。
很轻松就能找到这样的插件,比如 vite-plugin-arraybuffer 或者直接用了 satori 做例子的 unplugin-font-to-buffer 。但是实际运行中会出问题——会直接爆内存。查看这些插件的实现,可以发现它们简单地用 fs 将这些文件读进来,序列化之后 export default
出去。然而,可能因为需要生成很多个图片,所以被导入了很多次,然后就爆内存了。
Note
我的环境:Node.js 22 和 Vite 6 (Astro 5),运行在 macOS arm64 上。其他环境会不会爆内存未知。
之后通过简单的测试,我发现自己调用 fs 读文件而不是在 Vite 插件里读文件不会爆内存,因此显然只能这么干了。
无论是 node 的 fs 还是其他 JS 运行时提供的 fs 包,都需要提供一个路径。最朴素的想法当然是直接用当前文件和字体文件的想对路径。
然而,Vite 在 dev 和 build 之间的文件相对路径并不一致,比如一个 dev 环境中存在于 utils/a.ts
文件中的 fs 调用,在 build 环境中会先被打包进 pages/index.mjs
然后才被调用,同时字体文件也会被打包并且加上 hash 后缀,这时候显然就找不到文件了。
如果不想用黑魔法(比如逐级往上读文件目录直到项目根,然后再从项目根找字体文件)的话,还是得依靠 Vite 的 module resolve 来获取路径。
这时,如果对 Vite 比较熟悉的话,会想到 Vite 自带的 “path?url” 导入,可以获取一个路径。然而,这样获取的是一个相对于项目根目录的路径,但是 fs 模块并不知道项目根目录是哪个 😢,我搜查了一番也没找到怎么获取项目根目录的绝对路径。
但是,我们是可以做到 import 时获取绝对路径而不是相对路径的,因为 Vite 插件可以获取文件的绝对路径(或者是可以被 fs 正确读取的想对路径),只要把这个路径放回回来即可。
Note
如果在使用作为 ArrayBuffer 导入的方案不会出现内存问题的话,我还是推荐直接导入,更加直观。
首先写一个小插件:
function fileSystemPath() {
return {
name: "vite-plugin-file-system-path",
transform(_: unknown, id: string) {
if (id.endsWith("?filepath")) {
return {
code: `export default ${JSON.stringify(id.slice(0, -9))}`,
map: null,
};
}
},
};
}
然后把它放进 Vite 插件列表里。对于 Astro,可以在配置的 vite.plugins
中添加它。参考
其次,添加类型声明。找一个没有导入导出的文件(或者直接用一个 .d.ts
文件),添加下面的声明:
declare module "*?filepath" {
const value: string;
export default value;
}
使用:
import fontPath from "@assets/fonts/SarasaUiSC-Regular.ttf?filepath";
const fontBuffer = await fs.readFile(fontPath);
本文使用“署名-非商业性使用-相同方式共享 4.0 国际(CC BY-NC-SA 4.0)”进行许可。
商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接。 如果您再混合、转换或者基于本作品进行创作,您必须基于相同的协议分发您贡献的作品。