Next.js + Sharp在Vercel上SVG转PNG中文乱码?5分钟搞定字体配置
最近在折腾一个图片生成工具,用Next.js搭了个API,配合Sharp库处理图片转换,本地跑得风生水起。英文、数字、中文,想怎么显示就怎么显示。可一部署到Vercel,中文全变成了一个个“口口口”或者奇怪的方块,瞬间傻眼。这感觉就像精心准备的演讲稿,到了台上麦克风突然失灵,台下观众只能看到你嘴在动。
这问题其实挺典型的,尤其是在处理动态生成图片、图表水印或者自定义海报这类场景时。Sharp本身是个性能怪兽,但在无服务器(Serverless)环境里,它就是个“外来户”,环境里缺什么它可不会自己变出来。最常缺的,就是中文字体。本地开发机通常字体齐全,但Vercel的容器环境干净得像张白纸,除了最基本的系统字体,啥都没有。所以,我们的核心任务,就是教会这个容器认识中文,把字体“打包”进部署环境,并告诉Sharp该去哪儿找它们。
1. 问题根源:为什么Vercel上Sharp不认识中文?
要解决问题,得先摸清问题的来龙去脉。Sharp底层依赖于一系列图像处理库,比如libvips。当它处理SVG中的文本时,尤其是需要将SVG栅格化为PNG时,它需要调用系统的字体配置服务(通常是Fontconfig)来查找和加载字体文件。
本地环境 vs. Vercel环境的关键差异:
| 对比项 | 本地开发环境 | Vercel Serverless 环境 |
|---|---|---|
| 字体库 | 通常安装了完整的中文字体包(如系统自带的宋体、黑体,或开发者安装的思源、苹方等)。 | 极简的Linux容器,通常只包含少数几种开源英文字体(如DejaVu)。 |
| 字体缓存 | 有可写的字体缓存目录,Fontconfig可以正常工作。 | 默认可能没有可写的缓存目录,导致Fontconfig报错。 |
| 文件系统 | 拥有完整的项目文件访问权限。 | 部署后,你的代码被放置在特定目录(如/var/task),需要显式指定资源路径。 |
在Vercel的日志里,你可能会看到类似这样的错误信息:
Fontconfig error: Cannot load default config file
或者
Fontconfig error: No writable cache directories
这就像Sharp在喊:“我找不到字体配置文件,也不知道该把缓存放哪儿!”
所以,解决方案的思路非常清晰:
- 提供字体文件:将我们需要的中文字体文件(如.ttf或.otf格式)放入项目,并确保它能被部署到Vercel。
- 配置字体路径:创建一个Fontconfig能识别的配置文件(
fonts.conf),明确告诉它:“嘿,字体在这个文件夹里,缓存请放到那个临时目录。” - 设置环境变量:通过环境变量
FONTCONFIG_PATH,让整个系统(包括Sharp)知道我们的自定义字体配置文件在哪里。
2. 实战准备:项目中文字体与配置
理论清楚了,我们开始动手。整个过程就像为你的应用准备一个专属的“字体工具箱”。
2.1 获取并放置中文字体文件
首先,你需要一个支持中文的字体文件。出于版权和通用性考虑,推荐使用开源字体。思源黑体(Noto Sans CJK) 是一个绝佳的选择,它覆盖了简繁中文、日文、韩文,且质量很高。
操作步骤:
- 访问思源黑体的官方GitHub仓库(例如
github.com/googlefonts/noto-cjk),在Release页面下载你需要的字体变体。对于简体中文,NotoSansSC-Regular.ttf(常规体)和NotoSansSC-Bold.ttf(粗体)就足够了。 - 在你的Next.js项目根目录下,创建一个名为
fonts的文件夹。这个文件夹的名字可以自定义,但为了清晰,建议就用fonts。 - 将下载好的
.ttf字体文件复制到fonts文件夹内。
现在你的项目结构应该类似这样:
your-nextjs-project/
├── fonts/
│ ├── NotoSansSC-Regular.ttf
│ └── NotoSansSC-Bold.ttf
├── pages/
├── public/
├── styles/
└── package.json
注意:确保字体文件被Git跟踪(即没有被
.gitignore排除)。因为Vercel是通过Git仓库来部署的,如果字体文件没有被提交,部署后自然就不存在。对于较大的字体文件,可以考虑使用Git LFS(大文件存储),但对于几个TTF文件,直接提交到仓库通常是可以接受的。
2.2 创建Fontconfig配置文件
接下来,我们需要在 fonts 文件夹内创建一个名为 fonts.conf 的XML配置文件。这个文件是Fontconfig的“说明书”。
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- 指定字体文件所在的目录 -->
<dir>/var/task/fonts</dir>
<!-- 指定一个可写的缓存目录,Vercel的/tmp是临时的但可写 -->
<cachedir>/tmp/fonts-cache</cachedir>
<!-- 可以在这里添加其他字体配置,比如别名 -->
<alias>
<family>sans-serif</family>
<prefer>
<family>Noto Sans</family>
<family>Noto Sans SC</family>
</prefer>
</alias>
</fontconfig>
关键点解析:
<dir>/var/task/fonts</dir>: 这是Vercel Serverless函数运行时,你的项目代码被解压到的根目录路径。/var/task是Lambda/Serverless的常见工作目录。/fonts就是我们上一步创建的文件夹。<cachedir>/tmp/fonts-cache</cachedir>: 指定字体缓存目录。Vercel的/tmp目录在函数执行期间是可写的,非常适合存放临时缓存,能加速后续的字体加载。<alias>...</alias>: 这部分是可选的,但非常有用。它创建了一个字体别名。当系统请求sans-serif(无衬线字体)时,会优先尝试使用Noto Sans和Noto Sans SC。这能确保SVG中即使只写了font-family: sans-serif;,也能正确回退到我们的中文字体。
3. 代码集成:让Sharp找到并使用字体
配置好了“工具箱”,现在需要修改我们的Next.js API代码,确保Sharp在处理图片时能利用这些配置。
3.1 设置环境变量
环境变量是连接系统配置和应用程序的桥梁。我们需要在Vercel平台上设置一个关键变量。
首先,在项目根目录创建或修改 .env.local 文件(用于本地开发):
FONTCONFIG_PATH=./fonts
在本地,相对路径 ./fonts 就能让Fontconfig找到我们的配置文件。
然后,在Vercel项目设置中配置生产环境变量:
- 登录Vercel Dashboard,进入你的项目。
- 点击 Settings -> Environment Variables。
- 添加一个新的环境变量:
- Name:
FONTCONFIG_PATH - Value:
/var/task/fonts - Environment: 选择
Production(如果预发布环境也需要,可以同时勾选)。
- Name:
提示:在Vercel上,环境变量的优先级高于本地文件。部署后,Sharp会读取
FONTCONFIG_PATH=/var/task/fonts这个值,从而找到我们部署在容器内的fonts.conf文件。
3.2 修改SVG生成与Sharp处理代码
假设你有一个API路由(例如 pages/api/generate-image.ts),使用Sharp将SVG转换为PNG。关键点在于SVG内部的 font-family 声明,以及确保Sharp在初始化时能感知到字体环境。
一个完整的API处理函数示例:
import { NextApiRequest, NextApiResponse } from 'next';
import sharp from 'sharp';
import path from 'path';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { width = 800, height = 600, text = '示例文本' } = req.query;
try {
// 1. 构建SVG字符串,明确指定font-family
const svgBuffer = generateSvgBuffer({
w: Number(width),
h: Number(height),
text: String(text),
});
// 2. 使用Sharp进行转换
const pngBuffer = await sharp(svgBuffer)
.png() // 输出为PNG格式
.toBuffer();
// 3. 设置响应头并返回图片
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 'public, max-age=86400'); // 缓存一天
res.send(pngBuffer);
} catch (error) {
console.error('Image generation failed:', error);
res.status(500).json({ error: 'Failed to generate image' });
}
}
// 生成SVG Buffer的辅助函数
function generateSvgBuffer({ w, h, text }: { w: number; h: number; text: string }) {
// 注意font-family的设置顺序:优先使用我们配置的字体
const svgContent = `
<svg width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f0f0f0"/>
<text
x="50%"
y="50%"
font-family="'Noto Sans SC', 'Noto Sans', sans-serif"
font-size="60"
fill="#333"
text-anchor="middle"
dominant-baseline="middle"
>
${escapeXml(text)}
</text>
</svg>
`;
// 添加XML声明和编码,确保Sharp正确解析
const fullSvg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n${svgContent}`;
return Buffer.from(fullSvg, 'utf-8');
}
// 简单的XML特殊字符转义,防止文本内容破坏SVG结构
function escapeXml(unsafe: string): string {
return unsafe.replace(/[<>&'"]/g, (c) => {
switch (c) {
case '<': return '<';
case '>': return '>';
case '&': return '&';
case '\'': return ''';
case '"': return '"';
default: return c;
}
});
}
代码要点:
font-family链:"'Noto Sans SC', 'Noto Sans', sans-serif"。这是一个降级策略。首先尝试使用“Noto Sans SC”(思源黑体简体中文),如果不可用(理论上不会,因为我们提供了),则尝试“Noto Sans”(通用Noto字体),最后回退到系统默认的sans-serif(由于我们在fonts.conf中配置了别名,此时也会指向Noto字体)。- XML声明:在SVG字符串前添加
<?xml ... encoding="UTF-8"?>非常重要,它明确告诉解析器使用UTF-8编码来处理文本,这是正确渲染多语言字符(包括中文)的基础。 - 文本转义:
escapeXml函数防止用户输入的文本中包含<、>等字符,这些字符会破坏SVG的XML结构,导致Sharp解析失败或生成错误图片。
4. 部署验证与高级调试
完成以上步骤后,提交代码并推送到关联的Git仓库,触发Vercel自动部署。
4.1 验证部署是否成功
部署完成后,访问你的图片生成API端点,传入中文参数。如果一切顺利,你应该能看到正确显示中文的PNG图片。
如果问题依旧,可以按以下步骤进行深度调试:
- 检查Vercel构建日志:在Vercel项目的Deployment详情页,查看构建(Build)阶段日志,确认
fonts文件夹和fonts.conf文件是否被成功打包。 - 检查运行时日志:在Vercel项目的Functions页面,找到对应的Serverless函数,查看其运行时(Runtime)日志。过滤关键词如
Fontconfig、sharp或你代码中的错误输出。 - 在函数内打印环境信息(临时调试):
部署后查看日志,确认环境变量值是否正确,以及字体目录是否存在。// 在API路由开头临时添加 console.log('FONTCONFIG_PATH:', process.env.FONTCONFIG_PATH); console.log('Current working directory:', process.cwd()); console.log('Font dir exists?', require('fs').existsSync(path.join(process.cwd(), 'fonts')));
4.2 处理多字重与复杂样式
有时你可能需要粗体、斜体等不同样式。这就需要提供对应的字体文件,并在SVG和配置文件中进行更精细的控制。
扩展的 fonts 文件夹结构:
fonts/
├── NotoSansSC-Regular.ttf
├── NotoSansSC-Bold.ttf
├── NotoSansSC-Light.ttf
└── fonts.conf
对应的 fonts.conf 增强配置:
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>/var/task/fonts</dir>
<cachedir>/tmp/fonts-cache</cachedir>
<!-- 为Noto Sans SC家族定义具体的字体文件匹配 -->
<match target="pattern">
<test qual="any" name="family">
<string>Noto Sans SC</string>
</test>
<edit name="family" mode="assign" binding="strong">
<string>Noto Sans SC</string>
</edit>
</match>
<!-- 将不同字重映射到具体文件 -->
<match target="font">
<test name="family" compare="eq">
<string>Noto Sans SC</string>
</test>
<test name="weight" compare="eq">
<int>400</int> <!-- Regular -->
</test>
<edit name="file" mode="assign">
<string>/var/task/fonts/NotoSansSC-Regular.ttf</string>
</edit>
</match>
<match target="font">
<test name="family" compare="eq">
<string>Noto Sans SC</string>
</test>
<test name="weight" compare="eq">
<int>700</int> <!-- Bold -->
</test>
<edit name="file" mode="assign">
<string>/var/task/fonts/NotoSansSC-Bold.ttf</string>
</edit>
</match>
</fontconfig>
在SVG中,你就可以使用 font-weight 属性了:
<text font-family="Noto Sans SC" font-weight="700">这是粗体中文</text>
4.3 性能考量与字体子集化
中文字体文件通常较大(一个完整的思源黑体简体中文TTF文件可能超过10MB)。这可能会:
- 增加部署包体积,影响冷启动时间。
- 首次加载字体时消耗更多内存和时间。
对于性能敏感的应用,可以考虑 字体子集化(Font Subsetting)。即只提取你项目中实际用到的汉字字符,生成一个极小的字体文件。
使用工具如 glyphhanger 或 fonttools:
# 示例:使用 fonttools 的子集化功能 (需Python环境)
pip install fonttools
# 假设我们只需要“你好世界”这四个字
pyftsubset NotoSansSC-Regular.ttf --text="你好世界" --output-file=NotoSansSC-Subset.ttf
将生成的 NotoSansSC-Subset.ttf 放入 fonts 文件夹并更新 fonts.conf 引用即可。字体文件大小可能从10MB骤降到几十KB,对Serverless冷启动非常友好。
5. 避坑指南与最佳实践
踩过几次坑之后,我总结了一些能让这条路走得更顺的经验。
常见陷阱:
- 路径错误:这是最常见的问题。本地用
./fonts,Vercel上用/var/task/fonts,务必分清。在fonts.conf里写的路径,是容器内的绝对路径。 - 字体文件缺失:检查
.gitignore,确保fonts/目录及其内容被提交。可以在本地执行git add fonts/并git commit来确认。 - 缓存目录权限:确保
<cachedir>指向的目录(如/tmp/fonts-cache)在Vercel环境中是可写的。/tmp是标准选择。 - SVG格式不规范:缺少XML声明、编码声明错误,或者SVG文本中包含未转义的特殊字符,都可能导致Sharp解析失败,从而触发字体回退或直接错误。
推荐的最佳实践:
- 使用开源字体:优先选择像思源黑体(Noto Sans)、得意黑(Smiley Sans)这类优秀的开源中文字体,避免版权风险。
- 环境变量隔离:在
Vercel的项目设置中管理生产环境变量,在本地使用.env.local。可以考虑使用vercel env pull命令将生产环境变量拉取到本地用于测试。 - 将字体配置封装为可复用模块:如果你的项目中有多个地方需要图片生成,可以考虑将字体检查、SVG生成函数封装成一个独立的工具模块(如
lib/image-generator.ts),提高代码复用性和可维护性。 - 添加健康检查端点:创建一个简单的API端点(如
/api/font-check),输出当前环境识别的字体列表或渲染一个包含中英文的测试图片,便于快速验证部署后的字体状态。# 使用fc-list命令(如果容器内安装了fontconfig工具) console.log(require('child_process').execSync('fc-list').toString());
最后,记住Serverless环境是“无状态”且“短暂”的。每次请求都可能是一个全新的容器实例。因此,我们的字体解决方案必须是自包含的和快速初始化的。通过将字体和配置直接打包进项目,并利用 /tmp 目录进行缓存,我们正是在构建这样一个可靠、可移植的图片生成环境。现在,你的Next.js应用在Vercel上应该可以毫无压力地生成任何包含中文的图片了。
1717



被折叠的 条评论
为什么被折叠?



