Skip to content
CarlOpenLabPublic

About

Chrome/Edge DevTools panel that captures fetch/XHR requests and rewrites their responses — status, headers, body, delay and failures — so you can test how your code handles the cases the backend never returns

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

API Mock

Chrome / Edge 的 DevTools 面板:实时抓取页面上的 fetch / XMLHttpRequest,并按规则改写响应——响应体、状态码、响应头、延迟、故障,用来在前端把各种边界情况跑一遍。

装好之后按 F12,和 Elements / Network 并排多一个「API Mock」标签页。


快速开始

pnpm install
pnpm build

然后:

  1. 打开 chrome://extensions,右上角开启开发者模式
  2. 点「加载已解压的扩展程序」,选本项目的 dist/ 目录
  3. 跑实验台:pnpm demo,浏览器打开 http://localhost:8787
  4. 刷新一次页面(见下方「装完为什么要刷新」),按 F12 切到 API Mock 面板

用法

面板分两个页签。

请求页 —— 左边是实时请求流,每行显示方法、URL、应用实际收到的状态码、这条请求被怎么处理了(放行 / 改写 / 伪造 / 失败)、耗时。上面是过滤框(匹配 URL 或方法)和结果筛选(全部 / 改写 / 伪造 / 失败 / 放行),底部会显示「已过滤 N / M 条」,右下角可以关掉「跟随最新」让列表别自己跳。

点任意一行看详情:请求头、请求体、响应头、响应体(都是应用实际收到的,不是网络上的原始值)。JSON 会自动缩进 + 上色,每个区块的标题可以点开折叠,两栏之间的分隔条可以拖(双击复位)。URL 和每个区块各有一个「复制」,点完按钮自己变成「已复制」。

美化会改动空白。要看应用逐字节收到的东西,点响应体右上角的「显示原文」。面板上凡是美化过的 body 都会标出「已美化(空白与原文不同)」。

详情里有个「以此请求建规则」,会带着这条请求的 URL 通配和方法跳到规则页,是主要动线。

快捷键(焦点不在输入框里时)

键 作用
↓ / j、↑ / k 在请求流里上下切换
Enter 当前选中的请求 → 直接建规则
Esc 取消选中;编辑器里有未保存修改时,先关掉那条确认提示
⌘S / Ctrl+S 保存当前规则(焦点在编辑器里时)

规则页 —— 左边规则列表(勾选框启停、↑↓ 调优先级、✕ 删除),右边是编辑器。

编辑器会盯着草稿和你已经保存的那份规则的差别:改过之后保存按钮旁会出现「未保存」,保存后短暂变成「已保存」。这时候如果去点左边另一条规则、切页签或新建规则,面板不会静默丢掉你的修改,而是在顶部问一句——「保存并切换」/「丢弃修改」/「取消」。

删除规则、清空请求列表、导入规则都会立即执行,但底部会留一条 5 秒的「撤销」,反悔不用重来。所有反馈都在面板内部,不再有浏览器原生弹窗。

一条规则由三部分组成:

部分 说明
匹配 URL 模式(通配 / 包含 / 正则)+ 方法。编辑器里有「试试匹配」,粘个真实 URL 进去当场看会不会命中
动作 穿透改写 / 直接伪造;状态码、延迟、故障
响应头 / 响应体 头可以增可以删;体可以整段替换,也可以深合并到真实响应上

两种模式的区别

  • 穿透改写(默认)—— 真实请求照发,拿到响应后在最后一刻改掉。后端副作用真实发生、你没碰的字段都是真的。想让接口返回空列表时,这是唯一真实的做法。
  • 直接伪造 —— 完全不发请求,new Response() 现造。后端挂了、接口还没写、断网、或者要造一个后端不可能返回的数据时用。「这条请求有没有真的发出去」可以用 Network 面板验证。

合并 JSON 是默认选项,因为它最常用

只想改一个字段、别的都别动——写 {"data": []} 就够了,真实响应里的其他字段原样保留。数组整体替换(不逐项合并),因为逐项合并在长度不一致时的结果没法预测。


已知边界

这些是这条技术路线的固有边界,不是 bug:

拦不到的

  • navigator.sendBeacon —— 埋点常用它,不在 fetch / XHR 里
  • Service Worker 发起的请求
  • 页面资源(img / script / css)
  • WebSocket、EventSource

