脚本

写了一个嵌入式 Mock 工具

最近写了一个小工具:embedded-mock-tools,很直接地说,100% 由 AI 完成的 Coding 工作,我只是不断地告诉它我要实现什么需求,从结果来看,它对我的需求实现得很好。

LynanBreeze/embedded-mock-tools: An embedded network request interception and mock panel prototype with Service Worker support.

简单来说,就是一个可以嵌入页面的 Mock 面板。引入一个 devtools-panel.js,页面上会出现一个浮动按钮,展开后可以看到请求列表、请求详情和 Mock 规则编辑器。它现在支持拦截 fetchXMLHttpRequest,也可以通过 Service Worker 让 Mock 请求出现在浏览器原生 Network 面板中。

为什么写

Mock 接口这件事并不新鲜,也不难,已经有大把大把的工具可以使用。比较麻烦的是,很多时候我只是想快速验证一个页面状态,但需要绕很远。

比如:

  1. 接口还没好,但页面要先写
  2. 想看空列表、500、超时、字段缺失等异常状态
  3. 测试环境的数据今天有,明天可能就被别人改掉了
  4. 项目不是自己完全控制的,不方便改代理或启动额外服务
  5. 有些页面只是一个 demo 或者嵌在别的系统里,不想为了它搭一套 Mock Server
  6. Mock Server 和真实请求混搭可能会遇到麻烦

之前通常会有几种做法。一种是在业务代码里临时写:

1
2
3
4
5
if (IS_MOCK) {
return {
users: []
};
}

这种方式最快,但也最容易忘记删。另一种是用代理工具或者 Mock Server。这种更正规,但每次都要单独配置路径、启动服务、同步规则。如果只是想临时看一个 UI 状态,会显得有点重。

所以我想做一个更简单的方案:让 Mock 工具直接跟着页面走。只要页面可以加载一个脚本,就可以看到请求,也可以改响应。

1
2
3
4
<script src="./devtools-panel.js"></script>
<script>
window.MockTools.init();
</script>

这样就够了。

为啥不用 Chrome Overrides

为啥不用 Chrome Overrides,这我有话说了,我是真的用了很久的 Chrome Overrides。在此之前也用过 Charles (主要用来抓包)等 HTTP 请求拦截工具。写这个工具的时候,也会想 Chrome DevTools 里已经有 Local Overrides,它的缺点是什么?怎么补齐?

Chrome Overrides 可以在本地覆盖网页资源和 HTTP response headers,也可以对 XHR/fetch 请求的返回内容做修改。好处很明显:不需要改项目代码,也不用启动额外服务,直接在 DevTools 里对某个请求右键 Override content 就可以改返回。对于很多场景,Chrome Overrides 已经够用了。

比如:

  1. 临时改一个接口返回,甚至是线上页面的接口,直接都可以改,看页面是否正常
  2. 修改某个静态资源,比如 CSS、JS、HTML
  3. 本地调试 response headers,比如 CORS 或 Permissions-Policy
  4. 希望修改跨刷新保留,但不想动项目代码

但它也有一些地方不是特别适合我想解决的问题:

只有总开关

Chrome Overrides 开关在 Sources 面板里,对于添加过 Override 配置的 URL 来说,要么全部开,要么全部关,无法只开某一个,除非把不需要开的 Override 配置删除,但是删除以后下次还想用,就得重新创建。

规则不是很好复用

Chrome Overrides 更像是“把某一次请求的返回保存成一个本地文件”。它适合改具体资源,但如果我想维护一组 Mock 规则,并且按 method、URL pattern、状态码、delay、headers 来管理,Overrides 就没那么直观。embedded-mock-tools 里更像是维护规则列表,并且可以给规则加上别名,在 Mock Rules 列表里显而易见。

1
2
3
4
5
6
{
method: "GET",
pattern: "/api/users",
status: 500,
delay: 800
}

对我来说,这种形式更接近 Mock 配置。

操作起来不算简单

