Mock 数据策略:让原型看起来真实
原型好不好用,70% 取决于 Mock 数据的真实度。前面三篇业务模块的文章里我贴的截图,列表里那些"管网泄漏检测"、"压力监测分析"、"流量预测模型",名字不重复、时间有分布、状态有逻辑------不是因为我一口气写了 75 条手写数据,而是用了 mockjs。本文把这个"让假数据看起来像真的"的策略完整拆开。
一、为什么 Mock 数据不是"随便敲几条就行"
我给客户做演示时,对方第一个评价不是"界面不错",而是"你们已经接了很多模型了啊"------他看到了列表里的 30 条数据,每条名称都不一样、时间分布在最近三个月、状态有草稿有已发布有已下线,看起来就是一套真实运行中的系统。
这就是好 Mock 数据和差 Mock 数据的差距:
| 维度 | 差的 Mock | 好的 Mock |
|---|---|---|
| 数据量 | 3~5 条 | 30 条(填满分页) |
| 名称 | test1、test2、test3 |
"管网泄漏检测模型 V3"、"压力监测实时分析引擎" |
| 时间 | 全是同一天 | 分布在最近 3 个月,符合正态分布 |
| 状态 | 全是"草稿" | 四种状态按比例混合(30% 草稿、20% 审核中、40% 已发布、10% 已下线) |
| 字段关联 | 外键对不上 | 模型类型和模型名称保持一致 |
| 搜索效果 | 搜"管网"返回空 | 搜"管网"返回 8 条,且结果各不相同 |
差的 Mock 数据做演示,客户一看就知道是 demo;好的 Mock 数据,客户会把它当成真实系统在操作。
二、mockjs 基础:三分钟上手
mockjs 是国产 Mock 神器,核心就两个东西:数据模板语法 和占位符。
2.1 占位符:一行代码生成真实感数据
js
import Mock from 'mockjs'
// 随机中文名称,5~10 个字
Mock.mock('@ctitle(5, 10)') // → "管网泄漏检测模型"
// 随机中文人名
Mock.mock('@cname') // → "张伟"
// 随机日期时间
Mock.mock('@datetime("yyyy-MM-dd HH:mm:ss")') // → "2026-07-15 14:32:18"
// 随机 URL
Mock.mock('@url("https")') // → "https://api.pipe-network.cn/v3/leak-detect"
// 从数组中随机选一个
Mock.mock('@pick(["HTTP", "HTTPS", "gRPC", "WebSocket"])') // → "HTTPS"
// 随机 ID
Mock.mock('@id') // → "440000202607153217"
// 随机段落
Mock.mock('@cparagraph(2, 5)') // → 2~5 句随机中文段落
2.2 数据模板语法
模板语法让一条数据模板生成数组变得极其简单:
js
const template = {
'list|30': [{ // 生成 30 条
id: '@id',
modelName: '@ctitle(6, 14)',
modelType: '@pick(["algorithm", "geometry", "physics", "rule"])',
status: '@pick(["draft", "auditing", "published", "offline"])',
creator: '@cname',
createTime: '@datetime("yyyy-MM-dd HH:mm:ss")',
updateTime: '@datetime("yyyy-MM-dd HH:mm:ss")',
'apiCount|1-5': 1, // 1~5 之间的随机整数
description: '@cparagraph(1, 3)'
}]
}
const data = Mock.mock(template)
console.log(data.list.length) // 30
'list|30' 中的 |30 就是数据模板语法的核心------告诉 mockjs 这个数组要生成 30 条。每条里的占位符每次调用都会重新随机,30 条数据天然不重复。
三、集成 axios:拦截请求,返回 Mock 数据
mockjs 最优雅的用法不是独立调用,而是集成到 axios 拦截器里------前端代码写正常的 axios.get('/api/models'),请求被 mockjs 拦截,返回假数据。整个前端业务代码一行不用改。
3.1 目录结构
src/
├── mock/
│ ├── index.js # 统一入口,按环境判断是否加载
│ ├── api-registry.js # API 注册管理的 Mock
│ ├── model-hub.js # 模型汇聚的 Mock
│ └── model-publish.js # 模型标准发布的 Mock
3.2 统一入口:按环境加载
js
// src/mock/index.js
import { setupApiRegistryMock } from './api-registry'
import { setupModelHubMock } from './model-hub'
import { setupModelPublishMock } from './model-publish'
// 只在开发环境启用 Mock
if (import.meta.env.DEV) {
setupApiRegistryMock()
setupModelHubMock()
setupModelPublishMock()
console.log('[Mock] 已加载 3 个模块的 Mock 数据(共 75 条)')
}
3.3 单个模块的 Mock 定义
js
// src/mock/api-registry.js
import Mock from 'mockjs'
export function setupApiRegistryMock() {
// GET 列表接口(支持筛选和分页)
Mock.mock(/\/api\/registry\/list/, 'get', (options) => {
const params = new URLSearchParams(options.url.split('?')[1])
const page = parseInt(params.get('page')) || 1
const pageSize = parseInt(params.get('pageSize')) || 30
const keyword = params.get('keyword') || ''
const protocol = params.get('protocol') || ''
const status = params.get('status') || ''
// 生成 60 条原始数据(方便模拟筛选前后变化)
const allData = Mock.mock({
'list|60': [{
id: '@id',
name: '@ctitle(6, 14)',
protocol: '@pick(["HTTP", "HTTPS", "gRPC", "WebSocket"])',
url: '@url("https")',
method: '@pick(["GET", "POST", "PUT", "DELETE"])',
status: '@pick(["enabled", "disabled"])',
creator: '@cname',
'createTime': '@datetime("yyyy-MM-dd HH:mm:ss")',
description: '@cparagraph(1, 2)'
}]
}).list
// 筛选逻辑
let filtered = allData
if (protocol) filtered = filtered.filter(item => item.protocol === protocol)
if (status) filtered = filtered.filter(item => item.status === status)
if (keyword) filtered = filtered.filter(item => item.name.includes(keyword))
const total = filtered.length
const start = (page - 1) * pageSize
const list = filtered.slice(start, start + pageSize)
return {
code: 200,
data: { list, total }
}
})
// POST 新增接口
Mock.mock(/\/api\/registry\/create/, 'post', {
code: 200,
message: '新增成功'
})
// DELETE 删除接口
Mock.mock(/\/api\/registry\/delete/, 'delete', {
code: 200,
message: '删除成功'
})
}
注意几个关键设计:
- 正则匹配路径 :
/\/api\/registry\/list/用正则而非字符串,灵活性更高。 - 从 query 中解析参数 :
new URLSearchParams(options.url.split('?')[1])把真实 URL 参数拆出来用。 - 筛选在前端 Mock 层完成 :前端传
?protocol=HTTPS,Mock 层真的从 60 条里筛出 HTTPS 的------这样搜索、筛选、分页的行为和真实接口完全一致。
3.4 main.js 中引入
js
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './store'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import 'element-plus/dist/index.css'
// Mock 必须在 axios 初始化之前引入
import './mock'
import './styles/index.scss'
const app = createApp(App)
app.use(pinia)
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')
关键顺序 :import './mock' 必须放在 axios 初始化之前。因为 mockjs 是通过重写 XMLHttpRequest 实现拦截的,如果你的 axios 实例已经在 mock 之前创建了请求,就不会被拦截。
四、真实感数据的四个原则
写 Mock 数据不是敲占位符就完了,要让它"看起来像真的",需要遵循四个原则:
4.1 差异性:每条数据都有差别
js
// ❌ 差:所有模型都叫同一个名字
{ name: 'test_model', ... }
// ✅ 好:名称各不相同
{ name: '@ctitle(6, 14)', ... }
差异不仅体现在名称上,还要在元数据上有差异------描述长度不一、时间有先有后、状态按比例混合。
4.2 业务性:符合领域逻辑
不能随便拼字段。模型类型是"算法模型"时,名称里出现"几何"就不对。解决方式是按类型分组生成:
js
const modelTypes = ['algorithm', 'geometry', 'physics', 'rule']
const namePrefixes = {
algorithm: ['管网', '泄漏', '检测', '预测', '优化', '调度'],
geometry: ['管道', '三维', '空间', '拓扑', '网络', '结构'],
physics: ['压力', '流量', '温度', '流速', '水力', '水质'],
rule: ['阈值', '规则', '策略', '控制', '联动', '触发']
}
function generateModelName(type) {
const prefixes = namePrefixes[type]
const a = prefixes[Math.floor(Math.random() * prefixes.length)]
const b = prefixes[Math.floor(Math.random() * prefixes.length)]
return `${a}${b}模型 V${Math.floor(Math.random() * 5) + 1}`
}
4.3 时效性:时间分布在合理区间
js
// ❌ 差:所有数据都是同一天
createTime: '2026-08-10 14:00:00'
// ✅ 好:分布在最近 3 个月
createTime: Mock.mock('@datetime("yyyy-MM-dd HH:mm:ss")')
mockjs 的 @datetime 默认生成当年内的随机时间。如果你需要控制在最近 3 个月,可以用 Random.date() 配合手动拼接:
js
Mock.Random.extend({
recentDate() {
const now = Date.now()
const threeMonthsAgo = now - 90 * 24 * 3600 * 1000
const ts = threeMonthsAgo + Math.random() * (now - threeMonthsAgo)
const d = new Date(ts)
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
})
4.4 关联性:外键字段对得上
模型的标准发布记录里有个 modelId 字段,它必须和模型汇聚里的某个模型 ID 一致。在 Mock 阶段,可以先生成模型池,再基于池子生成发布记录:
js
// 先生成 30 个模型(全局共享)
const modelPool = Mock.mock({ 'list|30': [{ id: '@id', name: '@ctitle(6, 14)', type: '@pick(...)' }] }).list
// 再基于 modelPool 生成发布记录
const publishList = modelPool.slice(0, 15).map(model => ({
modelId: model.id,
modelName: model.name,
modelType: model.type,
status: Mock.mock('@pick(["draft","auditing","published","offline"])'),
// ...
}))
五、三个模块的 Mock 数据概览
| 模块 | 数据量 | 核心字段 | 特殊逻辑 |
|---|---|---|---|
| API 注册管理 | 60 条 | name / protocol / url / method / status | 支持 keyword / protocol / status 三维筛选 |
| 模型汇聚 | 30 条 | modelName / modelType / provider / interfaces | 4 种类型各 7~8 条,按比例混合 |
| 模型标准发布 | 15 条 | modelName / channel / version / status | 四种状态按 3:2:4:1 比例分布,modelId 外键关联模型池 |
三个模块合计 105 条原始数据,经过分页、筛选、搜索后呈现在界面上。数据够多、够真、够乱(有草稿、有已下线、有空描述),看起来就是"用了几个月的系统"。
六、分页接口的 Mock:关键是要返回 total
这是新手最容易踩的坑------Mock 的分页接口只是把前端传的 page 和 pageSize 乘一下切数组,但不返回 total。前端分页器的总条数永远是 0 或者 30。
正确做法:
js
Mock.mock(/\/api\/registry\/list/, 'get', (options) => {
const { page, pageSize, ...filters } = parseParams(options.url)
const allData = generate60Records() // 先生成全量数据
const filtered = applyFilters(allData, filters) // 在 Mock 层筛选
const total = filtered.length // ← 这个必须有
const start = (page - 1) * pageSize
const list = filtered.slice(start, start + pageSize)
return {
code: 200,
data: { list, total } // ← 返回 total
}
})
有了 total,前端分页器才能正确显示"共 60 条,第 1/2 页"。筛选条件变化时 total 也会随之变化------搜"管网"可能返回 8 条,筛"HTTPS"可能返回 23 条。
七、模拟网络延迟:让原型有"加载感"
真实系统不会瞬间返回数据。如果 Mock 接口 0ms 响应,用户感觉不到数据是从后端来的,而且你也没法验证 loading 态的效果。
js
function simulateDelay(ms = 300) {
return new Promise(resolve => setTimeout(resolve, ms + Math.random() * 200))
}
Mock.mock(/\/api\/registry\/list/, 'get', async (options) => {
// ... 数据生成逻辑 ...
await simulateDelay(400) // 模拟 400~600ms 网络延迟
return { code: 200, data: { list, total } }
})
Math.random() * 200 让每次请求的延迟略有差异(400~600ms),就像真实网络抖动。调试 loading 动画、骨架屏、防重复提交,这些全靠这段延迟。
八、dev/prod 环境自动切换
这是 mockjs 的保命技能------忘了关 Mock 就把项目部署上线,客户看到的全是假数据,这是灾难级事故。
js
// src/mock/index.js
if (import.meta.env.DEV) {
// 开发环境:加载所有 Mock
setupAllMocks()
} else {
// 生产环境:什么都不做,请求正常发送到真实后端
console.log('[Mock] 生产环境,Mock 已自动禁用')
}
原理:mockjs 通过重写 XMLHttpRequest.prototype.send 来拦截请求。如果你的 Mock 文件根本没有被 import,就不会发生重写------请求走正常网络通道。
Vite 的 import.meta.env.DEV 在 vite build 时会被静态替换为 false,tree-shaking 甚至会直接把整个 mock 目录的代码从生产包中剔除。零运行时开销,零泄露风险。
九、Mock 数据的工程价值:不只是"造假"
Mock 数据在原型阶段的价值是显而易见的------看得见摸得着。但它真正的工程价值在于三个方面:
1. 前端可以完全独立开发。
后端接口还没写好?没关系,Mock 先行。前后端只需要约定好接口字段(name / protocol / url / method / status),前端写完 Mock 就可以开发 UI、交互、状态管理、筛选分页,后端出来后再换接口------改一行 axios 的 baseURL 即可。
2. 倒逼接口设计。
写 Mock 的过程就是思考"这个接口需要返回什么字段"的过程。如果你连 Mock 数据都写不出来(不知道字段叫什么、类型是什么),说明接口设计还没想清楚。
3. 发现 UI 边界问题。
Mock 数据生成 60 条,列表正常;把某条数据的 description 设为空字符串,UI 是崩了还是优雅降级?把 name 设为 40 个字,表格列宽会不会撑破?这些问题,Mock 阶段就能发现。
十、一张图总结 Mock 策略
┌──────────────────────┐
│ 前端业务代码 │
│ axios.get('/api/ │
│ models?page=1) │
└──────────┬───────────┘
│
import.meta.env.DEV?
│ │
YES NO
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ mockjs 拦截 │ │ 真实网络请求 │
│ · 生成 60 条 │ │ · 请求后端 │
│ · 筛选+分页 │ │ · 返回真实数据 │
│ · 模拟 400ms │ │ │
└──────┬───────┘ └──────────────┘
│
▼
┌──────────────────┐
│ 返回 Mock 数据 │
│ { list, total } │
└──────────────────┘
前端代码只写 axios.get(),不关心数据来源。开发环境 mockjs 拦截,生产环境自动走真实网络。这就是 Mock 策略的工程之美。
下一篇预告:数据有了,交互也完整了,但首屏 3.2 秒、打包 1.8MB------原型也要快。下一篇讲路由懒加载、Element Plus 按需引入、KeepAlive 缓存,把首屏压到 0.9 秒。