返回博客

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。
坑:生产环境 fetch('/api') 命中的是 HTML,不是你的 Mock 数据

静态宿主不区分「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 效果不一致

正解:生产走内存 mock,开发走 MSW 网络拦截,共用一套生成函数
  • 不在生产启用 Service Worker(避免 hydration 闪白问题);
  • 不在生产发出注定失败的 fetch('/api')
  • mock 数据/时序只有一份定义,两条路径共用,永不漂移。

排查清单

修完之后用这两步验证:

# 1. 确认所有 /api fetch 都有 prod 分支
grep -rn "fetch.*['\"\`]/api" app/

逐条确认每处 fetch('/api/...') 要么有 NODE_ENV === 'production' 的内存分支,要么压根不走 fetch(直接 import mock 工厂函数)。

  1. 在真实浏览器里访问线上 URL,手动触发每个 Demo,确认数据正常产出。curl 和无头截图验证不了客户端 JS 的运行时行为,必须用真实浏览器。

一句话外卖

静态展示站里,MSW 只属于开发环境;凡是 Demo 需要的数据,生产分支必须有一条不经过网络的内存兜底路径。