MSW Mock Feature-First 方案

基于 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
相关推荐
sin6031 小时前
Talk is cheap 之后:AI Agent 时代,程序员真正要交付什么?
前端
Ticnix1 小时前
手把手教你在 Next.js 中接入本地大模型,实现 ChatGPT 同款流式对话
前端·next.js
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_18:(HTML 表格进阶特性与无障碍——从标题结构到屏幕阅读器适配)
前端·笔记·ui·html·音视频
沐 修1 小时前
前端调试 - 获取下拉框元素 F12 延时断点操作记录 - 秒杀其他所谓的F8和手速快操作
前端
恋猫de小郭1 小时前
AI 时代开源协议将消亡,malus 讽刺性展示了这一点
前端·人工智能·ai编程
Mike_jia1 小时前
MeterSphere:开源持续测试平台,让测试管理变得如此简单
前端
Csvn2 小时前
Vue 3 响应式原理深度解析
前端
恋猫de小郭2 小时前
Flutter 3.44 发布前夕,官方宣布 SwiftPM 将完全取代 CocoaPods
android·前端·flutter
Json____2 小时前
vue3-商城管理系统-前端静态网站
前端·vue3·ts·商城纯静态