Chrome Overrides 是浏览器里的能力。它适合我自己调试,但如果我要把一个 demo 发给别人,对方也需要打开 DevTools、设置 Overrides 目录、允许文件访问,再保存对应的覆盖文件。如果只是技术同事还好,如果是给产品、测试或者其他不熟悉 DevTools 的人,就有点麻烦。embedded-mock-tools 的思路是把工具嵌到页面里。对方打开页面就能看到面板,不需要提前配置 Chrome。

不方便做状态流转

有些接口不是固定返回一份 JSON。比如轮询接口,第一次返回 pending,第二次返回 processing,第三次返回 done。这种场景用 Overrides 也可以手动改文件,但每次都要自己保存、刷新、再触发请求。所以我在工具里加了 Snapshot 回放。它可以让同一个接口按顺序返回多份 Response。

又或者是比如我在做某个深层次功能的开发,但是也想开发时就能串联起来整个流程,那么我保存成为 Snapshot 就可以在开发环境直接地、符合直觉地、符合最终流程的开发与调试。

和项目的关系不同

Chrome Overrides 是个人浏览器配置,比较适合临时调试。embedded-mock-tools 更像是项目里的开发辅助脚本,可以随着 demo 页面或测试页面一起提交,也可以通过导入导出 JSON 分享一组 Mock 配置。所以它们不是替代关系。

我自己大概会这样选:

场景选择
临时改线上页面里的一个文件Chrome Overrides
临时改某个接口返回Chrome Overrides
需要维护一组 Mock 规则embedded-mock-tools
需要给别人一个可直接打开的 demoembedded-mock-tools
需要按顺序回放多次接口响应embedded-mock-tools
需要调试 response headersChrome Overrides 或 embedded-mock-tools

Chrome Overrides 更像是 DevTools 的一个高级功能。embedded-mock-tools 则更偏项目内工具。

功能

请求列表

面板左侧是请求列表,可以查看页面中的请求记录,实时更新。包括请求方式、URL、状态码、耗时、请求头、请求体、响应头和响应内容。

请求列表和响应详情

我最开始只想做一个简化版 Network 面板,至少不用在业务代码里到处 console.log。请求数量做了限制,目前最多保留 200 条,避免开着页面调试一段时间以后列表无限增长。

Mock 规则

右侧是 Mock 规则编辑区,一条规则大概长这样:

1
2
3
4
5
6
7
8
9
{
enabled: true,
method: "GET",
pattern: "/api/users",
status: 200,
delay: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({ users: [] }, null, 2)
}

Mock 规则列表和编辑区

目前 pattern 支持两种写法。普通字符串:

1
/api/users

简单正则:

1
/\/api\/users\/\d+/

没有做得太复杂。因为对这个工具来说,规则写起来越复杂,排查时就越不直观。大部分情况下,URL 包含匹配已经够用了。

一个 Rule 可以配置多个 Config,可添加多个 Config,只允许一个 Config 开启(当然了,一个接口一次怎么可能返回两个不同的 Response 呢)。

快照

后来又加了 Snapshot,普通 Mock 比较适合“这个接口一直返回这个结果”的场景。

但是经常会有但是,情景如下:

开发: 这啥 Bug,这么难复现,号借我登一下
测试: 好哦,但是等下要还我,我还要测试
开发: 好,我看个 Bug 就还你
测试: 不要改东西哦,我好不容易造的数据
开发: 好的,哥

很难说作为一个开发(特别是前端)没有遇到过这种场景。例如一个任务状态接口,第一次返回 pending,咔咔一番操作以后,第二次返回 processing,咔咔第二番操作以后,第三次返回 done。如果只有一份静态 JSON,就很难模拟这个过程。

Snapshot 里可以保存一组 Responses,然后按顺序回放。超过长度以后,可以重复最后一条,也可以循环。

从请求记录中选择多次响应并保存为 Snapshot

这个功能对轮询、分页、状态流转一类场景会比较有用。

持久化

Mock 规则刷新后不能丢,所以配置优先保存到 IndexedDB。如果 IndexedDB 不可用,就退回到 localStorage。之前老版本保存在 localStorage 里的数据,也会在初始化时迁移到 IndexedDB。另外也做了导入导出,因为浏览器存储不适合当备份,清理站点数据后就没了。导出成 JSON 后,可以从它恢复备份,也可以把某个问题的复现配置发给别人。