时序相关的

  • 装完插件后已打开的页面要刷新一次。content script 是声明式注入的,只在页面加载时跑;刷新后才会接管。刷新之后建立的每个 iframe 也都会自动接管。
  • 装了插件而没开 DevTools 面板时,规则依然生效(规则存在 storage.local,页面侧一直持有副本)。面板只是编辑和观察的界面,不是开关——要临时全关请用面板上的总开关。

改写能力的边界

  • 只有真的替换响应体时才会缓冲整个响应。 只改状态码或响应头时,body 用 new Response(real.body, …) 原样透传,SSE 这类流式响应不受影响。这条有专门的验证覆盖。
  • XHR 命中规则时一定会缓冲——因为 responseText 本身就是字符串,绕不开。
  • 伪造出来的响应不受 CORS 约束。 既是特性(能造跨域数据)也是陷阱(本地过了不代表线上过)。
  • new Response() 的状态码只能是 200–599,造不出 1xx,204 / 304 不能带 body。编辑器会直接拦下来而不是让你白试。

面板自身的开销

  • 面板用 Ant Design Vue,panel.js 从手写 CSS 那版的 ~82KB gzip 涨到 ~178KB gzip(AntD 的 CSS-in-JS 运行时 + 组件)。这是 DevTools 面板里可以忽略的量级,换来的是深色/浅色/对比度由框架算法保证。
  • 请求流最多渲染 300 行,超出会明确提示;background 每标签页缓冲 500 条。

采集开销

面板顶部「采集 Body」开关控制是否记录未命中规则的请求 / 响应体。关掉可以消除全部额外开销(此时未命中的请求只记录 URL、方法、状态码、响应头、耗时——头一直都在采,读它不碰 body)。text/event-stream 永远不采集——读下去会把流吃掉。


架构

┌─ 页面 (MAIN world) ───────────────────────────┐
│  inject.js   包装 window.fetch / XMLHttpRequest│
│              持有规则快照,本地同步匹配         │
└────────────────┬──────────────────────────────┘
                 │ window.postMessage
┌────────────────┴──────────────────────────────┐
│  bridge.js  (ISOLATED world content script)   │
└────────────────┬──────────────────────────────┘
                 │ chrome.runtime Port
┌────────────────┴──────────────────────────────┐
│  后台 service worker                          │
│    规则持久化 + 请求流按 tabId 缓冲            │
└────────────────┬──────────────────────────────┘
                 │ Port
┌────────────────┴──────────────────────────────┐
│  DevTools 面板 (Vue 3 + Ant Design Vue)       │
└───────────────────────────────────────────────┘

几个值得说明的决定:

为什么要有个 bridge。 MAIN world 的脚本跑在页面自己的环境里,能改到页面用的 window.fetch,但没有任何 chrome.* API;ISOLATED world 的 content script 正好相反。两个世界各缺一半,只能靠 postMessage 缝起来。(消息带一次性 nonce,用来和站点自己的 postMessage 区分开——这是卫生措施,不是安全边界:页面本来就能拿原始 fetch 绕开 hook。)

为什么匹配在页面里做,而不是问后台。 决策必须是同步的(XHR 的同步模式不允许 await),而且每次请求都往返一趟消息会白白加延迟。规则变更由后台广播,页面侧持有副本。

面板为什么是 Vue + Ant Design Vue,而且颜色一个都不手写。 手调一套深色板(背景层次、边框、悬停/选中对比度、代码高亮)很容易在某一块上翻车,所以面板的颜色全部由 Ant Design 的 darkAlgorithm / defaultAlgorithm 从一组 seed token 推导:AntD 组件直接用 token,自绘的密集行与代码块通过 theme.useToken() 把同一批 token 写成 --panel-* CSS 变量(src/devtools/theme.ts),CSS 里不出现任何色值。深浅切换因此不可能出现「组件是深色、自绘部分是另一套深色」。

主题来源也只有一个:chrome.devtools.panels.themeName。DevTools 的主题是它自己的设置,切主题不会改 prefers-color-scheme;只绑媒体查询会在「深色 DevTools + 浅色系统」下画出一整套浅色面板。面板在启动和每次重新获得焦点时各探一次,因为 DevTools 不会给扩展发主题变更事件。

组件用 TSX 而不是 SFC:仓库的 typescript@7(原生编译器)没有暴露 ./lib/tsc,vue-tsc 起不来,SFC 会让整条类型检查失效。jsx: preserve + jsxImportSource: vue 让 tsc --noEmit 继续覆盖全部组件。

