把一个在本地跑得好好的 Astro 站点部署到边缘节点,然后它「部署成功但页面打不开」——这篇文章记录了从现象到模块加载层的完整排查路径。
现象:502 只发生在动态路由
静态页面一切正常,但所有走 SSR 运行时的路由都返回 502。日志里只有一行模糊的 entry.mjs load failed。
第一步是分离变量:把首页改成 prerender = true 走静态直出,动态路由保留。结果静态页 200、动态页 502——问题被锁定在 SSR 运行时的模块加载阶段。
二分:一个最小 ISR 页
与其在完整项目里猜,不如做一个只有十几行的最小探测页:
export const GET = () => {
return new Response(JSON.stringify({ ok: true, t: Date.now() }), {
headers: { "content-type": "application/json" },
};
};
把它挂到 /isr-min,逐个引入项目里的真实依赖。当引入某个包含原生二进制的包时,远端开始报错——本地 Node 能加载,边缘运行时不能。
根因:桩包与路径分隔符
最终定位到两个叠加的问题:
- 桩包:产物里出现了十几个只有
package.json而没有任何 JS 产物的目录。它们在本地的node_modules里是完整的,但构建产物的复制环节只搬走了清单文件。 - 路径分隔符:Windows 上生成的清单使用
\作为分隔符,Linux 运行时按/查找,于是「文件明明在包里却找不到」。
教训:跨平台部署问题几乎总是这两类——原生二进制、路径分隔符。先查这两样,再考虑代码。
修复:构建后自检
与其依赖人工检查,把校验做成构建步骤的一部分:
- 遍历产物里的所有依赖目录,发现「只有 package.json」的桩包就地修复;
- 最后用一次真实的
import()加载入口文件,加载失败直接让构建失败。
const mod = await import(entryPath);
if (typeof mod.default !== "function") {
throw new Error("SSR entry export invalid");
}
边界:prerender 与 ISR 怎么选
| 维度 | prerender | ISR |
|---|---|---|
| 首次访问 | 构建期直出 | 回源渲染后缓存 |
| 更新时机 | 重新构建 | 到期再生 / 按需 purge |
| 适用 | 内容稳定的首屏 | 列表、详情等可缓存页 |
静态资源直出的页面必然可用;需要「静态速度 + 动态更新」的页面交给 ISR,并登记好缓存 tag 以便按需失效。
收尾
- 「部署成功」只代表产物上传成功,不代表运行时能加载;
- 构建产物自检(真实 import)比任何文档都可靠;
- 平台差异要在 CI 里暴露,而不是在用户的 502 里暴露。