为什么引入 Service Worker

最开始只靠 JavaScript hook 就可以做。拦截 fetch 时,可以替换 window.fetch;拦截 XMLHttpRequest 时,可以包装原始 XHR 对象,覆写 opensendsetRequestHeader,在命中规则时模拟 readyStatestatusresponseText 和相关事件。这种方式的好处是简单,而且兼容 file://,直接打开 index.html 也能用。

问题是,它不会真的走浏览器网络栈。也就是说,在页面里的面板能看到请求,但浏览器 DevTools 的 Network 面板里不一定能看到完整记录。所以又加了 Service Worker。

http://localhosthttps 环境下,工具会尝试注册 mocktools-sw.js。命中规则后,由 Service Worker 在 fetch 事件里返回 Mock Response,这样浏览器原生 Network 面板也可以看到这些请求。

但是 Service Worker 也不是万能的。

  • 不能在 file:// 下使用
  • 它要求安全上下文,通常是 httpslocalhost
  • 第一次注册后,当前页面不一定立刻被 controller 接管
  • 页面和 worker 之间要同步规则

所以现在工具里同时保留了两套方案。能用 Service Worker 时,让它来处理请求;不能用时,就退回到 JS hook。

难点

这个工具的代码没有复杂依赖,但里面有不少细节。

fetch 的响应只能读一次

Response body 是流。读取响应内容用于展示时,如果直接读原始 response,会影响业务代码后续读取。所以在拦截真实请求时,需要先 response.clone(),读取 clone 出来的响应文本,再把原始 response 返回给业务代码。这点很重要,调试工具不能因为自己要展示内容,就改变业务代码的行为。

XHR 的模拟比较麻烦

XMLHttpRequestfetch 麻烦。fetch 命中 Mock 后返回一个新的 Response 就可以了,但 XHR 有自己的状态机和事件模型。

业务代码可能监听:

  • readystatechange
  • load
  • loadend

也可能读取:

  • status
  • statusText
  • response
  • responseText
  • getAllResponseHeaders()

所以命中 XHR Mock 时,不能只是返回一段字符串,还要补齐这些状态和事件。

Service Worker 状态同步

Mock 规则是在页面面板里编辑的,但真正拦截请求的可能是 Service Worker。因此每次规则变化、全局开关变化、snapshot 变化,都要通过 postMessage 同步给 worker。另外 worker 可能处于 installingwaitingactive,也可能已经是 controller,所以同步时不能只找一个固定对象。

UI 状态

一开始只是请求列表和详情,写着写着状态就多了。

  • 面板展开或收起
  • 当前选中的请求
  • 当前选中的 Mock 规则
  • 请求搜索和排序
  • 分组 tab
  • snapshot 列表、选中项和编辑草稿
  • 详情区域布局
  • 存储状态和错误提示
  • Service Worker 是否可用

现在这个工具还是原生 JavaScript 写的,没有引入 React/Vue。好处是拷贝一个文件就能用,坏处是状态管理要自己收敛。如果继续做下去,应该会把 UI、存储、拦截逻辑拆开,不然 devtools-panel.js 会越来越长。

持久化降级

IndexedDB 也不是永远可用。隐私模式、浏览器限制、异常环境,都可能导致读取或写入失败。所以写入失败时会退到 localStorage,如果两者都失败,至少要保证当前内存状态还能继续用。

适合什么

我觉得它比较适合这些场景:

  • 快速调试前端页面的接口状态
  • 做 UI demo 时预置一组接口响应
  • 在没有完整后端环境时继续开发交互
  • 给测试或产品复现某个异常状态
  • 在静态页面、嵌入式页面、WebView 或第三方容器里做轻量调试
  • 把某组 Mock 配置导出后分享给其他人

它不是为了替代所有 Mock 方案。如果项目已经有成熟的 Mock Server、接口平台、契约测试,那还是继续用那些方案更合适。这个工具更像是一个临时但好用的小面板,需要的时候打开,不需要的时候关掉。

结束

目前先这样,至少在需要快速改接口返回、复现异常状态的时候,它已经能解决我的问题。