传统方式的痛点:
// 手写假数据的问题
const userList = [
{ id: 1, name: '张三', age: 25, email: 'test@example.com' }
// 数据量不足,不够真实
];问题分析:
Mock.js的价值:
| 工具 | 类型 | 数据生成 | 请求拦截 | 维护状态 | 推荐指数 |
|---|---|---|---|---|---|
| Mock.js | 数据+拦截 | ✓ | ✓ | 停止更新 | 传统项目推荐 |
| Faker.js | 数据生成 | ✓ | ✗ | 活跃维护 | 现代项目推荐 |
| MSW | 请求拦截 | ✗ | ✓ | 活跃维护 | 现代项目推荐 |
推荐策略:
# 安装
npm install mockjs --save-dev
# 引入
const Mock = require('mockjs'); // CommonJS
import Mock from 'mockjs'; // ES ModulesMock.mock({
'属性名|生成规则': 初始值
})const data = Mock.mock({
// 数值范围
'age|1-100': 1,
'price|1-100.2-4': 1,
// 数组生成
'users|3-5': [{
'id|+1': 1, // 自增
'name': '@cname'
}],
// 布尔值
'isActive|1-3': true, // 25%概率为true
// 字符串重复
'code|6': 'x' // "xxxxxx"
});const data = Mock.mock({
'name': '@cname', // 张三
'title': '@ctitle(5, 10)', // 5-10字标题
'sentence': '@csentence(20)', // 20字句子
'province': '@province', // 省
'city': '@city(true)', // 市
'address': '@county(true)', // 完整地址
'email': '@email', // 邮箱
'phone': /^1[385][1-9]\d{8}/, // 手机号正则
'date': '@datetime', // 日期时间
'image': '@image("200x200")' // 图片
});// 自定义占位符
Mock.Random.extend({
// 真实图片URL
realImage() {
const id = this.integer(1, 1000);
return `https://picsum.photos/id/${id}/200/150`;
},
// 中国手机号
phoneCN() {
const prefixes = ['138', '139', '150', '186', '187', '188'];
return this.pick(prefixes) + this.string('number', 8);
}
});
// 使用自定义占位符
const data = Mock.mock({
'avatar': '@realImage',
'phone': '@phoneCN'
});前端发起请求
↓
Mock.js拦截XHR
↓
匹配URL规则
↓
返回Mock数据
// Network面板不会看到真实HTTP请求!Mock.mock(url, method, template);参数说明:
url: String | RegExp - 匹配的URLmethod: String - HTTP方法(get/post/put/delete)template: Object | Function - 响应数据1. 对象模板:
Mock.mock('/api/user', 'get', {
code: 200,
data: {
'id|1-100': 1,
'name': '@cname'
}
});2. 函数模板(动态响应):
Mock.mock('/api/user/list', 'get', function(options) {
const url = new URL(options.url, 'http://localhost');
const page = url.searchParams.get('page') || 1;
return Mock.mock({
code: 200,
data: {
'list|10': [{
'id|+1': (page - 1) * 10 + 1,
'name': '@cname'
}],
page: parseInt(page)
}
});
});3. URL匹配规则:
// 精确匹配
Mock.mock('/api/user', 'get', {...});
// 正则匹配
Mock.mock(/\/api\/user\/\d+/, 'get', {...});
// 通配符
Mock.mock(/\/api\/.*/, 'get', {...});// GET /api/user/:id
Mock.mock(/\/api\/user\/\d+/, 'get', (options) => {
const id = options.url.match(/\/api\/user\/(\d+)/)[1];
return {
code: 200,
data: {
id: parseInt(id),
name: Mock.Random.cname()
}
};
});
// DELETE /api/user/:id
Mock.mock(/\/api\/user\/\d+/, 'delete', (options) => {
const id = options.url.match(/\/api\/user\/(\d+)/)[1];
return {
code: 200,
message: `用户${id}删除成功`
};
});src/
├── mock/
│ ├── index.ts # Mock入口
│ ├── modules/ # Mock模块
│ │ ├── user.ts
│ │ └── product.ts
│ └── utils/
│ └── extend.ts # 自定义占位符
├── api/
│ └── user.ts
└── main.ts// src/mock/modules/user.ts
import Mock from 'mockjs';
export default [
{
url: /\/api\/user\/list/,
method: 'get',
data: (options: any) => {
const url = new URL(options.url, 'http://localhost');
const page = parseInt(url.searchParams.get('page')) || 1;
return Mock.mock({
code: 200,
data: {
'list|10': [{
'id|+1': (page - 1) * 10 + 1,
'name': '@cname',
'email': '@email',
'phone': /^1[385][1-9]\d{8}/
}],
page
}
});
}
},
{
url: '/api/user/login',
method: 'post',
data: {
code: 200,
message: '登录成功',
data: {
'token': '@guid',
'userName': '@cname',
'role|1': ['admin', 'user', 'vip']
}
}
}
];// src/mock/index.ts
import Mock from 'mockjs';
export interface MockItem {
url: string | RegExp;
method: string;
data: any;
}
// 白名单配置(不拦截的接口)
const whiteList = [
// { url: '/api/real-data', method: 'get' }
];
// 自动导入modules下所有模块
const modules = import.meta.glob('./modules/*.ts', { eager: true });
// 注册Mock
function registerMock(handle: MockItem | MockItem[]) {
const mockHandler = (item: MockItem) => {
const { url, method, data } = item;
// 检查白名单
const isInWhiteList = whiteList.some(white => {
const urlMatch = typeof url === 'string'
? white.url === url
: white.url instanceof RegExp && url.source === white.url.source;
const methodMatch = !white.method || white.method.toLowerCase() === method.toLowerCase();
return urlMatch && methodMatch;
});
if (!isInWhiteList) {
Mock.mock(url, method.toLowerCase(), data);
console.log(`[Mock] 注册: ${method.toUpperCase()} ${url}`);
}
};
Array.isArray(handle) ? handle.forEach(mockHandler) : mockHandler(handle);
}
// 初始化Mock
export function setupMock() {
// 设置响应延迟
Mock.setup({ timeout: '200-600' });
// 注册所有模块
Object.values(modules).forEach((module: any) => {
if (module.default) {
registerMock(module.default);
}
});
console.log('[Mock] Mock服务已启动');
}Vue 3:
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
if (import.meta.env.DEV) {
import('./mock').then(({ setupMock }) => {
setupMock();
createApp(App).mount('#app');
});
} else {
createApp(App).mount('#app');
}React:
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
if (import.meta.env.DEV) {
import('./mock').then(({ setupMock }) => {
setupMock();
renderApp();
});
} else {
renderApp();
}
function renderApp() {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode><App /></React.StrictMode>
);
}注意: 使用动态import确保Mock代码不会被打包到生产环境!
应用场景:
// 白名单配置
const whiteList = [
// 精确匹配
{ url: '/api/real-data', method: 'get' },
// 正则匹配
{ url: /\/api\/.*\/update/, method: 'post' },
// 不指定method,匹配所有方法
{ url: '/api/health' }
];当白名单接口需要转发到真实后端时,配置Vite代理:
// vite.config.ts
export default {
server: {
proxy: {
'/api': {
target: 'http://real-backend.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
}// 全局设置
Mock.setup({
timeout: '200-600' // 200-600ms随机延迟
});
// 单个接口延迟
Mock.mock('/api/slow', 'get', () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({ code: 200, data: '延迟响应' });
}, 2000);
});
});Mock.mock('/api/error', 'get', {
code: 500,
message: '服务器错误',
data: null
});
Mock.mock('/api/not-found', 'get', {
code: 404,
message: '资源不存在',
data: null
});const originalMock = Mock.mock;
Mock.mock = function(url: any, method?: any, template?: any) {
const wrappedTemplate = function(options: any) {
console.log(`[Mock Request] ${method?.toUpperCase()} ${url}`);
console.log('[Mock Options]', options);
const result = typeof template === 'function'
? template(options)
: template;
console.log('[Mock Response]', result);
return result;
};
return originalMock.call(this, url, method, wrappedTemplate);
};| 问题 | 原因 | 解决方案 |
|---|---|---|
| Mock未生效 | Mock文件未正确导入 | 检查main.ts中动态import是否执行 |
| Network面板无请求 | Mock拦截了XHR请求 | 这是正常的,Mock拦截后不会发起真实HTTP请求 |
| 生产构建包含Mock | 使用了静态import | 改用动态import: import('./mock') |
| Mock数据不真实 | 占位符使用不当 | 使用Faker.js或自定义占位符 |
| 白名单不生效 | 匹配逻辑错误 | 检查URL和方法是否完全一致 |
Mock.js的问题:
Faker.js的优势:
import { faker } from '@faker-js/faker/locale/zh_CN';
const user = {
name: faker.person.fullName(), // 张三
email: faker.internet.email(), // zhang@example.com
phone: faker.phone.number(), // 13812345678
avatar: faker.image.avatar(), // 真实头像URL
address: faker.location.streetAddress(), // 上海市浦东新区xxx路xxx号
company: faker.company.name(), // 某某科技有限公司
date: faker.date.recent().toISOString() // 近期日期
};现代项目推荐方案:
├── 数据生成: Faker.js
├── 请求拦截: MSW (Mock Service Worker)
└── Mock服务: JSON Server
传统项目方案:
└── Mock.js (数据生成 + 请求拦截)