MSW 本地跑得好,静态部署后 Demo 永远转圈
做展示站时,我在本地用 MSW(Mock Service Worker)拦截所有 /api/* 请求,效果完美——聊天流式输出、分页表格、各种异步 Demo 全部丝滑。然后我把站 next build && next export 推到 CDN,打开页面,聊天窗口里那个光标就再也停不下来了,永远显示「...」,加载圈一直转。
本地没问题,线上挂死,这种差异往往有一个精准的根因。
现象
- 本地
next dev一切正常,MSW 在 Network 面板里拦截了/api/chat,流式响应顺畅。 - 部署到 Cloudflare Pages / Vercel Static / 任何纯静态宿主后,Demo 组件进入永久 loading 状态。
- 打开 Network 面板,
/api/chat请求返回了200,但响应体是一段 HTML(SPA shell 或 404 页)。
根因
MSW 是为开发环境设计的。展示站里的 MswProvider 通常长这样:
// 常见写法——也是坑所在
const [ready, setReady] = useState(
process.env.NODE_ENV === "production" // prod:直接置 true,跳过 MSW
);
useEffect(() => {
if (process.env.NODE_ENV === "production") return; // prod:不启动 worker
startMsw().then(() => setReady(true));
}, []);这段逻辑的出发点是好的:生产环境不注册 Service Worker,避免 if (!ready) return null 造成 SSG 内容在 hydration 时闪白。
但代价是:生产环境里根本没有 MSW。
next output:"export" 生成的是纯静态文件,没有 Node.js 路由层。当 Demo 发出 fetch('/api/chat') 时,CDN 找不到对应文件,直接把 index.html(或 404.html)当作响应返回——状态码还是 200。
接收方拿到这个响应:
- 聊天流:尝试把 HTML 解析成 SSE
data:帧,一帧都找不到,助手气泡永远停在「...」; - 列表/分页:
response.json()抛异常或返回垃圾,页面卡在 loading skeleton。
静态宿主不区分「API 请求」和「页面请求」,一律返回 SPA shell。MSW 在生产里被主动关掉,所以这个 fetch 什么都得不到——它只是悄悄地永远挂起,不报任何错误。
正解
给每一个 fetch('/api/...') 调用加一条生产分支,在客户端内存里直接生成数据,复用和 MSW handler 相同的 mock 生成函数(单一数据源,不重复定义逻辑):
// sendChatMessage.ts
export async function sendChatMessage(text: string, options: ChatOptions) {
const { signal, onEvent } = options;
// 生产分支:内存里跑相同的生成逻辑
if (process.env.NODE_ENV === "production") {
for (const event of scriptToEvents(selectScript(text))) { // 与 MSW handler 共用同一函数
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
onEvent(event);
await sleep(eventDelayMs(event)); // 共用同一计时函数
}
return;
}
// 开发分支:走网络,MSW 拦截
const res = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ text }),
signal,
});
// ... 解析 SSE 流
}关键点:scriptToEvents / selectScript / eventDelayMs 这些函数从 mock 包统一导出,MSW handler 和生产兜底都调用它们,行为严格对齐,不会出现开发/生产 Demo 效果不一致。
- 不在生产启用 Service Worker(避免 hydration 闪白问题);
- 不在生产发出注定失败的
fetch('/api'); - mock 数据/时序只有一份定义,两条路径共用,永不漂移。
排查清单
修完之后用这两步验证:
# 1. 确认所有 /api fetch 都有 prod 分支
grep -rn "fetch.*['\"\`]/api" app/逐条确认每处 fetch('/api/...') 要么有 NODE_ENV === 'production' 的内存分支,要么压根不走 fetch(直接 import mock 工厂函数)。
- 在真实浏览器里访问线上 URL,手动触发每个 Demo,确认数据正常产出。
curl和无头截图验证不了客户端 JS 的运行时行为,必须用真实浏览器。
一句话外卖
静态展示站里,MSW 只属于开发环境;凡是 Demo 需要的数据,生产分支必须有一条不经过网络的内存兜底路径。