大多数前端开发者平时联调时,本地接口响应速度都在几十毫秒以内,页面看起来一切正常。但真实用户的网络环境千差万别,一个接口卡上三秒,按钮没有loading、列表没有骨架屏、用户连点三次提交的问题就全冒出来了。要在开发阶段暴露这些问题,最直接的办法就是把API故意变慢。Mock Service Worker(简称MSW)正好提供了这个能力:它在浏览器内部用Service Worker拦截网络请求,不需要改动任何业务代码,就能给指定接口加上几秒的延迟。

一、MSW为什么适合做慢速API模拟
模拟慢接口的手段其实不少:可以在DevTools的Network面板里限速,可以后端加个sleep,也可以自己写个mock服务器。但这些方案各有短板。DevTools限速是全局性的,会把所有请求都拖慢,连静态资源一起遭殃,而且无法针对单个接口设置精确的延迟时间。后端改代码则依赖后端配合,节奏不受前端控制。自建mock服务器又多了一套服务需要维护,接口路径、端口、代理配置都要跟着改。
MSW的思路完全不同。它利用浏览器原生的Service Worker API,在真实的网络层注册一个拦截脚本。当页面发起fetch或XMLHttpRequest请求时,这个脚本会先捕获请求,判断是否有对应的handler:有就返回mock响应,没有就放行到真实服务器。整个过程发生在浏览器内部,请求地址、URL、请求头都与真实环境一致,前端代码完全无感知。
更重要的是,MSW对延迟的控制非常精细。它提供了delay()工具函数,既可以写死一个固定毫秒数,也可以用delay(2000)这种形式精确模拟,甚至支持'real'模式根据实际网络特征随机产生延迟。你可以只让订单提交接口慢三秒,其他接口照常秒回,这种按接口粒度的控制是限速工具做不到的。
二、在React项目中安装并启用MSW
先安装依赖。开发环境只需要msw本体:
npm install msw --save-dev # 或者 yarn add msw --dev
安装完成后,执行MSW提供的CLI命令生成Service Worker脚本文件,这个文件需要放在项目的public目录下,浏览器注册时才能找到它:
npx msw init public/ --save
这条命令会在public目录下生成一个mockServiceWorker.js文件,它就是运行在浏览器里的拦截脚本,通常不需要修改也不需要提交逻辑变更。接下来创建handler文件,定义要拦截的请求和对应的mock响应:
// src/mocks/handlers.js
import { http, HttpResponse, delay } from 'msw'
export const handlers = [
// 拦截获取用户列表的GET请求,延迟3秒返回
http.get('/api/users', async () => {
await delay(3000)
return HttpResponse.json([
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
])
}),
// 拦截提交订单的POST请求,延迟5秒返回
http.post('/api/orders', async () => {
await delay(5000)
return HttpResponse.json(
{ orderId: 'A1024', status: 'created' },
{ status: 201 }
)
})
]然后创建worker并导出,注意onUnhandledRequest建议配置成'bypass',这样没有匹配handler的请求会直接放行到真实后端,避免干扰正常联调:
// src/mocks/browser.js
import { setupWorker } from 'msw'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)最后在应用入口文件里,仅在开发环境下启动worker:
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
async function enableMocking() {
if (process.env.NODE_ENV !== 'development') {
return
}
const { worker } = await import('./mocks/browser')
await worker.start({ onUnhandledRequest: 'bypass' })
}
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
})这里有个细节值得注意:worker必须在应用发起任何请求之前启动,所以要把渲染逻辑放到enableMocking()的then回调里,或者至少保证启动顺序在前。生产构建中通过环境变量判断直接跳过,mock代码不会影响线上包体积,因为用了动态import,打包工具会自动分割。
三、用delay函数精细化控制延迟场景
MSW的delay()函数比想象中灵活。除了传固定毫秒数,还支持'infinite'让请求永远挂起,用来测试超时逻辑和用户取消操作特别有用。比如要验证前端的请求超时兜底是否生效,可以这样写:
import { http, HttpResponse, delay } from 'msw'
export const timeoutHandlers = [
// 模拟超时:请求永不返回
http.get('/api/config', async () => {
await delay('infinite')
}),
// 模拟真实网络的随机抖动,延迟在800到3000毫秒之间
http.get('/api/dashboard', async () => {
await delay(800 + Math.random() * 2200)
return HttpResponse.json({ revenue: 98210, orders: 341 })
})
]除了延迟,错误场景的模拟也很重要。delay经常和错误响应组合使用,比如模拟弱网下先超时再报错的情况:
// 慢接口 + 服务端错误
http.get('/api/report', async () => {
await delay(4000)
return new HttpResponse(null, { status: 500 })
})
// 模拟网络中断
http.get('/api/payment', async () => {
await delay(2000)
return HttpResponse.error()
})另一个实用技巧是配合环境变量控制延迟开关。团队协作时不可能让所有人都忍受慢接口,可以在启动命令里加一个参数:
// 在handlers.js中读取开关
const slowMode = process.env.REACT_APP_SLOW === 'true'
http.get('/api/users', async () => {
if (slowMode) {
await delay(3000)
}
return HttpResponse.json([...])
})package.json中增加一条脚本"start:slow": "cross-env REACT_APP_SLOW=true react-scripts start",需要压测弱网体验时执行npm run start:slow,日常开发继续用普通的npm start,两套环境互不干扰。
四、在组件测试中复用同一套慢速mock
MSW的一大优势是同一份handler既能跑在浏览器里,也能跑在Vitest或Jest的Node环境中,只需要用setupServer替换setupWorker。这对测试loading状态、防重复提交这类交互逻辑非常有价值:
// src/mocks/server.js
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)在测试配置文件中全局启用,并在每个用例结束后重置handler,避免用例间相互污染:
// src/setupTests.js
import { server } from './mocks/server'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())这样写一个测试用例验证按钮在慢接口期间的禁用状态:
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import OrderForm from './OrderForm'
test('提交订单期间按钮应处于禁用状态', async () => {
render(<OrderForm />)
const button = screen.getByRole('button', { name: '提交订单' })
await userEvent.click(button)
expect(button).toBeDisabled()
// 等待延迟5秒的响应返回后按钮恢复
expect(await screen.findByText('下单成功')).toBeInTheDocument()
expect(button).toBeEnabled()
})总结一下,MSW把慢速API模拟变成了前端可以独立掌控的事:安装一次、写好handler,开发环境、测试环境共享同一套延迟定义,无需后端配合,不污染真实请求。建议把delay写进常用的mock模板里,让弱网场景成为日常自测的常规动作,而不是上线后才补救的意外情况。
ReactMock Service WorkerMSW修改时间:2026-09-03 04:10:47