造数这件事看起来最不起眼实际上最能耗时间。前阵子帮一个团队梳理联调流程翻他们的代码仓库发现有一个文件叫fakeData.js里面整整八百多行手写的对象数组姓名从“张三1”排到“张三87”邮箱全是test1test.com创建时间统一写着2023-01-01 00:00:00。问了一下才知道这是三个人花了两个下午凑出来的而真实接口后来加了几个字段这个文件又得推倒重来。这种场面我见得太多了而 Mock 工具库里的Random组件本质上就是用来消灭这类体力活的——它把“造一批看起来像真的数据”从手写变成了声明式描述一行配置顶掉几十行重复代码。这篇内容我会把 Random 这个组件从头到尾拆一遍它能生成什么、底层怎么理解这些调用、模板语法怎么用、接到真实项目里有哪些策略以及我在实际使用中踩过的坑。不管你是刚接触 Mock 数据的新手还是已经用了半年但只停留在name、email这几个占位符上的老用户应该都能从里面找到点新东西。1. Random 组件在 Mock 体系里究竟扮演什么角色很多人第一次接触 Mock 工具库注意力都在“拦截请求、返回假数据”这个能力上反而忽略了 Random 才是真正决定数据质量的那一层。拦截只是把数据送出去数据长什么样、够不够真实、字段之间有没有内在关联全靠 Random 撑着。理解这个分层后面的很多用法和坑才讲得通。1.1 手写假数据的三类隐性成本先说清楚为什么需要它。手写假数据最直观的成本是时间但这只是表面。真正的麻烦在于三个地方第一是维护成本接口字段一变手写数据全员返工而且往往漏改导致前端拿到undefined还以为是后端的问题第二是覆盖度不足人手写数据天然偏向“理想值”名字都是两三个字、金额都是整数、时间都在白天边界情况一个都测不到等上线了才被用户的长名字和负数金额打脸第三是说服力差test1test.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)非负整数用它生成可正可负的差值差值类字段改用integerboolean()概率均等的布尔传参控制概率搞不清语义无参调用最稳这里有个实操建议把常用的数值生成规则抽成一个小函数集合比如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% 的重复生成问题。加上之后页面切换时的数据准备耗时从肉眼可见的卡顿降到了无感。另外提一句如果数据量真的很大上万条要留意内存占用。几百条对象占的内存可以忽略十万条带嵌套结构的对象就不是小数目了。这种情况建议改成分批生成、按需加载而不是一次性怼进内存。最后分享一个我一直在用的小习惯每次改完造数规则先单独跑一遍“数据体检”——统计生成结果里各字段的类型分布、空值比例、长度区间、重复率。这几项数据一出来规则有没有写歪基本一眼就能看出来。尤其重复率这一项很多隐藏的唯一性冲突都是靠它提前发现的比等到导入报错再去查要省事得多。