Astro (Vite) 中将字体作为 ArrayBuffer 导入

这些天正在折腾自动生成 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 来做渲染,也是我踩了不少坑的地方。

  • Vite 在 dev 和 build 时文件和文件之间的相对路径不同,因此需要一个使用 import 语句的方法,让 Vite 寻找对应的文件
  • 如果使用网上找到的将字体直接导入的方法(其实是将字体读入 Buffer 之后转成 base64, export default 出来),会爆内存,因此需要使用 fs 模块亲自读文件
  • 原文章里的通过 fetch 网站地址的方案感觉怪怪的 🤐 希望有个不依赖“是个网站”的方法

踩坑过程

首先,当然是看看参考的文章里是怎么干的。复制一小段:

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)”进行许可。

商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接。 如果您再混合、转换或者基于本作品进行创作,您必须基于相同的协议分发您贡献的作品。

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.4.1
2023-2024 Yunfi. | Source Code RSS | Site Map Powered by Astro. See all Credits.