为什么 XHR 的改写要另起一个对象发请求。 Response 的 status / headers 在 fetch 里还能重建,但 XHR 的 responseText / status 是原型上的只读 getter,读的是内部状态,没有 API 能改。反过来想「抑制真实事件、稍后重放」又得连 addEventListener 一起劫持,漏一处应用就会看到两次响应。换个对象发请求干净得多。


开发

pnpm dev          # 两条构建流水线都进 watch 模式
pnpm typecheck    # 类型检查(app 和 node 两份 tsconfig)
pnpm test         # 单测:匹配、深合并、改写决策、编辑器草稿校验
pnpm verify       # 端到端验证(见下)
pnpm demo         # 只起实验台服务器

改完代码后在 chrome://extensions 点一下刷新按钮,再刷新页面。

测试分三层

层 覆盖什么 怎么跑
单测(92 条) 纯逻辑:URL 匹配、JSON 深合并、改写规格、编辑器草稿校验、body 美化与分词 pnpm test,不需要浏览器
端到端(119 条) 真实浏览器 + 真实网络:改写行为、XHR 生命周期、上报链路、面板交互与主题切换 pnpm verify
实验台 手动点着玩 pnpm demo

pnpm verify 值得多说一句。它会起本地服务器 + 无头 Chrome,加载构建产物里那份 inject.js / bridge.js(不是源码的另一份拷贝),用一个假的 chrome.runtime 扮演后台,然后跑真实的 fetch 和 XHR。所以它证明的是「你装进扩展的那个文件确实能工作」:

  • 改写后的响应,应用侧到底收到了什么(状态码、头、body、response.url)
  • 只改状态码时流有没有被缓冲——服务端发完第一块等 500ms,断言第一块能立刻读到
  • 伪造模式下请求到底有没有溜出去——靠服务端的路径计数证伪
  • XHR 的 readystatechange / load / loadend / abort / error 时序,以及 responseType 的语义
  • hook → postMessage → bridge → Port 这条上报链路

另有一个面板套件,把构建出来的 panel.js 在普通页面里真跑起来,点一遍主要交互(建规则、改字段、保存、启停、删除与撤销、清空与撤销、未保存拦截、一键复制、DevTools 切主题),断言 UI 反应和写进 storage / 后台消息的内容都对。

之所以不直接装扩展来测:Chrome 137 起,正式版(非 Chromium / 非 Chrome for Testing)移除了 --load-extension,命令行加载会静默失败——不报错,只是扩展根本没装上。上面这套「加载产物 + 打桩宿主」的做法绕开了这个限制,而且覆盖到的面更宽。

找不到 Chrome 时用 CHROME_PATH 指定可执行文件。


目录

src/
  shared/      规则模型、URL 匹配、JSON 合并、消息协议、storage 键(纯逻辑,可单测)
  content/
    inject.ts    MAIN world 入口
    fetch-hook.ts / xhr-hook.ts / rewriter.ts / context.ts
    bridge.ts    ISOLATED world 中继
  background/   service worker:配置下发 + 请求流缓冲
  devtools/     DevTools 面板(Vue 3 + Ant Design Vue,TSX)
    theme.ts     DevTools 主题检测 + AntD seed token + token → CSS 变量
    panel.css    布局与密集行;颜色全部读 CSS 变量,不写色值
demo/           实验台 + 自动化验证页 + 面板预览页(chrome-stub.js 打桩宿主)
tests/          单测
scripts/        build-content.mjs / watch.mjs / verify.mjs

content 与 devtools 之间没有直接依赖,两边都只认 shared/。面板也不 import 后台代码——它只认 shared/storage.ts 里那两个键,不关心后台怎么消费。


构建为什么是两条流水线

manifest 里声明的 content script 是经典脚本,不能是 ES module,所以不能用 Vite 那套输出(ESM + 公共 chunk,content script 加载不了)。于是:

  • Vite → devtools.html、panel.html、background.js(service worker 可以 type: module,能分块)
  • esbuild → content/inject.js、content/bridge.js(各自打成自包含的 IIFE)

pnpm verify 的预检会检查 manifest 引用的文件是否齐全,以及 content script 里有没有混进 ESM 语句——这两类是扩展最典型的静默翻车方式。

About

Chrome/Edge DevTools panel that captures fetch/XHR requests and rewrites their responses — status, headers, body, delay and failures — so you can test how your code handles the cases the backend never returns

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages