做搜索数据工具的前端时,我一开始的流程很粗糙:本地开发把 fetch 指向真实接口,用开发环境的 Key 直接调。结果三个问题接踵而至------调试一个加载状态刷新十次页面就是十次请求;下班后想改样式,网络一抖就全报错;更别提 Key 在本地散落,随时可能被提交进仓库。后来用 MSW(Mock Service Worker)把接口 Mock 掉,开发体验才算正常。这篇记录整套做法。
为什么是「网络层 Mock」而不是改代码
一些人的做法是在代码里加开关:if (isDev) return fakeData。这有两个坏处:业务代码被污染,且真实请求路径在开发时根本没被走到。
MSW 的思路不同------它用 Service Worker 在浏览器网络层拦截请求 。你的 fetch 代码一行不改,请求发出去就被接管,返回你定义的 mock 数据。切到真实接口时,把 mock 关掉即可,代码路径完全一致。
第一步:采集一次真实响应做 fixture
Mock 数据不能凭空编------用真实响应做样本,字段结构才保真。用一个脚本各调一次需要的端点,把响应存下来:
python
import json, os, requests
FIXTURES = [
{"endpoint": "search", "params": {"q": "搜索 api", "hl": "zh-cn", "gl": "cn"}},
{"endpoint": "search", "params": {"q": "python tutorial", "hl": "en", "gl": "us"}},
]
for f in FIXTURES:
resp = requests.post(
f"https://api.serpbase.dev/google/{f['endpoint']}",
headers={"Content-Type": "application/json", "X-API-Key": os.environ["SERPBASE_API_KEY"]},
json=f["params"], timeout=30,
)
name = f"{f['endpoint']}-{f['params']['q'].replace(' ', '-')}.json"
json.dump(resp.json(), open(f"mocks/fixtures/{name}", "w", encoding="utf-8"), ensure_ascii=False)
print("saved", name)
注意采集「不完美」的样本 :至少存两份------一份字段齐全的,一份 snippet 缺失或没有精选摘要的。理由后面讲。
第二步:写 handlers
ts
// mocks/handlers.ts
import { http, HttpResponse, delay } from "msw";
import searchCn from "./fixtures/search-搜索-api.json";
import searchEn from "./fixtures/search-python-tutorial.json";
const fixtures: Record<string, unknown> = {
"搜索 api": searchCn,
"python tutorial": searchEn,
};
export const handlers = [
http.post("https://api.serpbase.dev/google/search", async ({ request }) => {
const body = (await request.json()) as { q?: string };
await delay(400); // 模拟真实延迟:让骨架屏/加载态真的有机会出现
// 场景:接口错误(业务错误信封)
if (body.q === "错误测试") {
return HttpResponse.json({ status: 1, request_id: "mock_err", error: "upstream_timeout" });
}
// 场景:命中 fixture
if (body.q && fixtures[body.q]) {
return HttpResponse.json(fixtures[body.q]);
}
// 场景:空结果 / 字段缺失样本
return HttpResponse.json({
status: 0,
request_id: "mock_req_001",
elapsed_ms: 420,
credits_charged: 0, // mock 不扣费
search_type: "search",
query: body.q,
page: 1,
organic: [
{ rank: 1, title: "无摘要样本", link: "https://example.com/1" }, // 故意没有 snippet
],
});
}),
];
三个 mock 场景是刻意为之:错误信封 测错误处理、缺字段样本 测 .get() 兜底、没有 fixture 的查询走默认空结果。
第三步:启动与开关
ts
// app 入口(仅开发环境)
if (process.env.NODE_ENV === "development" && process.env.NEXT_PUBLIC_API_MOCK === "1") {
const { worker } = await import("./mocks/browser");
await worker.start({ onUnhandledRequest: "bypass" }); // 未 mock 的请求放行
}
ts
// mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
.env.development 里设 NEXT_PUBLIC_API_MOCK=1;需要联调真实接口时改成 0 即可,回归成本几乎为零。
踩坑记录
坑 1:fixture 全是「理想响应」。 样本里字段全有,开发时一切正常,上线遇到 snippet 缺失直接崩。fixture 里必须混着缺字段的样本。
坑 2:没模拟延迟。 Mock 瞬间返回,加载态永远闪不出来,等真接口上来才发现骨架屏布局歪了。delay(400) 一行解决。
坑 3:Mock 进了生产包。 worker.start() 没加环境判断,线上也拦截请求。用 NODE_ENV === "development" + 环境变量双重判断。
坑 4:fixture 半年不更新。 真实接口加了新模块,fixture 还是老样子,开发永远看不到新字段。给 fixture 加「保质期」:每个季度重跑一次采集脚本。
坑 5:从不碰真实接口。 全 Mock 开发久了,不知道真实响应长啥样。建议留一个「冒烟脚本」,发布前用真实 Key 跑一次关键路径(几次请求而已,免费额度足够)。
工程清单
- 采集真实响应做 fixture,必须包含缺字段样本
- MSW 网络层拦截,业务代码零污染
- handler 覆盖:正常/错误信封/空结果/延迟四类场景
NODE_ENV+ 环境变量双重开关,杜绝进生产- fixture 定期重录;发布前留一次真实接口冒烟
Mock 的本质不是「造假数据」,而是把「接口的不确定性」从日常开发里拿掉:加载态、错误态、缺字段态都能随时复现,请求不花钱、不吃限流、离线也能写代码。开发速度的提升,比省下的那点 credits 值钱得多。
采集 fixture 时每个端点一次调用,注册送的 100 次免费搜索足够覆盖;接口的错误信封和字段结构见 SerpBase 官方文档,照着写 handler 不会跑偏。你们前端联调是直连真实接口还是 Mock?评论区聊聊。