造数这件事,看起来最不起眼,实际上最能耗时间。前阵子帮一个团队梳理联调流程,翻他们的代码仓库,发现有一个文件叫fakeData.js,里面整整八百多行手写的对象数组:姓名从“张三1”排到“张三87”,邮箱全是test1@test.com,创建时间统一写着2023-01-01 00:00:00。问了一下才知道,这是三个人花了两个下午凑出来的,而真实接口后来加了几个字段,这个文件又得推倒重来。这种场面我见得太多了,而 Mock 工具库里的Random组件,本质上就是用来消灭这类体力活的——它把“造一批看起来像真的数据”从手写变成了声明式描述,一行配置顶掉几十行重复代码。这篇内容我会把 Random 这个组件从头到尾拆一遍:它能生成什么、底层怎么理解这些调用、模板语法怎么用、接到真实项目里有哪些策略,以及我在实际使用中踩过的坑。不管你是刚接触 Mock 数据的新手,还是已经用了半年但只停留在@name、@email这几个占位符上的老用户,应该都能从里面找到点新东西。
1. Random 组件在 Mock 体系里究竟扮演什么角色
很多人第一次接触 Mock 工具库,注意力都在“拦截请求、返回假数据”这个能力上,反而忽略了 Random 才是真正决定数据质量的那一层。拦截只是把数据送出去,数据长什么样、够不够真实、字段之间有没有内在关联,全靠 Random 撑着。理解这个分层,后面的很多用法和坑才讲得通。
1.1 手写假数据的三类隐性成本
先说清楚为什么需要它。手写假数据最直观的成本是时间,但这只是表面。真正的麻烦在于三个地方:第一是维护成本,接口字段一变,手写数据全员返工,而且往往漏改,导致前端拿到undefined还以为是后端的问题;第二是覆盖度不足,人手写数据天然偏向“理想值”,名字都是两三个字、金额都是整数、时间都在白天,边界情况一个都测不到,等上线了才被用户的长名字和负数金额打脸;第三是说服力差,test1@test.com这种数据摆到演示环境里,产品经理看一眼就皱眉头,说这不像真的。
Random 组件针对的正是后两个问题。它内置了大量贴近真实分布的数据池:中文姓名、省市地址、公司后缀、邮箱域名、常见英文单词、段落结构等等。你写@cname,它吐出来的是“周慕云”“欧阳修远”这种长度和结构都合理的名字;你写@cparagraph(2, 4),它给的是两到四句话组成的段落,而不是重复的“内容内容内容”。这种“像真的”不是审美问题,它会直接影响你在联调时能不能发现布局溢出、截断异常、空状态错位这些视觉问题。
至于维护成本,那是模板语法解决的,下一节细说。这里先建立一个认知:Random 不是“随便给个值”的工具,而是一个带数据池的语义化生成器集合,它的价值在于用最少的描述产出最接近真实分布的数据。
1.2 两种调用形态:函数直调与占位符
Random 提供两种使用方式,很多人只知道其中一种。第一种是函数直调,把它当成普通工具函数用:
const Random = Mock.Random; Random.boolean(); // 随机布尔值 Random.integer(1, 100); // 1 到 100 之间的整数 Random.float(0, 1, 2, 4); // 0 到 1 之间,2 位整数 4 位小数 Random.string(8); // 长度 8 的随机字符串 Random.cname(); // 中文姓名 Random.email(); // 邮箱地址 Random.datetime(); // 日期时间字符串第二种是占位符形式,写在数据模板的字符串里,用@开头引用:
Mock.mock({ name: '@cname', email: '@email', age: '@integer(18, 65)', intro: '@cparagraph(1, 3)' });两种形态底层是同一套生成逻辑,区别只在于“什么时候执行”。函数直调是立即执行,你拿到的是最终值;占位符是延迟执行,模板解析到那个位置时才去调用对应的生成器。这个差异决定了它们适合的场景:需要拿随机值再做二次运算(比如根据年龄判断是否成年、根据金额计算折扣)的时候,用函数直调更顺手;纯粹的字段填充,用占位符更简洁,配置和逻辑分离得也更干净。
我个人的习惯是:模板负责结构,函数负责逻辑。模板里只放纯字段映射,任何需要判断、需要计算、需要跨字段联动的部分,都提到模板外面用函数处理,这样模板始终是一份“数据说明书”,读起来一目了然。
1.3 它不负责的那部分,得自己补
有一件事必须提前说清楚,否则后面一定会踩坑:Random 只负责单点数据生成,它完全不理解业务语义。也就是说,它能给你一个合法的身份证号格式,但不会保证这个身份证号的校验位正确;它能给你一个随机日期,但不会保证这个日期在你业务允许的范围内;它能给你一个随机金额,但不会保证这个金额符合“满减后不低于 0”这种规则。
这一点在纯展示场景无所谓,但一旦你的前端代码里做了格式校验、后端做了业务校验,这些“格式对但内容不对”的数据就会直接让流程卡住。我在一个订单相关的项目里就遇到过:用@integer(1, 99999)生成订单金额,单位是分,结果生成了个位数金额,被后端的“最小下单金额”拦下来,排查了半天才发现是造数的问题,不是代码的问题。
所以定位要摆正:Random 是基础零件供应商,业务规则得你自己在它之上包一层。这个认知建立起来之后,后面的扩展、封装、坑的规避都会顺理成章。
2. 生成能力拆解:从标量数值到带语义的中文数据
Random 内置的生成器数量比大多数人想象的多,粗略分类大概能分成四组:基础标量、文本类、语义化数据、特殊资源。这一节我按这个顺序过一遍,重点不是罗列 API,而是讲清楚每个生成器的参数到底是什么意思、什么场景该选哪个、哪些参数容易被误解。
2.1 数值与布尔:参数边界的真实含义
基础标量这块看着最简单,实际上参数语义是最容易搞混的。拿Random.integer(min, max)来说,它是闭区间,两端都能取到,这点和很多语言里Math.random()的用法不一样,写边界测试的时候要特别注意。而Random.float(min, max, dmin, dmax)的四个参数分别是整数部分最小值、整数部分最大值、小数位数最小值、小数位数最大值——注意后两个控制的是小数位数,不是小数部分的值。这个设计很容易看错,我第一次用的时候写了Random.float(0, 100, 0, 99),本意是想要 0 到 99 的小数部分,结果生成了几十位小数的数字,直接超出数据库字段精度。
Random.natural(min, max)生成的是自然数,也就是非负整数,默认从 0 开始。这个和integer的区别在于语义:金额、数量、库存这类不可能为负的字段,用natural更贴切,也更安全,能避免负数导致的业务异常。Random.boolean(min, max, current)稍微特殊一点,它有三个参数,分别是最小值、最大值、当前值,用来控制概率分布,实际用得不多,大多数场景直接调无参版本就够了。
| 生成器 | 参数含义 | 常见误用 | 正确姿势 |
|---|---|---|---|
integer(min, max) | 闭区间整数 | 以为 max 取不到 | 边界测试直接用两端值 |
float(min, max, dmin, dmax) | 后两参是小数位数 | 当成小数部分取值 | 需要 2 位小数写(0, 100, 2, 2) |
natural(min, max) | 非负整数 | 用它生成可正可负的差值 | 差值类字段改用integer |
boolean() | 概率均等的布尔 | 传参控制概率搞不清语义 | 无参调用最稳 |
这里有个实操建议:把常用的数值生成规则抽成一个小函数集合,比如amount()、quantity()、percentage(),内部固定好区间和小数位。这样散落在各处的Random.float(0, 1000, 0, 2)就变成了amount(),改规则的时候一处改完全局生效,也避免了不同人写的区间不一致。
2.2 文本类生成器:字符、单词、句子到段落
文本类生成器是 Random 里被低估的一组。很多人只用Random.string()生成个随机串当 ID,其实它在填充长文本字段时价值更大。
Random.character(pool)生成单个字符,可以指定字符池,比如'lower'(小写字母)、'upper'(大写字母)、'number'(数字)、'symbol'(符号),或者直接传一个自定义字符串作为池子。Random.string(length)默认是从大小写字母和数字里取,长度可以指定,也可以传范围Random.string(5, 10)。Random.word(min, max)生成英文单词,Random.sentence(min, max)生成句子,Random.paragraph(min, max)生成段落,Random.title(min, max)生成标题。后面这几个都支持长度范围参数,单位是词数而不是字符数,这点也容易搞混。
中文场景下对应的有Random.cword(pool, min, max)、Random.csentence()、Random.cparagraph()、Random.ctitle()。中文这几个在实际项目里用得非常多——商品标题、文章摘要、评论内容、通知文案,用它们填充出来的东西,长度合理、结构自然,前端做文本截断、行高计算、换行处理的时候,效果和真实数据几乎一致。
注意:
cparagraph这类生成器的输出长度波动比较大,如果你需要严格控制字符数(比如测试输入框的 50 字限制),别依赖它的范围参数,直接生成后用slice截断更可靠。
我在一个内容社区的模拟项目里做过一个对比:用固定字符串填充摘要字段,前端的“展开全文”按钮永远不出现,因为所有数据都是同一长度;换成Random.cparagraph(2, 6)之后,长短不一的数据立刻暴露了两个样式问题——短摘要下按钮位置错位、长摘要下最后一行被裁切。这些问题如果等到真实数据进来才发现,修复成本要高得多。
2.3 语义化数据:姓名、地址、邮箱、日期
这一组是 Random 相对其他随机库的最大差异化所在,也是中文项目里最实用的部分。它内置了中文姓氏池、名字用字池、省市行政区划数据、常见邮箱域名、公司名称后缀等等,生成出来的东西直接具备“像真的”的属性。
姓名相关的有Random.cfirst()(姓)、Random.clast()(名)、Random.cname()(全名)。地址相关的有Random.province()、Random.city()、Random.county()、Random.region()(省市区全称)。需要注意的是,这几个地址生成器之间没有关联性——Random.city()返回的城市不一定属于Random.province()返回的省份。如果你的业务需要“省市区保持一致”,就得自己维护一份层级数据,或者用Random.region()一次性拿到完整地址再拆分。这一点我在联调地址选择组件时踩过,生成的数据里出现了“广东省 杭州市”这种组合,前端的级联选择器直接无法回显。
邮箱和域名这块,Random.email()、Random.domain()、Random.url()、Random.ip()、Random.tld()都挺好用。日期时间这块稍微复杂一点,Random.date(format)、Random.time()、Random.datetime()、Random.now(unit, format),其中now支持按year、month、week、day等单位偏移,用来造“最近一周内的订单”这类数据非常方便。格式化字符串用的是yyyy-MM-dd HH:mm:ss这套写法,注意HH是 24 小时制、hh是 12 小时制,写错会有半天的时间差,这个坑我后面还会展开讲。
| 字段类型 | 推荐生成器 | 需要额外处理的地方 |
|---|---|---|
| 用户姓名 | cname() | 无 |
| 手机号 | string('number', 11) | 前缀是否符合业务规则 |
| 邮箱 | email() | 无 |
| 省市区 | region() | 拆分层级保持一致性 |
| 创建时间 | datetime() | 时区与格式 |
| 订单编号 | string('number', 16) | 唯一性、前缀规则 |
2.4 图片、颜色与 GUID 这类特殊生成器
特殊资源类用得少但关键时刻很顶用。Random.image(size, background, foreground, format, text)能直接生成占位图地址,尺寸、背景色、前景色、格式、水印文字都能指定。做列表页、卡片流、头像位的时候,用它填充图片字段,页面立刻就有了真实感,而且不需要往仓库里塞一堆静态图片。不过要注意它返回的是指向占位图服务的 URL,如果你的运行环境访问不了外部地址,图片会全部加载失败,这时候得换成 base64 内联图或者本地静态资源。
Random.color()返回十六进制颜色值,Random.rgb()返回rgb(r, g, b)格式,Random.hsl()类似。这两个在填充标签色、状态色、图表配色的时候很好用。Random.guid()生成 UUID 格式的字符串,Random.id()生成 18 位身份证号格式的字符串(注意只是格式像,校验位并不保证正确)。
Random.increment(step)比较特殊,它生成的是一个自增整数,每次调用递增,可以用来做序列号、自增主键。但它是全局状态的,多次调用会持续累加,如果你在多处使用,得到的值会互相干扰。我在一个模拟分页接口的场景里用过它,结果列表接口和详情接口共用同一个递增器,数据显示得乱七八糟。后来改成每个模块单独实例化一个计数器才解决。
3. 占位符模板:把造数逻辑从 JS 代码里挪出去
函数直调适合写逻辑,但如果你要造一个包含几十个字段的对象数组,满屏的Random.xxx()调用会把结构淹没。占位符模板就是来解决这个问题的,它让你用一种近乎 YAML 的方式描述数据形状,代码量和可读性完全是两个量级。
3.1 数据模板里的属性规则语法
模板语法的核心是属性名后面的|规则。基本形式是'字段名|规则': 值,规则决定了值怎么被处理。常见的有这么几类:|+1表示自增,|1-10表示范围,|3表示重复次数或取位数,|1-3用在数组上表示长度范围,|2用在对象上表示随机取两个属性。
Mock.mock({ 'id|+1': 1, 'age|18-65': 1, 'score|1-100.1-2': 1, 'tags|3-6': ['@ctitle(2, 4)'], 'status|1': ['待支付', '已支付', '已取消'], 'profile|2': { nickname: '@cname', avatar: '@image("80x80")' } });这段配置生成的是一条带自增 ID、随机年龄、带两位小数的分数、3 到 6 个标签、随机状态、随机取两个属性组成的 profile 的对象。同样效果用函数直调写,至少要三四十行,而且改一个字段要翻半天。
'status|1': [...]这种“从数组里随机取一个”的写法特别实用,枚举值字段用它填充最自然。'score|1-100.1-2'这种带小数规则的要留意,1-100是整数部分范围,.1-2是小数位数范围,和前面float的参数语义是一致的。
3.2@占位符的解析顺序与嵌套陷阱
占位符的解析有几个细节值得记一下。第一,@后面跟的生成器名必须是已经注册过的,内置的都能直接用,自定义的必须先注册再引用。第二,占位符可以带参数,写法是@generator(arg1, arg2),参数里可以嵌套其他占位符,理论上支持多层嵌套,但嵌套层级一深就很容易出问题。
我遇到过一个典型的嵌套坑:想在段落里嵌入随机姓名,写成了'content': '@cparagraph(1, 3) @cname'。这看起来像“段落加姓名”,实际上解析器会把整个字符串当成一个整体处理,@cparagraph后面的内容被当成了它的参数延续,最终结果完全不符合预期。正确的做法是分成两个字段,或者用函数后处理拼接。这个问题的本质是:占位符的边界是靠括号和空白识别的,任何模糊的写法都可能被误解析。
提示:如果一个字段里需要拼接多种随机内容,优先在模板外处理。模板保持“一个字段一个占位符”,可读性和可调试性都好得多。
还有一个容易忽略的点:占位符在字符串中转义。如果你的文本里确实需要出现@符号本身(比如邮箱前缀、社交媒体账号),需要用@@转义,否则会被当成生成器引用。这个细节在造用户数据时特别容易中招。
3.3 模板和函数直调怎么分工
两种方式不是非此即彼的关系,实际项目里通常是混用的。我的分工原则是这样的:结构性、批量性的字段填充用模板,需要跨字段计算、需要判断逻辑、需要引用外部变量的用函数。
举个例子,一个商品列表需要生成 20 条数据,每条包含商品名、价格、库存、上下架状态、创建时间、封面图。这些字段彼此独立,用模板一行搞定。但如果还要求“库存为 0 时状态必须是已下架”,这就是跨字段约束,模板表达不了,得在生成之后遍历修正,或者干脆不用模板,用函数逐条构造。
const list = Array.from({ length: 20 }, () => { const stock = Mock.Random.natural(0, 200); return { id: Mock.Random.guid(), name: Mock.Random.ctitle(4, 12), price: Mock.Random.float(9.9, 999, 2, 2), stock, status: stock === 0 ? '已下架' : '在售', createdAt: Mock.Random.datetime('yyyy-MM-dd HH:mm:ss'), cover: Mock.Random.image('200x200', '#f5f5f5', '#333', 'png', '商品') }; });这段代码比模板啰嗦,但它把业务约束表达清楚了。什么时候该用哪种,判断标准很简单:模板里能不能只用占位符表达?能就用模板,不能就上函数。不要为了用模板而把逻辑硬塞进去,那样只会让配置变得难懂。
4. 接进真实项目时的造数策略与可复现性处理
会调用 API 和能在项目里用好是两回事。真实项目里有一堆现实约束:接口字段多、数据结构嵌套深、多人协作需要统一的造数规则、出问题时需要能复现。这一节讲的是把这些约束落地的具体做法。
4.1 用字段映射表统一管理造数规则
项目一大,最大的问题不是不会写,而是每个人写的规则不一样。同一个用户对象,A 同学写的年龄是@integer(1, 100),B 同学写的是@integer(18, 60),C 同学直接写死 25。结果不同页面的模拟数据口径不一致,联调时经常出现“这个页面能过、那个页面报错”的情况。
解法是维护一份字段映射表,把所有业务字段和对应的生成规则集中定义,整个项目共用。形式可以是一个普通的对象:
const fieldRules = { userId: () => Mock.Random.guid(), userName: () => Mock.Random.cname(), userPhone: () => Mock.Random.string('number', 11), userEmail: () => Mock.Random.email(), userAge: () => Mock.Random.integer(18, 60), userLevel: () => Mock.Random.pick(['普通', '白银', '黄金', '钻石']), createdAt: () => Mock.Random.datetime('yyyy-MM-dd HH:mm:ss'), orderAmount: () => Mock.Random.float(1, 9999, 2, 2) };这份表有三个好处:规则集中,改一处全局生效;字段有唯一来源,避免口径分裂;新人接手时看这一份就知道所有字段该怎么造,不用翻遍整个仓库。我在几个项目里推行过这个做法,效果比预期好——尤其是当后端字段调整时,只需要改映射表里的一行,而不是全局搜索替换。
映射表还可以按业务域拆分成多个文件,比如userRules、orderRules、productRules,再统一导出。规模大到一定程度之后,这种拆分是必要的,否则单文件会膨胀到几百行。
4.2 结果不可复现时怎么处理
这是一个绕不开的问题:Random 的生成基于Math.random,没有内置的种子机制,也就是说你没法让两次运行产生完全相同的随机数据。这在调试阶段非常难受——今天测出来的 bug,明天把数据刷一下,场景变了,复现不了。
我试过几种应对办法,各有适用场景:
第一种是录制真实响应。拿一次真实接口返回的数据存成 JSON 文件,需要复现的时候直接读文件,不走随机生成。这是最可靠的方式,缺点是数据是静态的,字段变化要手动更新。
第二种是固定种子 + 自实现随机源。自己包一层伪随机数生成器(比如线性同余),把种子固定住,然后用它替代Math.random。这样做能保证同一份配置每次生成完全一样的数据,代价是需要额外维护一套随机数实现,而且要保证所有生成器都走这个源——由于内置生成器内部直接调用Math.random,实际上很难完全替换,只能在自己写的数据构造逻辑里用。
第三种是关键字段固定、非关键字段随机。把会影响断言的字段(ID、状态、金额)固定下来,把纯展示字段(姓名、描述、图片)保持随机。这是折中方案,实操性最好,我在大多数项目里用的都是这一套。
| 方案 | 可复现性 | 维护成本 | 适用场景 |
|---|---|---|---|
| 录制真实响应 | 完全可复现 | 中,需定期更新 | 复杂嵌套接口、回归测试 |
| 自实现随机源 | 完全可复现 | 高,需全局替换 | 需要大批量固定数据 |
| 关键字段固定 | 部分可复现 | 低 | 日常联调、UI 开发 |
4.3 与请求拦截层配合的完整链路
Random 本身只管生成,真正让它发挥作用的是和拦截层的组合。完整的链路大致是这样:定义数据模板 → 配置拦截规则匹配目标 URL → 拦截触发时按模板生成数据 → 返回给发起请求的代码。这个链路里,Random 的定位是“数据工厂”,拦截层是“配送系统”。
实际配置时有两个细节值得注意。第一是生成时机。有的配置方式是在定义拦截规则时就生成好数据,之后每次请求都返回同一份;有的是每次请求时重新生成。前者适合需要稳定数据的场景,后者适合测试“数据变化时的界面响应”。这两种行为差异要在配置时明确区分,否则会出现“改了配置但页面没变”或者“每次刷新数据都在跳”的困惑。
第二是响应结构的一致性。真实接口通常有统一的响应包装(比如{ code, message, data }),模拟数据也一定要保持同样的结构,否则前端封装的请求层会解析失败。我见过有同学为了图快,拦截层直接返回裸数组,结果前端的统一错误处理逻辑把正常响应当成了异常,排查了好一阵。
5. 踩坑实录:随机值让断言和联调翻车的四个现场
这一节讲的是真实发生过的四类问题,每一个我都完整走过排查链路。写出来不是为了列结论,而是希望你在遇到类似现象时能顺着线索找到根因。
5.1 批量生成时的唯一性冲突
现象是:一个批量导入的模拟接口,生成 200 条用户数据,导入时提示“手机号重复”。第一反应是后端去重逻辑有问题,查了半天发现是造数的问题——Random.string('number', 11)虽然看起来是随机的,但在 200 条这个量级上,重复概率并不低,尤其是当生成器内部实现比较简单的时候。
排查链路是这样的:先把生成的 200 个手机号导出到文件,用脚本算重复率,确认确实有重复;再试 500 条、1000 条,观察重复率随数量增长的趋势,确认是概率问题而不是固定值;最后确认生成器的实现方式,明白它的取值空间有限。
修复方案有两层。短期方案是生成后做一次去重,用 Set 过滤,数量不够就补生成。长期方案是换生成策略——用自增序列加前缀拼接,或者用时间戳加随机后缀,从根本上保证唯一性:
const usedPhones = new Set(); function uniquePhone(prefix = '13') { let phone; do { phone = prefix + Mock.Random.string('number', 9); } while (usedPhones.has(phone)); usedPhones.add(phone); return phone; }教训是:任何需要唯一的字段,都不能直接依赖随机生成器。ID、手机号、订单号、邮箱、用户名,这些都要单独设计唯一性策略。
5.2 日期格式化里的时区与大小写坑
现象是:模拟数据里生成的创建时间是2024-03-15 08:00:00,但页面上显示成了2024-03-15 20:00:00,差了整整 12 小时。第一反应是后端返回了 UTC 时间,前端没做转换,查了接口返回发现数据本身就是错的。
根因在格式化字符串上。Random.datetime('yyyy-MM-dd hh:mm:ss')里的hh是12 小时制,HH才是24 小时制。我写成了hh,导致下午的时间全部被映射到了上午,看起来就像差了 12 小时。这个坑的迷惑性在于,它只在部分数据上表现异常,如果当天生成的时间恰好都在上午,你根本发现不了。
排查方法很简单:把生成的原始值和格式化后的值一起打出来对照,多生成几十条,找出异常样本,看时间分布规律。如果异常样本全部集中在某个时间段,基本可以确定是格式化问题。
注意:日期相关的生成器还有时区问题。如果生成器返回的是本地时间字符串,而前端做了
new Date()解析再格式化,可能因为运行环境时区不同产生偏移。跨时区协作的项目里,建议统一用时间戳作为中间格式。
5.3 中文长度和后端校验规则打架
现象是:某个输入框限制 20 个字,造数时用了@ctitle(5, 20),结果提交时被后端拒绝,提示“标题过长”。明明生成器参数写的是 20,为什么还超了?
原因在于长度单位不一致。生成器的参数单位可能是“词数”或者“字符数”,而后端校验的可能是字节数。中文字符在 UTF-8 下占 3 个字节,20 个中文字符就是 60 字节,如果后端限制是 50 字节,那必然超限。这个差异在纯英文场景下不存在,一进中文就暴露。
排查链路:先在前端打印生成值的length,确认字符数没超标;再打印new Blob([value]).size或者用编码函数算字节数,确认字节数超标;最后去翻后端校验逻辑,确认是按字节算的。
修复方案取决于你的目标。如果是想让数据通过校验,那就把生成范围调小,或者生成后按字节数截断。如果是为了测试边界,那就故意生成刚好卡在边界的值,看看前端有没有做字节数提示——这反而是个有价值的测试用例。
5.4 数据太“理想化”导致前端漏处理异常分支
这类问题不报错,但危害最大。用 Random 生成的数据,天然偏向“正常值”:名字长度适中、金额都是正数、列表都有内容、图片都能加载。前端在这些数据上跑得好好的,一上线遇到空列表、超长文本、加载失败的图片,页面就塌了。
我在一个商品列表页的项目里遇到过:模拟数据里所有商品都有封面图,前端就没做图片加载失败的兜底;上线后部分商品图挂了,卡片高度直接塌陷,整个列表布局错位。这个问题的根因不是代码写错,而是测试数据的分布和真实分布不一致。
解法是主动制造异常样本。在造数逻辑里预留一部分“脏数据”的比例:
function maybeEmpty(value, emptyRate = 0.1) { return Math.random() < emptyRate ? null : value; } function maybeLong(value, longRate = 0.05) { return Math.random() < longRate ? value + Mock.Random.cparagraph(3, 5) : value; } const item = { cover: maybeEmpty(Mock.Random.image('200x200')), title: maybeLong(Mock.Random.ctitle(4, 10)), price: maybeEmpty(Mock.Random.float(9.9, 999, 2, 2)), tags: Math.random() < 0.15 ? [] : [Mock.Random.ctitle(2, 4)] };这样生成的列表里,大约一成的卡片没有图,一小部分标题超长,一部分商品没有标签。前端如果没做处理,这些问题在开发阶段就会暴露,而不是等上线被用户发现。这个技巧我从实际项目里总结出来之后,几乎每个新项目都会先加进去,效果非常明显。
6. 自定义扩展与大数据量下的性能取舍
内置生成器覆盖了大多数通用场景,但业务字段千奇百怪,总有力所不及的时候。另外,当数据量上到几千上万条,生成耗时也会成为一个需要关注的点。这一节讲讲扩展和性能这两件事。
6.1 注册自定义生成器
自定义生成器的作用是把业务特有的造数规则封装起来,让模板里可以直接用占位符引用。注册方式通常是往 Random 对象上挂一个同名方法,挂上去之后@yourName就能在模板里用了:
Mock.Random.extend({ orderNo() { const date = Mock.Random.now('day', 'yyyyMMdd'); const seq = Mock.Random.string('number', 6); return `ORD${date}${seq}`; }, skuCode() { const category = Mock.Random.pick(['EL', 'CL', 'FD', 'HM']); return `${category}-${Mock.Random.string('upper', 4)}-${Mock.Random.string('number', 4)}`; }, statusText() { return Mock.Random.pick(['草稿', '待审核', '已通过', '已驳回', '已归档']); } });注册之后,模板里就能写'orderNo': '@orderNo',业务代码里也能直接Mock.Random.orderNo()。这个机制的价值在于把领域知识固化下来:订单号应该长什么样、SKU 编码遵循什么规则、状态枚举有哪些值,全部收敛到一处。新人不需要知道这些规则,直接用就行。
自定义生成器里也可以调用其他生成器,组合出更复杂的结构。但要注意别把生成器写得过于智能——如果一个生成器内部需要访问外部状态、需要做异步操作、需要读数据库,那它就不该是生成器,应该是独立的构造函数。生成器保持纯函数特性,输入相同参数输出同类结果,是最稳的设计。
6.2 关联字段的一致性维护
前面提过省市区不一致的问题,这里展开说一下通用的解决思路。关联字段的本质是多个字段共享同一份数据源,所以核心做法就是先确定数据源,再从中派生出各个字段。
以地址为例,比较稳妥的做法是维护一份简化的层级数据:
const regions = [ { province: '广东省', cities: ['广州市', '深圳市', '珠海市'] }, { province: '浙江省', cities: ['杭州市', '宁波市', '温州市'] }, { province: '江苏省', cities: ['南京市', '苏州市', '无锡市'] } ]; function mockAddress() { const region = Mock.Random.pick(regions); const city = Mock.Random.pick(region.cities); return { province: region.province, city, district: `${city}${Mock.Random.pick(['朝阳区', '高新区', '开发区'])}`, detail: `${Mock.Random.ctitle(3, 6)}路${Mock.Random.natural(1, 999)}号` }; }同样的思路适用于其他关联字段:订单的商品信息和订单金额要一致(金额 = 单价 × 数量)、用户的等级和折扣要一致、文章的作者和评论区用户名要能对上。这类一致性没法靠单个生成器解决,只能靠“先生成主数据,再派生从数据”的顺序来保证。
提示:需要保证一致性的字段,尽量在同一个函数里一次性生成完,不要分成多次独立调用。分得越散,不一致的概率越高。
6.3 上千条数据时的生成耗时
数据量小的时候无所谓,上到几千条,生成耗时就开始明显了。我做过一次简单的测试,生成 5000 条包含 15 个字段的订单数据,耗时大概在几百毫秒量级。单次看不出来,但如果页面每次进入都重新生成,累计起来就会影响体验。
优化的思路有这么几个。第一是减少不必要的生成,分页场景下只生成当前页需要的数据,不要一次性生成全量再截取。第二是缓存生成结果,同一份配置的结果如果不需要每次变化,生成一次存到内存里复用。第三是避免在生成器里做重操作,比如不要在自定义生成器里拼接超长字符串、不要做嵌套多层的递归调用。
const cache = new Map(); function getMockList(key, count, factory) { if (!cache.has(key)) { cache.set(key, Array.from({ length: count }, factory)); } return cache.get(key); }这个缓存写法很朴素,但解决了我项目里 80% 的重复生成问题。加上之后,页面切换时的数据准备耗时从肉眼可见的卡顿降到了无感。
另外提一句,如果数据量真的很大(上万条),要留意内存占用。几百条对象占的内存可以忽略,十万条带嵌套结构的对象就不是小数目了。这种情况建议改成分批生成、按需加载,而不是一次性怼进内存。
最后分享一个我一直在用的小习惯:每次改完造数规则,先单独跑一遍“数据体检”——统计生成结果里各字段的类型分布、空值比例、长度区间、重复率。这几项数据一出来,规则有没有写歪基本一眼就能看出来。尤其重复率这一项,很多隐藏的唯一性冲突都是靠它提前发现的,比等到导入报错再去查要省事得多。