基于 MSW (Mock Service Worker) 的前端 Mock 方案,遵循 feature-first 架构原则
一、方案概述
核心设计
markdown
1. Mock 跟着 feature 走(就近原则)
2. Handler 汇总控制界限(注释即切换)
3. 环境变量总开关(一键切换)
架构图
bash
┌─────────────────────────────────────────────────────────┐
│ 前端应用 │
│ ┌───────────────────────────────────────────────────┐ │
│ │ API 调用层 │ │
│ │ └── 统一使用 fetch/axios,不感知 mock │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Service Worker (MSW) │ │
│ │ ├── 匹配到 handler → 返回 mock 数据 │ │
│ │ └── 未匹配 → 放行到真实服务器 │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
二、后端响应格式规范
所有 Mock 数据必须遵循此规范,确保与真实 API 格式一致
成功响应(无数据)
json
{
"code": "0",
"msg": "成功",
"data": null
}
成功响应(带数据)
json
{
"code": "0",
"msg": "成功",
"data": {}
}
失败响应
json
{
"code": "-1",
"msg": "错误信息描述",
"data": null
}
分页响应
json
{
"code": "0",
"msg": "成功",
"data": {
"list": [],
"total": 100
}
}
响应工具函数
typescript
// src/mock/utils/response.ts
/**
* 成功响应
* @param data - 响应数据
* @param msg - 响应消息
*/
export function success<T>(data: T, msg = '成功') {
return {
code: '0',
msg,
data,
}
}
/**
* 失败响应
* @param msg - 错误信息
* @param code - 错误码,默认 -1
*/
export function error(msg: string, code = '-1') {
return {
code,
msg,
data: null,
}
}
/**
* 分页响应
* @param list - 当前页数据列表
* @param total - 总记录数
*/
export function page<T>(list: T[], total: number) {
return {
code: '0',
msg: '成功',
data: {
list,
total,
},
}
}
三、目录结构
ruby
src/
├── mock/ # 全局 Mock 配置
│ ├── browser.ts # MSW 实例
│ ├── setup.ts # 启动入口 + Handler 汇总
│ └── utils/
│ └── response.ts # 响应格式化工具
│
├── features/ # Feature 模块
│ ├── module-a/
│ │ ├── mock/
│ │ │ ├── data.ts # Mock 数据
│ │ │ └── handler.ts # Mock 处理器
│ │ ├── api/
│ │ │ └── index.ts # 真实 API
│ │ ├── components/
│ │ ├── composables/
│ │ ├── types/
│ │ └── index.vue
│ │
│ ├── module-b/
│ │ ├── mock/
│ │ │ ├── data.ts
│ │ │ └── handler.ts
│ │ └── ...
│ │
│ └── module-c/
│ ├── mock/
│ │ ├── data.ts
│ │ └── handler.ts
│ └── ...
│
├── main.ts # 应用入口
└── .env.development # 环境变量
四、环境变量
env
# .env.development
VITE_MOCK=true
VITE_API_BASE_URL=http://192.168.1.100:8080
# .env.production
VITE_MOCK=false
VITE_API_BASE_URL=https://api.example.com
五、核心文件实现
1. MSW 实例
typescript
// src/mock/browser.ts
import { setupWorker } from 'msw/browser'
/**
* MSW Service Worker 实例
*/
export const worker = setupWorker()
2. 启动入口 + Handler 汇总
typescript
// src/mock/setup.ts
import { worker } from './browser'
// 导入各 feature 的 handlers
import { moduleAHandlers } from '@/features/module-a/mock/handler'
import { moduleBHandlers } from '@/features/module-b/mock/handler'
import { moduleCHandlers } from '@/features/module-c/mock/handler'
/**
* 所有 Handlers 汇总
* 注释掉某个 feature 即可让该模块走真实 API
*/
const allHandlers = [
...moduleAHandlers, // 模块 A
...moduleBHandlers, // 模块 B
// ...moduleCHandlers, // 注释掉,走真实 API
]
/**
* 启动 Mock 服务
*/
export async function setupMock() {
await worker.start({
onUnhandledRequest: 'bypass', // 未匹配的请求走真实 API
})
worker.use(...allHandlers)
console.log('[MSW] 已注册', allHandlers.length, '个接口')
}
3. 应用入口
typescript
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './styles/global.css'
async function bootstrap() {
// Mock 开关:VITE_MOCK=true 时启用
if (import.meta.env.VITE_MOCK === 'true') {
const { setupMock } = await import('./mock/setup')
await setupMock()
}
const app = createApp(App)
app.use(router)
app.mount('#app')
}
bootstrap()
4. Feature Mock 层示例
模块 A - 数据层
typescript
// src/features/module-a/mock/data.ts
import type { Item } from '../types'
/**
* 列表数据
*/
export const items: Item[] = [
{ id: '1', name: '项目一', status: 'active' },
{ id: '2', name: '项目二', status: 'inactive' },
{ id: '3', name: '项目三', status: 'active' },
]
/**
* 详情数据
*/
export const detail: Record<string, Item> = {
'1': { id: '1', name: '项目一', status: 'active', description: '这是项目一的详细描述' },
'2': { id: '2', name: '项目二', status: 'inactive', description: '这是项目二的详细描述' },
'3': { id: '3', name: '项目三', status: 'active', description: '这是项目三的详细描述' },
}
模块 A - Handler 层
typescript
// src/features/module-a/mock/handler.ts
import { http, HttpResponse } from 'msw'
import { items, detail } from './data'
import { success, error, page } from '@/mock/utils/response'
const API = import.meta.env.VITE_API_BASE_URL || ''
/**
* 模拟网络延迟
*/
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
/**
* 模块 A Mock Handlers
*/
export const moduleAHandlers = [
// 获取列表(分页)
http.get(`${API}/api/module-a/list`, async ({ request }) => {
await delay(200)
const url = new URL(request.url)
const pageSize = Number(url.searchParams.get('pageSize')) || 10
const pageNum = Number(url.searchParams.get('pageNum')) || 1
// 模拟分页
const start = (pageNum - 1) * pageSize
const end = start + pageSize
const list = items.slice(start, end)
return HttpResponse.json(page(list, items.length))
}),
// 获取详情
http.get(`${API}/api/module-a/:id`, async ({ params }) => {
await delay(200)
const item = detail[params.id as string]
if (!item) {
return HttpResponse.json(error('数据不存在'))
}
return HttpResponse.json(success(item))
}),
// 新增
http.post(`${API}/api/module-a`, async ({ request }) => {
await delay(200)
const body = await request.json() as { name: string }
if (!body?.name) {
return HttpResponse.json(error('名称不能为空'))
}
const newItem = {
id: Date.now().toString(),
name: body.name,
status: 'active',
}
return HttpResponse.json(success(newItem, '添加成功'))
}),
// 更新
http.put(`${API}/api/module-a/:id`, async ({ params, request }) => {
await delay(200)
const body = await request.json() as { name?: string; status?: string }
const item = detail[params.id as string]
if (!item) {
return HttpResponse.json(error('数据不存在'))
}
return HttpResponse.json(success({ ...item, ...body }, '更新成功'))
}),
// 删除
http.delete(`${API}/api/module-a/:id`, async ({ params }) => {
await delay(200)
return HttpResponse.json(success(null, '删除成功'))
}),
]
模块 B - 数据层
typescript
// src/features/module-b/mock/data.ts
/**
* 用户列表数据
*/
export const users = [
{
id: '1',
username: 'admin',
name: '管理员',
role: 'admin',
createdAt: '2024-01-01',
},
{
id: '2',
username: 'user1',
name: '用户一',
role: 'user',
createdAt: '2024-01-10',
},
]
模块 B - Handler 层
typescript
// src/features/module-b/mock/handler.ts
import { http, HttpResponse } from 'msw'
import { users } from './data'
import { success, error } from '@/mock/utils/response'
const API = import.meta.env.VITE_API_BASE_URL || ''
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
/**
* 模块 B Mock Handlers
*/
export const moduleBHandlers = [
// 获取用户列表
http.get(`${API}/api/module-b/users`, async () => {
await delay(200)
return HttpResponse.json(success(users))
}),
// 添加用户
http.post(`${API}/api/module-b/users`, async ({ request }) => {
await delay(200)
const body = await request.json() as { username: string; name: string; role: string }
if (!body?.username || !body?.name) {
return HttpResponse.json(error('参数不完整'))
}
const newUser = {
id: Date.now().toString(),
...body,
createdAt: new Date().toISOString().split('T')[0],
}
return HttpResponse.json(success(newUser, '添加成功'))
}),
// 删除用户
http.delete(`${API}/api/module-b/users/:id`, async () => {
await delay(100)
return HttpResponse.json(success(null, '删除成功'))
}),
]
六、使用方式
1. 安装 MSW
bash
npm install msw --save-dev
npx msw init public/ --save
2. 启动开发服务器
bash
# Mock 模式(VITE_MOCK=true)
npm run dev
# 真实 API 模式(修改 .env.development: VITE_MOCK=false)
npm run dev
3. 部分 Mock
在 src/mock/setup.ts 中注释掉对应的 handler:
typescript
const allHandlers = [
...moduleAHandlers, // 模块 A 用 mock
// ...moduleBHandlers, // 注释掉,模块 B 走真实 API
...moduleCHandlers,
]
4. 修改 Mock 数据
直接修改 features/xxx/mock/data.ts 文件:
typescript
// src/features/module-a/mock/data.ts
export const items: Item[] = [
{ id: '1', name: '测试数据', status: 'active' },
// 添加更多测试数据...
]
七、关键概念说明
onUnhandledRequest: 'bypass'
MSW 工作原理:
markdown
前端发起请求
│
▼
┌─────────────────────────────────┐
│ Service Worker 拦截层 │
│ │
│ 请求 URL 匹配到 handler? │
│ │ │
│ ├─ 是 → 执行 handler,返回 mock 数据
│ │ │
│ └─ 否 → 放行到真实服务器 │
│ │
└─────────────────────────────────┘
举例:
| 请求 URL | 是否匹配 handler | 结果 |
|---|---|---|
GET /api/module-a/list |
✅ 匹配 | 返回 mock 数据 |
GET /api/module-b/users |
❌ 未匹配(已注释) | 走真实 API |
八、方案优势
| 需求 | 实现方式 |
|---|---|
| ENV 简单开关 | VITE_MOCK=true/false |
| 目录清晰 | features/xxx/mock/ 就近管理 |
| 数据结构干净 | data.ts 纯数据,handler.ts 纯逻辑 |
| 真实网络环境 | MSW 在网络层拦截 |
| 切换真实服务 | 注释 handler 或 VITE_MOCK=false |
| 部分接口 Mock | 注释 setup.ts 中的 handler |
| 不侵入生产 | 条件导入 + tree-shaking |
| 响应格式统一 | utils/response.ts 工具函数 |
九、文件清单
css
需要创建的文件:
├── src/mock/browser.ts
├── src/mock/setup.ts
├── src/mock/utils/response.ts
├── src/features/module-a/mock/data.ts
├── src/features/module-a/mock/handler.ts
├── src/features/module-b/mock/data.ts
├── src/features/module-b/mock/handler.ts
└── public/mockServiceWorker.js (npx msw init 生成)
需要修改的文件:
├── src/main.ts
└── .env.development