在 Netlify 上部署 Node.js Express 应用后,无法向前端发送 HTML 文件怎么办?
这个报错表示,Express 在函数运行环境中查找 /var/task/data.html,但该路径下没有这个文件。需要检查 HTML 是否已打入函数包,以及 sendFile() 使用的路径是否与打包后的目录一致。
这类应用建议由 Netlify 直接托管 HTML、JS 和 CSS,Express 只处理数据接口。如果需要通过 Express 返回 HTML,就要明确将文件加入函数包。
为什么本地正常,部署后却找不到文件?
process.cwd() 返回 Node.js 进程的当前工作目录。这次部署中的工作目录是 /var/task,因此:
path.join(process.cwd(), "data.html")
得到的是:
/var/task/data.html
Netlify 不会因此把整个项目复制到该目录。
Netlify 的静态站点文件和 Functions 分别部署。publish = "dist" 指定静态文件的发布目录,函数打包工具则负责收集函数代码和依赖。即使 HTML 已放进 dist,它也不会自动成为函数可以读取的本地文件。Netlify 构建配置说明
原配置还包含:
[[redirects]]
force = true
status = 200
from = "/*"
to = "/.netlify/functions/index"
这条规则会把所有匹配请求强制交给函数,包括原本可以直接返回的静态文件。即使发布目录中有对应的 HTML,也会优先执行这条规则。Netlify 强制重写规则
题目没有提供实际目录树,无法确定 data.html 原本放在哪里。可以确定的是,函数读取的路径与部署产物不一致。
推荐方案:静态文件直接发布,Express 处理接口
以下示例使用 CommonJS,并假设项目根目录就是 Netlify 的 base directory。
1. 整理静态文件
将静态文件放入发布目录:
project/
├── dist/
│ ├── index.html
│ ├── data.html
│ ├── index.js
│ └── index.css
├── netlify/
│ └── functions/
│ └── api.js
├── server.js
├── package.json
└── netlify.toml
如果这些文件由构建生成,需要确认构建完成后它们已出现在 dist 中。仅设置 publish = "dist" 不会自动创建或复制文件。
在 HTML 中直接引用静态资源:
<link rel="stylesheet" href="/index.css">
<script src="/index.js" defer></script>
随后可以删除 Express 中专门发送 HTML、JS 和 CSS 的路由。
2. 将 Express 改为数据接口
安装依赖:
npm install express serverless-http
server.js:
const express = require("express");
const app = express();
const router = express.Router();
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
// 仅用于演示,不作为持久化存储。
const data = Object.create(null);
router.post("/data/input", (req, res) => {
const { id, name } = req.body || {};
if (typeof id !== "string" || typeof name !== "string") {
return res.status(400).json({
error: "id 和 name 必须是字符串"
});
}
data[id] = name;
return res.status(200).json({ ok: true });
});
router.post("/data/output", (req, res) => {
return res.json(data);
});
app.use("/api", router);
module.exports = { app };
这里使用了 Express 内置的请求体解析中间件,不再需要 body-parser。
3. 导出 Netlify Function 的 handler
netlify/functions/api.js:
const serverless = require("serverless-http");
const { app } = require("../../server");
exports.handler = serverless(app);
module.exports = { app } 导出了 Express 实例,函数入口还需要将它适配为 Netlify Functions 的 handler。Netlify 的 Express 部署示例也采用这种方式,函数入口无需调用 app.listen()。Express on Netlify
4. 只将 API 请求转给函数
将 netlify.toml 改为:
[build]
publish = "dist"
functions = "netlify/functions"
[functions]
node_bundler = "esbuild"
external_node_modules = ["express"]
[[redirects]]
from = "/data"
to = "/data.html"
status = 200
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/api/:splat"
status = 200
force = true
删除原先的 /* 强制重写规则。
| 路径 | 处理方式 |
|---|---|
/ | 返回 dist/index.html |
/data | 重写到静态页面 data.html |
/index.js、/index.css | 返回静态资源 |
/api/data/input | Express 接收数据 |
/api/data/output | Express 返回数据 |
前端的 fetch() 地址和表单 action 也要相应更新,例如:
async function saveData() {
const response = await fetch("/api/data/input", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: "1",
name: "Alice"
})
});
if (!response.ok) {
throw new Error(`请求失败:${response.status}`);
}
return response.json();
}
如果不需要保留 /data 这个地址,可以直接链接到 /data.html,并删除对应的页面重写规则。
另一种方案:继续使用 res.sendFile()
如果页面需要先经过服务端逻辑,再由 Express 返回,可以保留原有路由,通过 included_files 指定函数需要携带的文件。
假设 index.html、data.html、index.js 和 index.css 都在项目根目录,函数入口名为 index.js:
project/
├── index.html
├── data.html
├── index.js
├── index.css
├── server.js
├── dist/
├── netlify/
│ └── functions/
│ └── index.js
└── netlify.toml
配置如下:
[build]
publish = "dist"
functions = "netlify/functions"
[functions]
node_bundler = "esbuild"
external_node_modules = ["express", "body-parser"]
[functions.index]
included_files = [
"index.html",
"data.html",
"index.js",
"index.css"
]
[[redirects]]
from = "/*"
to = "/.netlify/functions/index/:splat"
status = 200
force = true
included_files 的路径按 Netlify 的 base directory 解析,用来向函数包补充文件。external_node_modules 管理 npm 依赖,无法代替 included_files 携带 HTML。path 是 Node.js 内置模块,应从 external_node_modules 中移除。Netlify 函数打包配置
函数入口 netlify/functions/index.js:
const serverless = require("serverless-http");
const { app } = require("../../server");
exports.handler = serverless(app);
题目中的当前工作目录是 /var/task。如果文件按上述根目录结构打入函数包,原路由可以继续使用:
app.get("/data", (req, res) => {
res.sendFile(path.join(process.cwd(), "data.html"));
});
如果文件实际位于 pages/data.html,配置和代码都要包含这一层目录:
[functions.index]
included_files = ["pages/**"]
app.get("/data", (req, res) => {
res.sendFile(path.join(process.cwd(), "pages", "data.html"));
});
不要将路径写死为 /var/task。单纯把 process.cwd() 换成 __dirname 也不能解决文件未打包的问题:前者是进程工作目录,后者是当前模块所在目录,打包过程还可能改变模块位置。无论使用哪个基准路径,都要先将文件加入函数包。
还需要修正哪些问题?
原来的 /data/input 路由没有结束响应,需要补上响应调用:
app.post("/data/input", (req, res) => {
const dataPkg = req.body;
data[dataPkg.id] = dataPkg.name;
res.sendStatus(200);
});
如果不调用 res.send()、res.json() 或 res.end() 等方法,请求会一直等待,最终可能超时。这与 HTML 的 ENOENT 错误是两个独立问题,都需要处理。
data = {} 也应改为显式声明变量。即使改成 const data = {},内存对象仍无法可靠保存业务数据:函数实例可能被复用或回收,也可能有多个实例并行处理请求。需要跨请求可靠保存的数据,应写入数据库或其他持久化存储。
重新部署后,分别检查页面、静态资源和 POST 接口。如果仍出现 ENOENT,可以临时记录路径:
const fs = require("fs");
const filePath = path.join(process.cwd(), "data.html");
console.log({
cwd: process.cwd(),
filePath,
exists: fs.existsSync(filePath)
});
接着核对文件名大小写、included_files 所依据的 base directory,以及函数部署产物中是否包含目标文件。未部署的文件需要补入函数包,单纯调整路径字符串无法解决。
备注:内容仅供参考。
