我最近遇到了同样的问题,我终于找到了解决方案。我不是大师,所以有人可能会提出更好的方法,但这对我有用。
由于 Vercel 运行其无服务器函数的方式,函数并不真正了解项目的其余部分或公用文件夹。这是有道理的(因为安全性),但是当您需要文件的实际路径时确实会变得很棘手。你可以导入字体文件没有问题,构建过程会给它一个新的名字并把它放在磁盘上(in /var/task),但你不能访问它。path.resolve(_font_name_)可以看到,但无法访问。
我最终编写了一个非常糟糕的单独 api 页面,该页面使用path.join并fs.readdirSync查看从 api 页面实际可见的文件。可见的一件事是 node_modules 文件夹,其中包含该 api 页面上使用的模块的文件。
fs.readdirSync(path.join(process.cwd(), 'node_modules/')
所以我所做的是编写一个本地模块,将其安装在我的项目中,然后将其导入我的 api 页面。在本地模块中package.json,我有一行"files": ["*"],因此它将所有模块文件捆绑到其 node_modules 文件夹中(而不仅仅是 .js 文件)。在我的模块中,我有我的字体文件和一个将其复制到/tmp(/tmp可读和可写)然后返回文件路径的函数,/tmp/Roboto-Regular.ttf.
在我的 api 页面上,我包含了这个模块,然后运行它,并将生成的路径传递给registerfont.
有用。我会分享我的代码,但现在很草率,我想先清理它并尝试几件事(比如我不确定是否需要将它复制到 /tmp,但我没有t 没有那个步骤就对其进行了测试)。当我把它理顺时,我会编辑这个答案。
-- 编辑由于我无法改进我原来的解决方案,让我提供一些关于我所做的更多细节。
在我的 package.json 中,我添加了一行来包含一个本地模块:
"dependencies": {
"canvas": "^2.6.1",
"fonttrick": "file:fonttrick",
在我的项目根目录中,我有一个文件夹“fonttrick”。文件夹内是另一个 package.json:
{
"name": "fonttrick",
"version": "1.0.6",
"description": "a trick to get canvas registerfont to work in a Vercel serverless function",
"license": "MIT",
"homepage": "https://grumbly.games",
"main": "index.js",
"files": [
"*"
],
"keywords": [
"registerfont",
"canvas",
"vercel",
"zeit",
"nextjs"
]
}
这是我必须编写的唯一本地模块;关键字没有任何作用,但起初我考虑将其放在 NPM 上,所以它们就在那里。
fonttrick 文件夹还包含我的字体文件(在本例中为“Roboto-Regular.ttf”)和一个主文件index.js:
module.exports = function fonttrick() {
const fs = require('fs')
const path = require('path')
const RobotoR = require.resolve('./Roboto-Regular.ttf')
const { COPYFILE_EXCL } = fs.constants;
const { COPYFILE_FICLONE } = fs.constants;
//const pathToRoboto = path.join(process.cwd(), 'node_modules/fonttrick/Roboto-Regular.ttf')
try {
if (fs.existsSync('/tmp/Roboto-Regular.ttf')) {
console.log("Roboto lives in tmp!!!!")
} else {
fs.copyFileSync(RobotoR, '/tmp/Roboto-Regular.ttf', COPYFILE_FICLONE | COPYFILE_EXCL)
}
} catch (err) {
console.error(err)
}
return '/tmp/Roboto-Regular.ttf'
};
我在这个文件夹中运行了npm install,然后 fonttrick 在我的主项目中作为一个模块可用(不要忘记在那里运行npm install)。
由于我只需要将其用于 API 调用,因此该模块仅在一个文件中使用,/pages/api/[img].js
import { drawCanvas } from "../../components/drawCanvas"
import { stringIsValid, strToGameState } from '../../components/gameStatePack'
import fonttrick from 'fonttrick'
export default (req, res) => { // { query: { img } }
// some constants
const fallbackString = "1xThe~2ysent~3zlink~4yis~5wnot~6xa~7xvalid~8zsentence~9f~~"
// const fbs64 = Buffer.from(fallbackString,'utf8').toString('base64')
// some variables
let imageWidth = 1200 // standard for fb ogimage
let imageHeight = 628 // standard for fb ogimage
// we need to remove the initial "/api/" before we can use the req string
const reqString64 = req.url.split('/')[2]
// and also it's base64 encoded, so convert to utf8
const reqString = Buffer.from(reqString64, 'base64').toString('utf8')
//const pathToRoboto = path.join(process.cwd(), 'node_modules/fonttrick/Roboto-Regular.ttf')
let output = null
if (stringIsValid({ sentenceString: reqString })) {
let data = JSON.parse(strToGameState({ canvasURLstring: reqString }))
output = drawCanvas({
sentence: data.sentence,
cards: data.cards,
width: imageWidth,
height: imageHeight,
fontPath: fonttrick()
})
} else {
let data = JSON.parse(strToGameState({ canvasURLstring: fallbackString }))
output = drawCanvas({
sentence: data.sentence,
cards: data.cards,
width: imageWidth,
height: imageHeight,
fontPath: fonttrick()
})
}
const buffy = Buffer.from(output.split(',')[1], 'base64')
res.statusCode = 200
res.setHeader('Content-Type', 'image/png')
res.end(buffy)
}
这样做的重要部分是import fonttrick,它将字体的副本放在 tmp 中,然后返回该文件的路径;然后将字体的路径传递给画布绘图函数(以及其他一些东西;绘制什么,绘制多大等)
我的绘图功能本身在components/drawCanvas.js;这是开头的重要内容(TLDR版本:如果从 API 页面调用它,它将获得字体的路径;如果是,则使用该路径,否则常规系统字体可用):
import { registerFont, createCanvas } from 'canvas';
import path from 'path'
// width and height are optional
export const drawCanvas = ({ sentence, cards, width, height, fontPath }) => {
// default canvas size
let cw = 1200 // canvas width
let ch = 628 // canvas height
// if given different canvas size, update
if (width && !height) {
cw = width
ch = Math.floor(width / 1.91)
}
if (height && width) {
cw = width
ch = height
}
if (height && !width) {
ch = height
cw = Math.floor(height * 1.91)
}
// this path is only used for api calls in development mode
let theFontPath = path.join(process.cwd(), 'public/fonts/Roboto-Regular.ttf')
// when run in browser, registerfont isn't available,
// but we don't need it; when run from an API call,
// there is no css loaded, so we can't get fonts from @fontface
// and the canvas element has no fonts installed by default;
// in dev mode we can load them from local, but when run serverless
// it gets complicated: basically, we have a local module whose only
// job is to get loaded and piggyback the font file into the serverless
// function (thread); the module default function copies the font to
// /tmp then returns its absolute path; the function in the api
// then passes that path here so we can load the font from it
if (registerFont !== undefined) {
if (process.env.NODE_ENV === "production") {
theFontPath = fontPath
}
registerFont(theFontPath, { family: 'Roboto' })
}
const canvas = createCanvas(cw, ch)
const ctx = canvas.getContext('2d')
此 API 路径在我的游戏的标题中使用,在元标记中用于在 facebook 或 twitter 或任何地方共享页面时按需创建图像:
<meta property="og:image" content={`https://grumbly.games/api/${returnString}`} />
反正。丑陋而骇人听闻,但它对我有用。