别再傻等后端接口了!手把手教你用MSW在前端独立Mock数据(附完整配置流程)
别再傻等后端接口了!手把手教你用MSW在前端独立Mock数据(附完整配置流程)
每次新项目启动时,最让人头疼的就是前端开发被后端接口进度卡住。明明页面逻辑都写好了,却因为接口没准备好而无法继续开发。这种等待不仅浪费时间,还会打断开发节奏。MSW(Mock Service Worker)的出现彻底改变了这种困境——它让我们能在浏览器层面拦截所有API请求,返回预先定义好的模拟数据,实现真正的前后端并行开发。
1. 为什么选择MSW而不是其他Mock方案
在接触MSW之前,我们团队尝试过各种Mock方案:从简单的JSON文件到搭建本地Mock服务器,再到使用Postman的Mock功能。这些方案要么配置复杂,要么无法模拟真实网络行为,最终都难以长期维护。MSW的独特之处在于它直接在浏览器网络层工作,这意味着:
- 零侵入性:不需要修改任何业务代码,所有拦截对应用完全透明
- 真实网络模拟:可以精确控制响应延迟、错误状态码等网络特性
- 开发/测试通用:同一套Mock逻辑可以无缝用于单元测试和E2E测试
- 现代工具链支持:完美适配Vite、Webpack、Next.js等主流构建工具
与其他方案对比:
| 特性 | MSW | JSON Server | Nock | Cypress拦截 |
|---|---|---|---|---|
| 浏览器环境支持 | ✅ | ❌ | ❌ | ✅ |
| Node环境支持 | ✅ | ✅ | ✅ | ❌ |
| 无需修改生产代码 | ✅ | ❌ | ❌ | ❌ |
| 模拟网络延迟 | ✅ | ❌ | 部分 | ✅ |
| 支持GraphQL | ✅ | ❌ | ❌ | ✅ |
2. 十分钟快速上手MSW基础配置
让我们从一个全新的Vite+React项目开始,演示如何快速集成MSW。假设项目目录结构如下:
my-app/ ├── src/ │ ├── mocks/ │ │ ├── handlers.js │ │ └── browser.js │ ├── main.jsx2.1 安装依赖
npm install msw --save-dev # 或者 yarn add msw --dev2.2 创建请求处理器
在src/mocks/handlers.js中定义你的第一个Mock接口:
import { http, HttpResponse } from 'msw' export const handlers = [ // 拦截GET /api/user 请求 http.get('/api/user', () => { return HttpResponse.json({ id: 'f79e82e8-c34a-4dc7-a49e-9fadc0979fda', name: 'John Mock', email: 'john.mock@example.com' }) }), // 拦截POST /api/login 请求 http.post('/api/login', async ({ request }) => { const { email, password } = await request.json() return HttpResponse.json({ token: btoa(`${email}:${password}`), expiresIn: 3600 }) }) ]2.3 配置Service Worker
在src/mocks/browser.js中初始化worker:
import { setupWorker } from 'msw' import { handlers } from './handlers' export const worker = setupWorker(...handlers)2.4 在开发环境启用Mock
修改src/main.jsx,只在开发环境启用Mock:
import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' async function prepare() { if (import.meta.env.DEV) { const { worker } = await import('./mocks/browser') return worker.start() } return Promise.resolve() } prepare().then(() => { ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <App /> </React.StrictMode> ) })现在启动开发服务器,所有对/api/user和/api/login的请求都会被拦截并返回Mock数据。
3. 高级Mock技巧:打造真实开发体验
基础Mock只能满足简单场景,实际开发中我们需要更精细的控制。以下是几个提升Mock真实度的技巧:
3.1 模拟网络延迟
http.get('/api/products', async () => { // 随机1-3秒延迟 await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000) ) return HttpResponse.json([ { id: 1, name: 'Product A', price: 99 }, { id: 2, name: 'Product B', price: 199 } ]) })3.2 动态响应与状态管理
const shoppingCart = new Map() http.post('/api/cart', async ({ request }) => { const { productId, quantity } = await request.json() if (!shoppingCart.has(productId)) { shoppingCart.set(productId, 0) } const newQuantity = shoppingCart.get(productId) + quantity shoppingCart.set(productId, newQuantity) return HttpResponse.json({ success: true, cart: Array.from(shoppingCart.entries()) }) })3.3 错误场景模拟
http.get('/api/orders/:orderId', ({ params }) => { // 50%概率返回404 if (Math.random() > 0.5) { return new HttpResponse(null, { status: 404, statusText: 'Order not found' }) } return HttpResponse.json({ id: params.orderId, status: 'shipped', items: [ { id: 1, name: 'Product X', quantity: 2 } ] }) })4. 与前端框架深度集成
MSW的强大之处在于它能与各种前端框架无缝配合。以下是几个常见场景的集成方案:
4.1 在React中处理加载状态
import { useQuery } from '@tanstack/react-query' function UserProfile() { const { data, isLoading, error } = useQuery({ queryKey: ['user'], queryFn: () => fetch('/api/user').then(res => res.json()) }) if (isLoading) return <div>Loading...</div> if (error) return <div>Error: {error.message}</div> return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> </div> ) }4.2 在Vue中结合Pinia使用
// stores/user.js import { defineStore } from 'pinia' import { ref } from 'vue' import { useFetch } from '@vueuse/core' export const useUserStore = defineStore('user', () => { const user = ref(null) const error = ref(null) async function fetchUser() { const { data, error: fetchError } = await useFetch('/api/user') if (fetchError.value) { error.value = fetchError.value } else { user.value = data.value } } return { user, error, fetchUser } })4.3 在Next.js中的特殊配置
Next.js的App Router需要额外配置:
// app/layout.js import { Inter } from 'next/font/google' import './globals.css' const inter = Inter({ subsets: ['latin'] }) export default function RootLayout({ children }) { // 在开发环境启动MSW if (process.env.NODE_ENV === 'development') { require('../mocks') } return ( <html lang="en"> <body className={inter.className}>{children}</body> </html> ) }5. 测试环境的最佳实践
MSW在测试中的价值甚至比开发环境更大。我们可以用同一套Mock逻辑保证测试一致性:
5.1 单元测试配置示例
// src/setupTests.js import { server } from './mocks/server' // 在所有测试之前启动MSW beforeAll(() => server.listen()) // 重置每个测试之间的handler afterEach(() => server.resetHandlers()) // 所有测试完成后关闭MSW afterAll(() => server.close())5.2 测试不同网络状态
import { server, HttpResponse } from '../mocks/server' import { render, screen, waitFor } from '@testing-library/react' import UserProfile from './UserProfile' test('显示加载状态', async () => { server.use( http.get('/api/user', async () => { await new Promise(resolve => setTimeout(resolve, 1000)) return HttpResponse.json({ name: 'Test User' }) }) ) render(<UserProfile />) expect(screen.getByText('Loading...')).toBeInTheDocument() await waitFor(() => { expect(screen.getByText('Test User')).toBeInTheDocument() }) })5.3 E2E测试中的使用
// cypress/support/e2e.js import { setupWorker } from 'msw' import { handlers } from '../../src/mocks/handlers' const worker = setupWorker(...handlers) before(() => { worker.start({ onUnhandledRequest: 'bypass' }) }) after(() => { worker.stop() })6. 常见问题与性能优化
在实际项目中使用MSW半年后,我们总结出以下经验:
6.1 处理CORS问题
当Mock接口与实际接口域名不同时,可能会遇到CORS错误。解决方案:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://real-api.com', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })6.2 大型项目的Mock组织
建议按功能模块拆分handlers:
src/ └── mocks/ ├── handlers/ │ ├── auth.handlers.js │ ├── products.handlers.js │ └── orders.handlers.js ├── db.js # 共享的模拟数据库 ├── handlers.js # 聚合所有handlers └── browser.js6.3 性能优化技巧
使用
once()方法处理只需要Mock一次的请求:http.get('/api/config', () => { return HttpResponse.json({ theme: 'dark' }) }, { once: true })对于大量数据,使用分页Mock:
http.get('/api/products', ({ request }) => { const url = new URL(request.url) const page = parseInt(url.searchParams.get('page') || '1') const perPage = 20 return HttpResponse.json({ data: Array.from({ length: perPage }, (_, i) => ({ id: (page - 1) * perPage + i, name: `Product ${(page - 1) * perPage + i}`, price: Math.floor(Math.random() * 1000) })), page, total: 1000 }) })
7. 从Mock平滑过渡到真实API
当后端接口就绪后,我们需要安全地移除Mock而不影响现有功能:
渐进式替换:逐个handler替换为真实接口,使用环境变量控制:
http.get('/api/user', () => { if (process.env.USE_REAL_API) { return passthrough() } return HttpResponse.json(mockUser) })差异检测:编写测试比较Mock和真实API的响应结构:
test('Mock与真实API结构一致', async () => { const mockRes = await fetch('/api/user') const realRes = await fetch('https://real-api.com/user') expect(realRes.status).toBe(mockRes.status) expect(Object.keys(realRes.json())).toEqual( expect.arrayContaining(Object.keys(mockRes.json())) ) })监控回归:在移除Mock后,使用Sentry等工具监控相关接口的报错情况
