Umi Mock 如何从文件声明变成 HTTP 响应

声明:本文由 Agent 生成、审校、润色

Umi Mock 不会在浏览器中替换 fetch,也不会生成前端 Mock 代码。它在 Node Server 中读取 Mock 文件、构建路由表,再注册 Express middleware。因此,排查 Mock 应沿着服务端处理链路进行:先确认声明是否进入路由表,再检查方法和路径是否匹配,最后确认 handler 是否结束响应。

本文以 Umi 4.7.6 源码 为依据,说明 umi devumi preview 如何加载、匹配和更新 Mock。Mock.js、Umi 3 以及生产环境中的 Mock 不在讨论范围内。

Mock 在请求链路中的位置

umi dev 启动时,Mock 插件读取 Mock 文件,并通过 addBeforeMiddlewares 注册 middleware。请求到达开发服务器后,Mock middleware 查找匹配的路由:命中后返回静态响应或执行函数 handler,未命中则调用 next(),把请求交给后续 middleware。

text 复制代码
Mock 文件
  │
  ▼
getMockData() ──► 路由表
  │
  ▼
createMockMiddleware()
  │
  ▼
HTTP 请求 ──► 方法、路径命中?
                 ├─ 是:执行 handler 或返回 JSON
                 └─ 否:next()

Mock middleware 挂载后,请求能否命中只取决于路由表、请求方法和路径,与页面模块是否已在浏览器中加载无关。而 Mock 与 proxy、构建工具 middleware 的先后关系则取决于 devpreview 的注册顺序,后文会分别说明。

文件声明如何进入路由表

扫描文件

加载器始终扫描 mock/**/*.[jt]s,然后追加 mock.include 中的模式,扫描结果再排除 .d.tsmock.exclude 匹配的文件。

ts 复制代码
// .umirc.ts
export default {
  mock: {
    include: ['src/pages/**/_mock.ts'],
    exclude: ['mock/legacy/**'],
  },
};

这段配置会额外加载页面旁的 _mock.ts,include 只扩大扫描范围,不替换默认目录。当文件没有生效时,先确认 glob 能否以项目根目录为基准匹配目标文件,再检查文件是否被 exclude 排除。

解析默认导出

Mock 文件默认导出一个对象。对象的 key 定义 HTTP 方法和路径,value 定义静态响应或函数 handler:

ts 复制代码
import { defineMock } from 'umi';

export default defineMock({
  '/api/users': [{ id: 1, name: 'Ada' }],
  'POST /api/users': (req, res) => {
    res.status(201).json({ id: 2, name: req.body.name });
  },
});

省略方法时,Umi 使用 GET。显式方法会转成大写,只接受 GETPOSTPUTDELETEPATCHHEADOPTIONS。方法不受支持或路径缺失时,加载阶段就会报错。

value 在运行时只能是函数、数组或普通对象。defineMock 只原样返回参数并提供类型提示;它不注册路由,也不改变加载器的运行时校验。

路由表以 method + path 作为 ID。遇到重复 ID 时,Umi 会打印 warning,并用后加载的声明覆盖已有声明。由于 glob 的文件顺序不应决定业务行为,每个 method + path 应只声明一次。

加载文件并清理缓存

Umi 使用 esbuild 临时注册 TypeScript 加载器。加载每个 Mock 文件前,它会删除该文件的 require.cache。路由表构建成功后,Umi 还会清理本轮记录的依赖缓存,并恢复注册器。因此,下次重建路由表时,相关模块会重新执行,不会沿用 Node Cache。

路由如何匹配并生成响应

middleware 逐条检查路由表:先比较 req.method,再用 path-to-regexp 匹配 req.path。Search String Params 不参与路径匹配。所有路由都未命中时,middleware 才调用 next()

ts 复制代码
export default {
  'GET /api/users/:id': (req, res) => {
    res.json({
      id: req.params.id,
      keyword: req.query.keyword,
    });
  },
};

请求 /api/users/42?keyword=admin 时,req.params.id42req.query.keywordadmin。调用函数 handler 前,middleware 会解码路径参数;解码失败时,它会抛出状态为 400URIError。静态对象和数组不需要构造 req.params,因此不会执行参数解码。

静态值返回固定响应

数组或普通对象命中后,middleware 固定执行 res.status(200).json(handler)

ts 复制代码
export default {
  'GET /api/features': {
    flags: ['search', 'export'],
  },
};

静态值适合固定 JSON,但不能根据请求改变数据、状态码或响应头。需要这些能力时,应使用函数 handler。

函数 handler 控制并结束响应

函数 handler 可以读取请求并控制响应:

ts 复制代码
export default {
  'POST /api/users': (req, res) => {
    if (!req.body.name) {
      return res.status(400).json({ message: 'name is required' });
    }

    res.status(201).json({ id: 1, name: req.body.name });
  },
};

