PAYATHON 2026

在 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/inputExpress 接收数据
/api/data/outputExpress 返回数据

前端的 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,以及函数部署产物中是否包含目标文件。未部署的文件需要补入函数包,单纯调整路径字符串无法解决。

备注:内容仅供参考。