在 Vercel 上部署 Next.js 14.1 应用程序时,我一直在努力解决一个问题。具体来说,我在尝试访问服务器操作中的本地文件时遇到问题。在搜索了大量 StackOverflow 线程和 GenAI 答案后,我还没有找到可行的解决方案。
我有一个服务器操作,可以使用
template.pdf
文件和多个字体文件(例如 .ttf
文件)生成 PDF。这些文件存储在 actions/pdf_data
文件夹中。但是,在生产中,当我尝试访问这些文件时,我收到 ENOENT
(文件未找到)错误,因为生产环境中似乎缺少目录和文件。
我尝试通过在我的
next.config.js
文件中添加以下配置来解决此问题:
webpack: (config, { dev, isServer }) => {
if (!dev && isServer) {
config.plugins.push(
new CopyPlugin({
patterns: [
{
from: path.join(__dirname, "actions", "pdf_data"),
to: path.join(
__dirname,
".next",
"server",
"actions",
"pdf_data",
),
},
],
}),
);
}
return config;
},
但是,当我在生产环境中执行服务器操作期间尝试读取文件时,
.next/server
文件夹内的目录结构如下所示:
directory contents: [ 'app', 'app-paths-manifest.json', 'chunks', 'font-manifest.json', 'middleware-build-manifest.js', 'middleware-manifest.json', 'middleware-react-loadable-manifest.js', 'next-font-manifest.js', 'next-font-manifest.json', 'pages', 'pages-manifest.json', 'server-reference-manifest.js', 'server-reference-manifest.json', 'webpack-runtime.js']
我的解决方案似乎不起作用,因为在 Vercel 边缘环境中,仅生成某些文件,而不是构建时存在的完整 .next 包。
我正在考虑直接在服务器操作中导入必要的文件,因为这可能会强制构建过程正确捆绑文件。但是,我不确定这种方法是否有效或如何有效实施。
是否有人遇到过类似问题或找到了在 Vercel 上的服务器操作中访问本地文件的可靠解决方案?
PS:我知道可以使用
@vercel/blob
来存储文件,但我不想使用它,因为它会显着增加生成 PDF 所需的时间。我正在寻找一种替代解决方案来避免这种开销。
"use server";
import { PDFDocument, PDFFont, PDFImage, rgb } from "pdf-lib";
import * as fs from "fs";
import * as path from "path";
import { User } from "@/utils/userTypes";
import { db, storage, getCurrentUser } from "@/lib/firebase/firebase-admin";
import fontkit from "@pdf-lib/fontkit";
import sharp from "sharp";
export default async function generatePDF(id: string) {
const pdfDataDir = path.join(process.cwd(), ".next/server/actions/pdf_data");
const [templatePath, arialPath, arialItalicsPath, arialBoldPath] = [
"template.pdf",
"ARIAL.TTF",
"ARIALI.TTF",
"ARIALBD.TTF",
].map((filename) => path.join(pdfDataDir, filename));
logger.info("File paths:", {
templatePath,
arialPath,
arialItalicsPath,
arialBoldPath,
});
[templatePath, arialPath, arialItalicsPath, arialBoldPath].forEach(
(filePath) => {
if (!fs.existsSync(filePath)) {
logger.error(`File not found at path: ${filePath}`);
throw new Error(`File not found at path: ${filePath}`);
}
},
);
const [pdfDoc, arialFont, arialItalicsFont, arialBoldFont] =
await Promise.all([
PDFDocument.load(fs.readFileSync(templatePath)),
fs.promises.readFile(arialPath),
fs.promises.readFile(arialItalicsPath),
fs.promises.readFile(arialBoldPath),
]);
pdfDoc.registerFontkit(fontkit);
const [arial, arialItalics, arialBold, dbData] = await Promise.all([
pdfDoc.embedFont(arialFont),
pdfDoc.embedFont(arialItalicsFont),
pdfDoc.embedFont(arialBoldFont),
getInfoFromDB(id),
]);
...
...
const base64String = Buffer.from(pdfBytes).toString("base64");
return base64String;
}
讨论: https://github.com/vercel/next.js/discussions/70125
从服务器操作切换到 api 路由,我可以相应地访问我的文件。
尽管我的问题已解决,但我仍在尝试找出在使用服务器操作时是否有其他方法可以解决此问题
以下是一些可能的解决方案和解决方法:
使用
public
目录:
将静态文件(PDF 和字体)存储在 public
目录中。这些将在运行时访问。相应地更新您的文件路径:
const pdfDataDir = path.join(process.cwd(), "public", "pdf_data");
动态导入: 尝试动态导入您的文件:
const templateBuffer = await import('../public/pdf_data/template.pdf');
const pdfDoc = await PDFDocument.load(templateBuffer);
字体的数据 URI: 对于字体等较小的文件,请考虑将它们转换为数据 URI 并将它们直接包含在代码中:
const arialFontDataUri = "data:font/ttf;base64,BASE64_ENCODED_FONT_DATA";
const arialFont = await pdfDoc.embedFont(arialFontDataUri);
使用
next/config
:
利用 next/config
访问运行时配置值,其中可能包括文件路径:
import getConfig from 'next/config';
const { serverRuntimeConfig } = getConfig();
const pdfDataDir = serverRuntimeConfig.pdfDataDir;
自定义服务器设置: 如果可能,请考虑使用允许更传统的文件系统访问的自定义服务器设置。
外部文件存储: 虽然您提到不喜欢使用 @vercel/blob,但请考虑其他存储解决方案,例如 AWS S3 或 Google Cloud Storage,用于生产中的文件访问。
捆绑策略: 使用 webpack 的
copy-webpack-plugin
或 Next.js 的自定义 webpack 配置进行调查,以确保您的文件正确捆绑并在生产中可用。
您当前使用 API 路由而不是服务器操作的解决方案是一个有效的解决方法。如果您想坚持使用服务器操作,您可能需要尝试结合上述建议。
请记住,核心问题是无服务器功能没有持久性文件系统,因此任何解决方案都需要解决此限制。您可能需要调整 PDF 生成过程以处理与应用程序捆绑在一起或通过外部存储访问的文件。
如果这些解决方案都不适合您的特定用例,您可能需要考虑重组您的应用程序,以便更好地应对无服务器约束,或探索提供更传统服务器环境的替代部署选项。