对于非 GET 请求,middleware 依次运行 JSON、URL-encoded 和 multipart parser,再调用 handler。JSON 和 URL-encoded body 的大小限制为 5 MB。GET handler 不经过这些 parser,查询条件应放在路径参数或查询参数中,不依赖 req.body

函数 handler 必须调用 res.json()res.end() 等方法结束响应,或调用 next() 转交请求,否则请求会一直等待。

devpreview 的 Mock 生命周期

两条命令的行为对比如下:

行为 umi dev umi preview
路由更新 监听 mockinclude 路径,成功时整体替换 不监听,修改后需重启
proxy 顺序 Mock 在 proxy 前 proxy 在 Mock 前
配置门控 Mock 插件的 enableBy() 参与判断 直接调用加载器

umi dev:监听文件并替换路由表

Mock 插件默认只在 dev 命令中启用。MOCK=nonemock: false 都会禁用该插件。

插件会监听项目的 mock 路径和 include 指定的路径。文件发生变化后,它重新扫描并加载全部 Mock,再用新路由表替换共享 context 中的旧表。middleware 每次请求都从这个 context 读取路由,因此服务器无需重启,也无需重新挂载 middleware。

text 复制代码
mock 或 include 路径发生文件事件
  └─ getMockData() 重建整张路由表
      └─ context.mockData 替换成功
          └─ 下一次请求读取新表

如果 Mock 文件导入了观察路径之外的本地模块,这些模块变化时不一定会触发重建。值得注意的是,exclude 只在扫描阶段排除文件,不会缩小 dev watcher 的观察范围。重载失败时,updateMockData() 会在写入新表前捕获错误,已经成功加载的旧路由表仍然有效。此时应先修复终端最早报告的加载错误。因此,同一个请求同时匹配 Mock 和 proxy 时,Mock 会先处理。Mock 未命中或函数 handler 调用 next() 后,请求才会继续传递。在 Webpack dev 中,如果后续 GETHEAD 请求匹配到 Webpack 输出资源,webpack-dev-middleware 会在 proxy 之前直接响应。

umi preview:启动时只加载一次

umi preview 不经过 Mock 插件的生命周期。它在启动时直接加载一次路由表,不监听文件变化;修改 Mock 后必须重启 previewpreview 将 proxy 注册在 Mock 之前;匹配并被 proxy 消费的请求不会到达 Mock。当前版本的 preview 直接以 api.config.mock || {} 调用加载器,MOCK=none 不影响这条路径,这属于内部实现。

按阶段排查 Mock

Mock 没有按预期响应时,按服务端处理顺序定位:

  1. 确认命令和插件状态。 区分 umi devumi preview。使用 dev 时,检查 mock: falseMOCK=none
  2. 确认文件进入路由表。 核对默认 glob、includeexclude,并先修复终端最早报告的加载错误。语法、导入、方法和 handler 类型错误都发生在请求匹配之前。
  3. 确认路由是否匹配。 省略方法只代表 GET;Umi 匹配不含查询字符串的 req.path;同一个 method + path 不应重复声明。
  4. 确认函数 handler 结束响应或转交请求。 调用响应方法结束响应,或调用 next() 转交请求。
  5. 检查后续 middleware。 只有请求被转交后,才需要根据 devpreview 的注册顺序检查 proxy、构建工具和路由回退。

这套顺序先把问题归入三个阶段:文件没有进入路由表、路由没有匹配请求,或 handler 命中后没有结束响应。确定失败阶段后,再修改对应的 Mock 声明。

相关推荐
yivifu20 分钟前
HTML元素的textContent和innerText两个属性的差别
前端·html
天若有情67332 分钟前
开源轻量双语工具|一键批量查询 NPM 包历史下载量,支持按作者批量统计
javascript·npm·github pages·开源工具·netlify·前端开源·npm克隆量
咸鱼老弟1 小时前
Speculative Decoding(投机采样):大模型"先猜后验",生成速度翻倍
前端·算法·ai编程
a1117761 小时前
网页版「MATLAB」开源
前端·开源
JianZhen✓1 小时前
解决SPA发版旧版本残留+动态路由打包失效的完整方案(附落地代码)
前端·状态模式
计算机魔术师1 小时前
Dario Amodei 发文呼吁放缓前沿 AI 开发后各方表态汇总
前端
anyup1 小时前
仍然是简单一句话,uView Pro Starter 一键清理 Skill 发布
前端·人工智能·uni-app
福兮说2 小时前
纯前端把图片压缩到指定体积:canvas.toBlob 配合二分查找
前端·javascript·canvas·图片处理
是立不是利2 小时前
CSS 入门与进阶:从选择器到响应式布局的完整指南
前端·css
镭立智能制造2 小时前
电线电缆换线频繁?MES系统如何缩短换线时间40%
前端·制造