起因
后端 API 还没好,前端要先做 UI;或者测试时不想真打后端(速度 + 隔离)。
传统做法:在 fetch 调用处 if (mocked) return mockData,丑陋且要清理。
MSW 在 service worker / Node 层拦截 HTTP 请求,让"前端代码无修改 +
真的发请求 + 拦截返 mock 数据"。同一套 mock 给开发 / 测试 / Storybook
共用。
解决方案
装
npm i -D msw
npx msw init public/ --save # 给浏览器生成 service worker 文件
写 handler
// src/mocks/handlers.ts
import { http, HttpResponse, delay } from 'msw'
export const handlers = [
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'Alice',
email: '[email protected]',
})
}),
http.get('/api/users', async ({ request }) => {
const url = new URL(request.url)
const page = Number(url.searchParams.get('page') ?? 1)
await delay(300) // 模拟网络延迟
return HttpResponse.json({
items: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
page,
hasMore: page < 5,
})
}),
http.post('/api/login', async ({ request }) => {
const body = await request.json() as { email: string; pw: string }
if (body.email === '[email protected]' && body.pw === 'secret') {
return HttpResponse.json({ token: 'fake-jwt-token' })
}
return new HttpResponse(null, { status: 401 })
}),
http.delete('/api/posts/:id', async () => {
await delay(200)
return new HttpResponse(null, { status: 204 })
}),
]
浏览器集成(dev)
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
// src/main.ts 入口
async function enableMocking() {
if (import.meta.env.MODE !== 'development') return
const { worker } = await import('./mocks/browser')
return worker.start({
onUnhandledRequest: 'bypass', // 未 mock 的请求放行
})
}
enableMocking().then(() => {
// 启动你的 React / Vue app
ReactDOM.createRoot(...).render(<App />)
})
npm run dev,所有 fetch / axios 调用被 MSW 拦截返 mock。
Console 显示:
[MSW] GET /api/users/1 (200 OK)
[MSW] GET /api/users?page=1 (200 OK)
DevTools Network 也能看到这些请求(带 [from service worker] 标记)。
Node 集成(Jest / Vitest 测试)
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// vitest.setup.ts (或 jest.setup.ts)
import { server } from './src/mocks/server'
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
测试代码不需要 mock fetch,直接调真函数:
test('login flow', async () => {
const { getByLabelText, getByRole } = render(<LoginForm />)
await userEvent.type(getByLabelText('Email'), '[email protected]')
await userEvent.type(getByLabelText('Password'), 'secret')
await userEvent.click(getByRole('button', { name: 'Sign in' }))
await waitFor(() => {
expect(localStorage.getItem('token')).toBe('fake-jwt-token')
})
})
MSW 在 Node 端拦截 fetch(Node 18+ 内置 fetch)/ axios,返 mock 数据。
单测里 override handler
test('shows error on 500', async () => {
server.use(
http.get('/api/users', () => {
return new HttpResponse(null, { status: 500 })
}),
)
const { findByText } = render(<UserList />)
expect(await findByText(/something went wrong/i)).toBeInTheDocument()
})
server.use(...) 临时覆盖;每个测试 resetHandlers() 清回基准。
Storybook 集成
npm i -D msw-storybook-addon
.storybook/preview.ts:
import { initialize, mswLoader } from 'msw-storybook-addon'
initialize()
export default {
loaders: [mswLoader],
}
每个 story 设特定 handler:
export const ErrorState: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/users', () => new HttpResponse(null, { status: 500 })),
],
},
},
}
Storybook 里展示"网络错误"状态而不需要真实坏后端。
E2E (Playwright)
import { test, expect } from '@playwright/test'
test('login', async ({ page }) => {
await page.route('/api/login', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ token: 'fake' }),
})
})
await page.goto('/login')
await page.fill('[name=email]', '[email protected]')
await page.fill('[name=password]', 'secret')
await page.click('button[type=submit]')
await expect(page).toHaveURL('/dashboard')
})
Playwright 自己有 page.route 类似机制;用 MSW 时直接复用 handlers
更统一。
动态响应(基于状态)
mock 用户数据库 + 增删改:
let users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
export const handlers = [
http.get('/api/users', () => HttpResponse.json(users)),
http.post('/api/users', async ({ request }) => {
const data = await request.json() as { name: string }
const u = { id: Date.now(), name: data.name }
users.push(u)
return HttpResponse.json(u, { status: 201 })
}),
http.delete('/api/users/:id', ({ params }) => {
users = users.filter(u => String(u.id) !== params.id)
return new HttpResponse(null, { status: 204 })
}),
]
前端做 CRUD 流,mock 像真的在工作。
与 OpenAPI 集成
npm i -D openapi-msw
import { createOpenApiMock } from 'openapi-msw'
import type { paths } from './generated-api-types'
const http = createOpenApiMock<paths>()
export const handlers = [
http.get('/api/users/{id}', () => HttpResponse.json({
id: 1,
name: 'Alice',
// 类型完全跟着 OpenAPI schema,缺字段编译报错
})),
]
后端给 OpenAPI 文档 → 前端类型 + mock 一起自动有,零手写。
效果
- 后端 delay 一周时前端可独立开发
- 单测从"mock fetch 函数"升到"在网络层拦截",更接近真实场景
- Storybook 演示"loading / error / empty / 满数据"四种状态都有
- E2E 测试稳定(不依赖真后端 / 不污染数据库)
- Mock data 文件被 dev / test / Storybook 共用,单一来源
与替代品对比
| MSW | json-server | nock | jest.mock('axios') | |
|---|---|---|---|---|
| 拦截层 | network (sw / node) | 真后端 | node only | 模块 mock |
| 浏览器支持 | ✅ | ✅ | ❌ | ❌ |
| 真发 HTTP | ✅ | ✅ | ❌(模拟) | ❌ |
| 灵活程度 | 极高 | 中(REST 模板) | 中 | 高 |
| Storybook 集成 | ✅ | 难 | 无 | 无 |
MSW 全面胜出,是 2024 后的事实标准。
踩过的坑
-
Service Worker 没注册成功:
npx msw init没把 mockServiceWorker.js
放对位置(应在 publicly served root)。生产 build 后路径变也会失效。 -
MSW 拦截了真实生产请求:忘了
if (MODE === 'development')包裹
worker.start()。线上版本不能注入 MSW。 -
Node fetch vs cross-fetch:MSW 在 Node 18+ 拦截 global fetch;
老 Node 用node-fetch不被拦截,要装@mswjs/interceptors。 -
测试间 handler 泄漏:忘
afterEach(() => server.resetHandlers()),
上个测试 override 的 handler 影响下个测试。 -
WebSocket 不支持:MSW 主要拦截 HTTP;WebSocket 用 mock-socket
或专门的 ws mock 库。
登录后参与评论。