Mock 服务设计指南

本文档总结了一套经过实践验证的 Mock 服务设计方案,适用于前端开发阶段的接口模拟,具有模块化、可维护、真实业务逻辑等优势。

1 设计初衷

1.1 解决的问题

问题 传统 Mock 方案 本方案
前后端并行开发 依赖后端接口完成 前端可独立开发和调试
接口变更成本 多处修改 Mock 数据 集中维护,一处修改
业务逻辑验证 只能测试静态数据 可验证状态流转、权限等逻辑
联调效率 频繁等待后端环境 本地完整运行,随时测试
数据真实性 随机数据,缺乏关联 生成符合业务规则的关联数据

1.2 核心目标

  1. 独立运行:Mock 服务作为独立后端,不侵入业务代码
  2. 真实逻辑:实现必要的业务规则(状态流转、权限校验、数据关联)
  3. 易于维护:模块化组织,与业务接口一一对应
  4. 快速切换:通过环境变量或命令一键切换 Mock/真实模式

2 架构设计

2.1 整体架构

bash 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      前端应用                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  业务代码(无 Mock 逻辑)                            │   │
│  │  - API 调用                                         │   │
│  │  - 状态管理                                         │   │
│  │  - UI 渲染                                          │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ HTTP 请求
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    开发服务器(Vite)                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Proxy 配置                                         │   │
│  │  - /api/* → Mock 服务 或 真实后端                    │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
┌─────────────────────────┐     ┌─────────────────────────┐
│      Mock 服务          │     │      真实后端            │
│  - 独立端口(如 8082)  │     │  - 测试/生产环境        │
│  - 内存数据存储         │     │                         │
│  - 业务逻辑模拟         │     │                         │
└─────────────────────────┘     └─────────────────────────┘

2.2 目录结构

bash 复制代码
project-root/
├── src/                          # 业务代码(纯净,无 Mock 逻辑)
│   ├── api/                      # API 调用层
│   ├── stores/                   # 状态管理
│   ├── pages/                    # 页面组件
│   └── components/               # 公共组件
│
├── mock/                         # Mock 服务(独立目录)
│   ├── server.ts                 # 服务入口
│   ├── shared/                   # 共享工具
│   │   ├── response.ts           # 统一响应格式
│   │   ├── types.ts              # 共享类型定义
│   │   └── utils.ts              # 工具函数
│   │
│   ├── module-a/                 # 业务模块 A
│   │   ├── index.ts              # 模块导出
│   │   ├── routes.ts             # 路由定义
│   │   ├── store.ts              # 数据存储
│   │   ├── services.ts           # 业务逻辑
│   │   └── types.ts              # 类型定义
│   │
│   ├── module-b/                 # 业务模块 B
│   │   └── ...
│   │
│   └── user/                     # 用户模块(公共)
│       ├── routes.ts
│       ├── users.ts              # 预设用户数据
│       └── types.ts
│
├── package.json                  # npm scripts
├── vite.config.ts                # Vite 配置(proxy)
└── .env.mock                     # Mock 环境变量

3 核心设计原则

3.1 模块化组织

原则:每个业务模块独立目录,与后端接口模块一一对应。

typescript 复制代码
// mock/incident/index.ts
export * from './routes';
export * from './store';
export * from './types';
export { incidentRoutes as default } from './routes';

优势

  • 职责清晰:每个模块只负责自己的业务
  • 易于查找:接口路径与目录结构对应
  • 独立演进:模块间低耦合,可独立修改

3.2 统一响应格式

原则:所有接口使用统一的响应格式,便于前端统一处理。

typescript 复制代码
// mock/shared/response.ts
export function ok<T>(data: T, extra: Record<string, unknown> = {}) {
  return {
    status: 0,
    msg: 'ok',
    data,
    ...extra,
  };
}

export function fail(message: string, status = 1) {
  return {
    status,
    msg: message,
    data: null,
  };
}

响应格式

json 复制代码
{
  "status": 0,        // 0=成功,非0=失败
  "msg": "ok",        // 提示信息
  "data": { ... },    // 业务数据
  "count": 100,       // 可选:分页总数
  "offset": 0,        // 可选:偏移量
  "size": 20          // 可选:每页大小
}

3.3 内存数据存储

原则:使用内存对象存储 Mock 数据,支持运行时增删改查。

typescript 复制代码
// mock/incident/store.ts
export const incidentStore = {
  incidentSeq: 1,
  incidents: createInitialIncidents(),  // 初始化时生成
};

// 初始化数据
function createInitialIncidents(): MockIncidentPayload[] {
  return Array.from({ length: 100 }, (_, index) => createIncident(index + 1));
}

// 数据操作函数
export function appendIncident(incident: MockIncidentPayload) {
  incidentStore.incidents.unshift(incident);
}

export function findIncidentById(id: number) {
  return incidentStore.incidents.find(item => item.id === id);
}

优势

  • 数据可变:支持真实的增删改查操作
  • 状态一致:所有接口共享同一份数据
  • 生命周期:服务重启时重置,保证干净状态

3.4 业务逻辑模拟

原则:在 Mock 服务中实现关键业务逻辑,而非只返回静态数据。

typescript 复制代码
// 状态流转逻辑
export function editIncidentStatus(incident: MockIncidentPayload, newStatus: number) {
  const validTransitions: Record<number, number[]> = {
    1: [2, 3],      // 待确认 → 处理中、已驳回
    2: [4, 5],      // 处理中 → 已解决、已关闭
    4: [6],          // 已解决 → 已关闭
    5: [2],          // 已关闭 → 处理中(重新打开)
  };

  const allowed = validTransitions[incident.status] || [];
  if (!allowed.includes(newStatus)) {
    return `不允许从状态 ${incident.status} 转换到 ${newStatus}`;
  }

  incident.status = newStatus;
  incident.updated_at = Math.floor(Date.now() / 1000);
  return null;  // 成功
}

模拟的业务逻辑类型

  • 状态流转:数据状态变更规则
  • 权限校验:操作权限、数据权限
  • 数据校验:必填字段、格式校验
  • 关联操作:级联更新、触发事件

3.5 类型安全

原则:完整的 TypeScript 类型定义,与后端接口文档对齐。

typescript 复制代码
// mock/incident/types.ts
export type MockIncidentPayload = {
  id: number;
  incident_id: string;
  event_title: string;
  status: number;
  created_at: number;
  updated_at: number;
  // ... 其他字段
};

优势

  • 编译时检查:字段名、类型错误在开发时发现
  • 智能提示:IDE 自动补全,提高开发效率
  • 文档即代码:类型定义即接口文档

4 数据生成策略

4.1 初始数据生成

原则:使用确定性算法生成初始数据,保证可重现。

typescript 复制代码
function createIncident(sequence: number): MockIncidentPayload {
  // 使用 sequence 作为种子,保证每次生成相同数据
  const creator = pickOne(mockUsers, sequence - 1);
  const status = pickOne([1, 1, 1, 2, 3, 4, 5, 6, 7, 7, 8], sequence);
  const eventCategory = pickOne(['S0', 'S1', 'S2', 'S3', 'S4', 'S5'], sequence);

  return {
    id: sequence,
    incident_id: `INC-2026-${String(sequence).padStart(4, '0')}`,
    status,
    event_category: eventCategory,
    // ...
  };
}

4.2 边界场景覆盖

原则:初始数据中包含特殊场景,用于验证边界情况。

typescript 复制代码
function createInitialIncidents() {
  const incidents = Array.from({ length: 100 }, (_, index) => createIncident(index + 1));

  // 特殊场景 1:待补充状态
  incidents[0] = {
    ...incidents[0],
    system_list: [{ id: 'pending-system', name: '待补充系统' }],
  };

  // 特殊场景 2:空状态验证
  incidents[1] = {
    ...incidents[1],
    event_title: '暂无空状态样式',
    mock_no_operation_audit: true,
  };

  // 特殊场景 3:大数据量验证
  incidents[2] = {
    ...incidents[2],
    event_title: '1000 条操作',
    mock_operation_audit_count: 1000,
  };

  return incidents;
}

4.3 数据关联

原则:维护数据间的关联关系,保证查询结果真实。

typescript 复制代码
// 数据与评价关联
export function getEvaluationStatistics() {
  const allEvaluations = incidentStore.incidents.flatMap(incident =>
    incident.evaluations.map(evaluation => ({
      id: evaluation.id,
      incident_id: incident.incident_id,
      incident_name: incident.event_title,
      score: evaluation.score,
      // ...
    }))
  );

  return {
    score_counts: [1, 2, 3, 4, 5].map(score => ({
      score,
      count: allEvaluations.filter(e => e.score === score).length,
    })),
    total: allEvaluations.length,
    evaluations: allEvaluations,
  };
}

5 路由设计

5.1 路由定义

原则:使用声明式定义路由,与后端接口路径一致。

typescript 复制代码
// mock/incident/routes.ts
export const incidentRoutes: MockMethod[] = [
  {
    url: '/api/v1/incident/list',
    method: 'get',
    response(this: RespThisType, input: MockRequestInput) {
      const page = listIncidentPage(input);
      return ok(page.data, { count: page.count }, input, this.res);
    },
  },
  {
    url: '/api/v1/incident/add',
    method: 'post',
    response(this: RespThisType, input: MockRequestInput) {
      const incident = createMockIncident(input.body);
      appendIncident(incident);
      return ok({ id: incident.id }, {}, input, this.res);
    },
  },
  // ...
];

5.2 请求参数处理

原则:统一处理查询参数和请求体。

typescript 复制代码
// mock/shared/response.ts
export function getTextParam(query: Record<string, unknown>, key: string) {
  const value = query[key] ?? query[`${key}[]`];
  if (Array.isArray(value)) return String(value[0] || '').trim();
  return String(value || '').trim();
}

export function getNumberParam(query: Record<string, unknown>, key: string) {
  const text = getTextParam(query, key);
  if (!text) return undefined;
  const parsed = Number(text);
  return Number.isFinite(parsed) ? parsed : undefined;
}

5.3 错误处理

原则:返回明确的错误信息,便于前端展示。

typescript 复制代码
{
  url: '/api/v1/incident/edit',
  method: 'put',
  response(this: RespThisType, input: MockRequestInput) {
    const incident = findIncidentById(input.body.id);
    if (!incident) {
      return fail('数据不存在', 404, input, this.res);
    }

    if (incident.status === 4) {
      return fail('已关闭的数据不可编辑', 400, input, this.res);
    }

    patchIncident(incident, input.body);
    return ok({ id: incident.id }, {}, input, this.res);
  },
},

6 用户与权限模拟

6.1 预设用户

原则:预设多个用户角色,便于测试不同权限场景。

typescript 复制代码
// mock/user/users.ts
export const mockUsers: MockUserPayload[] = [
  {
    uid: 'u10001',
    name: '张三',
    personaKey: 'admin',
    resources: ['uatu.evaluation', 'uatu.edit', 'uatu.close'],
  },
  {
    uid: 'u10002',
    name: '李四',
    personaKey: 'operator',
    resources: ['uatu.edit'],
  },
  {
    uid: 'u10003',
    name: '王五',
    personaKey: 'viewer',
    resources: [],
  },
];

6.2 用户切换

原则:支持运行时切换用户身份,便于测试权限逻辑。

typescript 复制代码
// mock/user/routes.ts
{
  url: '/api/v2/user/self',
  method: 'get',
  response(this: RespThisType, input: MockRequestInput) {
    // 从 Cookie 或 Header 读取当前用户
    const user = getMockUserByCookie(input.headers);
    return ok({
      uid: user.uid,
      name: user.name,
      resources: user.resources,
    }, {}, input, this.res);
  },
},

7 与前端集成

7.1 Vite 配置

原则:通过 Proxy 将请求转发到 Mock 服务,业务代码无需感知。

typescript 复制代码
// vite.config.ts
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, __dirname, 'VITE_');
  const isMockMode = env.VITE_APP_MODE === 'mock';

  return {
    server: {
      proxy: isMockMode ? {
        '/api/': {
          target: 'http://localhost:8082',
          changeOrigin: true,
        },
      } : {
        '/api/': {
          target: env.VITE_API_SERVER_HOST,
          changeOrigin: true,
        },
      },
    },
  };
});

7.2 环境变量

原则:通过环境变量控制 Mock 模式,业务代码不依赖 Mock 逻辑。

bash 复制代码
# .env.mock
VITE_APP_MODE=mock
VITE_API_BASE_URL=/api

# .env.development
VITE_APP_MODE=proxy
VITE_API_SERVER_HOST=https://api-dev.example.com

# .env.production
VITE_APP_MODE=api
VITE_API_SERVER_HOST=https://api.example.com

7.3 npm scripts

json 复制代码
{
  "scripts": {
    "dev": "vite",
    "mock": "node mock/server.js",
    "dev:mock": "concurrently \"npm run mock\" \"vite --mode mock\""
  }
}

8 脱敏与安全

8.1 数据脱敏

原则:Mock 数据不包含真实业务数据,使用虚构数据。

typescript 复制代码
// ✅ 正确:使用虚构数据
const mockUsers = [
  { uid: 'u10001', name: '张三' },
  { uid: 'u10002', name: '李四' },
];

// ❌ 错误:使用真实数据
const realUsers = [
  { uid: 'EMP2024001', name: '真实姓名' },
];

8.2 接口路径通用化

原则:移除项目特定的路径前缀,使用通用路径。

typescript 复制代码
// 原始路径(项目特定)
url: '/xxx/api/v1/incident/list'

// 通用化路径
url: '/api/v1/incident/list'

8.3 配置外部化

原则:将可变配置提取到环境变量或配置文件。

typescript 复制代码
// 不硬编码,使用环境变量
const API_PREFIX = process.env.API_PREFIX || '/api';
const MOCK_PORT = process.env.MOCK_PORT || 8082;

9 最佳实践

9.1 命名规范

类型 规范 示例
目录名 kebab-case incident/major-event/
文件名 kebab-case routes.tsstore.ts
类型名 PascalCase + Payload MockIncidentPayload
函数名 camelCase createMockIncident()
常量名 UPPER_SNAKE_CASE MOCK_PORT

9.2 文件职责

文件 职责 内容
routes.ts 路由定义 URL、Method、Response
store.ts 数据存储 内存对象、CRUD 操作
services.ts 业务逻辑 状态流转、权限校验
types.ts 类型定义 接口类型、请求/响应类型
index.ts 模块导出 统一导出入口

9.3 开发流程

  1. 定义类型 :先写 types.ts,明确接口契约
  2. 实现存储 :编写 store.ts,定义数据结构和 CRUD
  3. 编写路由 :编写 routes.ts,实现接口
  4. 添加逻辑 :如需业务逻辑,编写 services.ts
  5. 测试验证:启动 Mock 服务,前端调用验证

9.4 常见陷阱

陷阱 正确做法
Mock 数据写死 使用算法生成,保证可重现
业务代码依赖 Mock 业务代码只调用接口,不判断环境
Mock 逻辑过于复杂 只模拟关键业务逻辑,不追求 100% 覆盖
忘记重置数据 服务重启时自动重置,或提供重置接口

10 扩展能力

10.1 延迟模拟

typescript 复制代码
// 模拟网络延迟
{
  url: '/api/v1/incident/list',
  method: 'get',
  response: async (input) => {
    await new Promise(resolve => setTimeout(resolve, 300));
    return ok(data);
  },
}

10.2 错误注入

typescript 复制代码
// 模拟随机失败
{
  url: '/api/v1/incident/add',
  method: 'post',
  response: (input) => {
    if (Math.random() < 0.1) {
      return fail('服务器内部错误', 500);
    }
    return ok(data);
  },
}

10.3 数据持久化

typescript 复制代码
// 可选:将数据持久化到文件
import fs from 'fs';

const DATA_FILE = './mock-data.json';

function loadData() {
  if (fs.existsSync(DATA_FILE)) {
    return JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8'));
  }
  return createInitialData();
}

function saveData(data: unknown) {
  fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
}

11 适用场景

11.1 适用

  • 前后端并行开发
  • 接口契约已确定
  • 需要验证业务逻辑
  • 演示和培训环境

11.2 不适用

  • 接口契约未确定(先定义接口)
  • 简单的 CRUD(直接用 json-server)
  • 性能测试(使用专业工具)

12 总结

本方案的核心优势:

  1. 独立性:Mock 服务独立运行,不侵入业务代码
  2. 真实性:模拟业务逻辑,可验证状态流转
  3. 可维护性:模块化组织,易于扩展和修改
  4. 类型安全:TypeScript 类型定义,编译时检查
  5. 数据关联:维护数据间关系,查询结果真实

适用团队规模:3-20 人前端团队,项目周期 3 个月以上。

相关推荐
gis开发之家7 小时前
《Vue3 从入门到大神50篇》Vue3 源码详解(二十):生命周期钩子源码解析 —— onMounted / onUpdated 如何实现?
前端·javascript·前端框架·vue3·vue3源码
word7 小时前
从零接入 MCP:把任意工具变成 AI 的能力(协议级实践)
人工智能·前端框架
C++ 老炮儿的技术栈1 天前
从 Qt Designer 属性编辑器的层级可以看到继承链
c语言·数据库·c++·qt·sqlite·visual studio
To_OC1 天前
绕开层层 props 搬运:我把 useContext 和自定义 Hook 跑通了
前端·react.js·前端框架
倾听醉梦语1 天前
React/Vite/Next.js 前端开发工具 SpotPatch:点击页面元素精准定位 JSX/TSX 源码
javascript·react·ai编程·vite·next.js·前端开发工具
小林ixn1 天前
从混乱到清晰:项目架构与自定义 Hook 的双重实践
react.js·架构·前端框架
meilindehuzi_a1 天前
WebGPU DeepSeek项目实战(二):用单例模式加载Tokenizer并转发下载进度
单例模式·typescript·react
Hhy_11071 天前
《C++深度解构01》C++入门基础
c语言·开发语言·c++·学习·visual studio
小林ixn1 天前
前端卡顿终结者:用 useRef 把 Web Worker 请进 React 项目
前端·react.js·前端框架