本文档总结了一套经过实践验证的 Mock 服务设计方案,适用于前端开发阶段的接口模拟,具有模块化、可维护、真实业务逻辑等优势。
1 设计初衷
1.1 解决的问题
| 问题 | 传统 Mock 方案 | 本方案 |
|---|---|---|
| 前后端并行开发 | 依赖后端接口完成 | 前端可独立开发和调试 |
| 接口变更成本 | 多处修改 Mock 数据 | 集中维护,一处修改 |
| 业务逻辑验证 | 只能测试静态数据 | 可验证状态流转、权限等逻辑 |
| 联调效率 | 频繁等待后端环境 | 本地完整运行,随时测试 |
| 数据真实性 | 随机数据,缺乏关联 | 生成符合业务规则的关联数据 |
1.2 核心目标
- 独立运行:Mock 服务作为独立后端,不侵入业务代码
- 真实逻辑:实现必要的业务规则(状态流转、权限校验、数据关联)
- 易于维护:模块化组织,与业务接口一一对应
- 快速切换:通过环境变量或命令一键切换 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.ts、store.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 开发流程
- 定义类型 :先写
types.ts,明确接口契约 - 实现存储 :编写
store.ts,定义数据结构和 CRUD - 编写路由 :编写
routes.ts,实现接口 - 添加逻辑 :如需业务逻辑,编写
services.ts - 测试验证:启动 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 总结
本方案的核心优势:
- 独立性:Mock 服务独立运行,不侵入业务代码
- 真实性:模拟业务逻辑,可验证状态流转
- 可维护性:模块化组织,易于扩展和修改
- 类型安全:TypeScript 类型定义,编译时检查
- 数据关联:维护数据间关系,查询结果真实
适用团队规模:3-20 人前端团队,项目周期 3 个月以上。