新闻详情

新闻详情

首页 / 资讯中心 / 详情

ajax-hooker实战:统一拦截所有AJAX请求的原理、用法与踩坑

发布时间:2026/9/30 7:43:57来源:尧图网络
ajax-hooker实战:统一拦截所有AJAX请求的原理、用法与踩坑
1. 拦截所有 AJAX 请求这件小事为什么值得认真做在浏览器里调试一个别人写好的埋点 SDK 时我遇到了一个特别憋屈的需求后端要求所有/api开头的请求都带一个动态签名参数并且把响应里的密文统一解密。SDK 的代码是压缩产物我改不动源码业务请求又都走各自封装的request方法想一个一个打补丁又怕漏。那段时间我几乎一整天都在翻浏览器 Network 面板人工把 cookie、时间戳、随机数拼到请求里效率低到让人抓狂。后来我换了个思路与其在业务层到处打点不如在浏览器网络请求的源头做文章。几乎所有 AJAX 请求最终都要经过XMLHttpRequest或fetch这两个原生入口只要在应用加载早期把这两个入口换掉就能统一拦截和改写所有请求——这就是ajax-hooker的核心价值。它解决的从来不是某个接口怎么调而是整个前端所有异步请求我能不能有一个最后的、全局的、可控的抓手。读到这里的读者我默认你至少有过写XMLHttpRequest或fetch的经验并且被类似这些问题困扰过第三方 SDK 的请求不受控制、接口联调需要频繁 mock、线上接口报错只有 Network 面板能看到。这篇文章会把 ajax-hooker 的原理、API 用法、以及我在真实项目里踩过的坑一次讲透。1.1 先看三种拦截姿势的取舍在决定用 ajax-hooker 之前我认为值得先梳理一下前端能用的全局拦截方案因为很多人一上来就在业务层封装 axios结果管不到第三方库。拦截方案能管到第三方 SDK能改请求头/参数能改响应体主要缺点业务层封装 request 方法否是是管不到绕开封装的请求且多个 SDK 各管各Service Worker是受限是需要在 HTTPS 下运行生命周期复杂改 header 能力有限调试成本高XHR/fetch 原型代理ajax-hooker是是是需技巧需要懂底层 API响应体读取有一处大坑我自己在项目里最终选了原型代理原因很朴素它处在最底层不管是 axios、umi-request 还是某个闭源 SDK只要它们最终走的是浏览器原生异步接口就都会经过我这个钩子。这不是银弹但对于全局统一处理而言它是最接近银弹的位置。1.2 但它不是万能的先认清边界必须把丑话说在前面ajax-hooker 类工具只拦截XMLHttpRequest和fetch这两个入口下面这些请求它管不到WebSocket、EventSource、navigator.sendBeacon()是完全独立的网络通道img、script、link等标签发起的资源请求不走 JS API小程序环境里wx.request是独立网络栈浏览器里的 hook 根本不生效Service Worker 内部发起的 fetch 也取决于你在哪个 global 作用域打了补丁。另外别指望拿它当安全防线。前端 hook 只能约束有意配合的代码对恶意脚本、xss 注入后的行为没有防御价值它更像是开发调试、联调 mock、全局监控这类场景的工程手段。2. 原理拆解给浏览器 XHR 换一套代理内核理解了它适合做什么我们再看它究竟是怎么做到的。很多人一听hook就觉得很玄其实拆开来看就两件事方法替换和属性替换。2.1 方法替换open 和 send 被换掉之后XMLHttpRequest的请求动作最终靠原型上的open()和send()触发。要拦截最简单粗暴的方式就是替换原型方法const originalOpen XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open function (method, url, async true, user null, pass null) { console.log([hook] open -, method, url); // 在这里可以收集参数甚至改掉 url this.__hookMeta { method, url, async, user, pass }; return originalOpen.call(this, method, url, async, user, pass); };注意这里为什么用originalOpen.call(this, ...)而不是直接originalOpen(...)原型方法在被实例调用时this指向当前的 xhr 实例丢掉this会导致内部状态错乱。替换send也是同一个套路改 body、改 header 都可以在send被调用前完成。2.2 属性替换responseText 怎么变可拦截了方法替换只解决了一半问题。能不能在响应阶段拿到数据、甚至改写后再交给业务代码取决于responseText、status、readyState这些属性怎么处理。第一个坑是xhr.responseText是一个只有 getter、没有 setter 的原生访问器属性你直接赋值在严格模式下会抛TypeError。第二个坑是每个 xhr 实例读到的responseText都来自原型上的 getter你不能给单个实例单独塞一个值。所以 ajax-hooker 这类库的经典做法是用Object.defineProperty在XMLHttpRequest.prototype上重新定义responseText的 getter同时用一个WeakMap把每个实例应当返回的改写内容存起来const overrideMap new WeakMap(); const origDesc Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, responseText); Object.defineProperty(XMLHttpRequest.prototype, responseText, { configurable: true, enumerable: origDesc.enumerable, get() { const override overrideMap.get(this); if (override ! undefined) return override; return origDesc.get.call(this); }, });这就是整件事最精巧的部分用 WeakMap 建立实例到改写值的映射getter 优先返回映射里的内容读起来跟原生一模一样。只要在 readyState 变成 4、代码真正去读响应之前把想要返回的内容放进overrideMap业务层几乎无感知地拿到被改过的响应。顺带一提如果用封装库提供的onResponse回调修改responseText库内部帮你处理的也是这套逻辑你不需要手写defineProperty。2.3 请求配置对象与 handler.next 的洋葱模型在用 ajax-hooker 时你写的回调通常长这样hookXhr({ onRequest(config, handler) { // config: { url, method, body, async, headers } config.headers[X-Foo] bar; handler.next(config); }, onResponse(response, handler) { // response: { status, statusText, responseText, response } handler.next(response); }, });handler.next(config)的意思是我已经处理完了请按修改后的配置继续往下走。这跟 Express/Koa 中间件的洋葱模型很像请求从外向内流过一层层处理器响应从内向外再流回来。你不调next请求就会卡在当前这一层你调next时传入新对象就完成了拦截并改写。如果不喜欢洋葱模型很多版本的封装也提供了直接终止流程的快捷方式比如在onRequest里直接返回一个伪造的响应对象后面我会用 mock 场景演示。具体 API 名不同版本略有差异但请求前改写、响应后改写、必须显式放行这三个心智模型是通用的。2.4 fetch 为什么比 XHR 难处理fetch只返回一个 Promise拦截它看起来只是包一层const originalFetch window.fetch; window.fetch async function (...args) { const res await originalFetch.apply(this, args); // 想读一下 body const text await res.text(); console.log([hook] response -, text); return res; };上面这段代码会让业务拿到的res.body变成 null因为Response 对象是一次的读了一次之后后面再调用res.json()或res.text()都只会拿到 null。正确的做法是先clone()再读window.fetch async function (...args) { const res await originalFetch.apply(this, args); const cloned res.clone(); cloned.text().then((body) { console.log([hook] fetch response body -, body); }); return res; };clone()也不是万能的它依然要求响应体没有被消费过而且如果你的拦截逻辑里修改了响应内容回到业务代码时还得用new Response(modifiedBody, res)重新构造一个。这也是为什么很多封装库在 fetch 拦截上建议能不改 body 就不改 body只做采集和上报。3. 上手实战三个可以直接抄的拦截用例讲完原理下面进入实际操作。我以 ajax-hooker 的通用 API 为例代码里的回调名和你安装版本的 README 可能有细微出入但核心流程可以直接套。3.1 安装与挂载时机npm install ajax-hooker # 或者 yarn add ajax-hooker挂载时机非常关键必须在业务代码发起任何请求之前完成 hook。以 Vue 项目为例我习惯在main.js的最顶部先于createApp和router注册import { hookXhr, hookFetch } from ajax-hooker; import App from ./App.vue; hookXhr({ onRequest, onResponse }); hookFetch({ onRequest, onResponse }); createApp(App).mount(#app);如果用 React就放在index.jsx入口文件最前面。如果你把这个初始化放到了路由beforeEach里那大概率会漏掉首屏请求。同样重要的还有幂等控制hook 函数最好配合一个全局标记避免热更新或者模块被重复执行时把钩子叠加两层后面我会专门讲。3.2 用例一给所有请求注入鉴权头和请求 ID这个场景最常见。业务里可能有一百个请求入口但如果拦截层统一注入就只需要写一处function generateRequestId() { return req_${Date.now()}_${Math.random().toString(36).slice(2, 10)}; } const onRequest (config, handler) { config.headers config.headers || {}; config.headers[Authorization] Bearer ${getToken()}; config.headers[X-Request-Id] generateRequestId(); handler.next(config); };这里有个细节值得注意config.headers可能在某些版本里不存在也可能已经被设置为Headers实例而不是普通对象所以先做|| {}兜底统一成可书写的形式再赋值。如果只是给url追加密文参数同样在onRequest里拼接后handler.next(config)这和给 ajax 请求参数赋值的场景是一回事。3.3 用例二请求体编码格式改写与参数赋值有些后端接口只认application/x-www-form-urlencoded前端如果传 JSON 对象就会报 415。与其每个调用方单独处理不如在钩子里一次收编const onRequest (config, handler) { const { body, headers } config; if ( body typeof body object !(body instanceof FormData) !(body instanceof URLSearchParams) ) { // 把对象转成表单编码 config.body new URLSearchParams(body).toString(); } const contentType headers?.[Content-Type] || ; if (typeof config.body string !contentType) { config.headers[Content-Type] application/x-www-form-urlencoded;charsetUTF-8; } handler.next(config); };这也是我当时改请求编码格式需求的标准答案。要注意FormData和URLSearchParams会被new URLSearchParams(body)误伤所以要先排除它们。如果你需要给某个接口的参数统一叠加默认值就是在config.body或config.url上做同样的合并再 next。3.4 用例三接口异常采集与自动重试统一上报网络错误是拦截层最值得做的投资。我通常会在onResponse里做状态码过滤const retryCountMap new Map(); const onResponse (response, handler) { const { status, config } response; if (status 500 status 600) { const key config.url; const count retryCountMap.get(key) || 0; if (count 2) { retryCountMap.set(key, count 1); // 这里重新发起一次请求并让本次直接吞掉 fetch(config.url, { method: config.method, headers: config.headers, body: config.body, }); return; // 不调用 handler.next避免把错误响应传给业务层 } } handler.next(response); };我特意写了一个限制重试次数的小Map因为直接无条件重试 POST 接口是有风险的可能在支付、下单这类场景造成重复提交。如果业务上确实需要重试我建议把所有非幂等接口提前拉进一个白名单只对 GET、查询类接口做自动重试其他一律只上报。4. 最后一公里兼容性与坑位盘点把钩子挂到生产环境之前有几处看起来没问题、上线就翻车的坑位必须先排掉。4.1 响应体被吞掉的两种版本XHR 场景里如果你在onResponse里只做采集但库为了让回调拿到responseText已经强制读了一次业务代码再读时可能发现响应为空fetch 场景则是我前面讲的Response一次性问题。我的规避策略是在onResponse里优先使用库给你的response对象而不是自己再去读xhr.responseTextfetch 拦截时如果只是做采集一律clone()如果必须改写响应体那就做好改写后重新构造 Response的心理准备并用response.ok、response.status把原状态透传下去。4.2 与 axios 拦截器叠加时的执行顺序很多人会问我业务里已经用了 axios还需要 ajax-hooker 吗其实两者不冲突axios 的拦截器作用在你的代码 - axios 实例这段链路而 ajax-hooker 作用在axios 实例 - 浏览器原生网络这段链路。顺序上同一个请求先经过 ajax-hooker 的onRequest再进入 axios 内部的请求拦截器响应返回时则反过来axios 响应拦截器先处理ajax-hooker 的onResponse后处理。理解这个顺序有个好处如果你在 ajax-hooker 里给config.headers加了头axios 的拦截器能看到并可能覆盖它反过来axios 拦截器加的头ajax-hooker 在onResponse阶段也能检查到。两者不存在谁取代谁的关系但要注意层级越高越晚生效改配置时要清楚自己是在哪一层说话。4.3 重复 hook、卸载与内存泄漏hookXhr这类 API 通常返回一个还原函数开发环境热更新时尤其要记得调用// 开发环境热更新时先还原再重新挂 const restoreXhr hookXhr({ ... }); export default restoreXhr;否则模块被 HMR 重新执行时钩子会套在之前的钩子外面形成俄罗斯套娃。你可能会看到请求被打印了三四次、响应被改写了多次。另一个容易漏的是WeakMap虽然能自动回收实例键但如果你在 getter 或者回调里意外持有大对象引用比如把整个 response 存进了全局数组照样会有内存增长。线上采集错误时建议只保留url、status、error.message、时间戳不要把整个responseText堆进内存。4.4 环境边界小程序、Web Worker、Electron最后列一下我在不同环境里实测的结论小程序微信/支付宝等不可以用wx.request是独立网络栈浏览器 XHR/fetch 钩子对它无效Web Workerworker 有自己的globalThis如果你只想拦截主线程请求别在 worker 里加载这个库想统一处理时要分别 patchwindow.XMLHttpRequest和self.XMLHttpRequestElectron 渲染进程原理上可用但要注意webSecurity配置和 CSP 设置部分环境不允许你重定义内置原型属性严格 CSP 环境如果页面script-src限制了 unsafe-inline / unsafe-eval某些打包产物可能加载失败优先用符合 CSP 的构建版本。我在生产项目里的最终落地形态是用一个networkInterceptor模块统一管理 hook 的注册、注销和开关所有回调都带__debug__环境判断日志只在本地开发环境输出线上只保留错误上报和请求耗时统计。这样即使未来某个第三方库升级导致兼容问题我也可以随时卸载钩子回到裸跑状态把影响面控制在一个可回滚的开关上。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

强化学习驱动的机器人认知情感交互模型:从状态建模到奖励塑形 2026/9/30 8:33:18

强化学习驱动的机器人认知情感交互模型:从状态建模到奖励塑形

简介:面向人机交互与机器人情感计算研究者的专题文档,系统探讨如何借助强化学习构建具备认知情感交互能力的机器人模型,解决传统单轮情感模型忽视上下文情境与情感长期影响的问题。资源为1个docx文档,压缩包约393KB,内…

阅读更多 →
基于粒子群算法的夏季综合能源系统冷电联调优化调度 2026/9/30 8:33:18

基于粒子群算法的夏季综合能源系统冷电联调优化调度

1. 夏季的冷负荷和电负荷为什么不能当两个独立问题处理每年六到九月,很多做园区综合能源管理的朋友都会遇到同一个现象:电网的峰时电价还没到,办公楼和工厂的空调负荷就已经把配电容量顶到了上限;等到光伏满发的时候,冷…

阅读更多 →
PDF格式解析与工程实践:从底层结构到OCR、压缩和避坑 2026/9/30 8:33:18

PDF格式解析与工程实践:从底层结构到OCR、压缩和避坑

PDF 这个后缀名,几乎是每一个用电脑的人都绕不开的东西,但真要问一句“PDF 到底是什么”,能说清楚的人并不多。有人把它当成“不会乱码的 Word”,有人把它当成“扫描件的容器”,还有人一遇到 PDF 就只会截图贴进文档。…

阅读更多 →
花卉种类识别实战:基于ResNet18的迁移学习与训练避坑指南 2026/9/30 8:33:18

花卉种类识别实战:基于ResNet18的迁移学习与训练避坑指南

简介:围绕深度学习模型在花卉种类识别中应用的期刊论文PDF,面向计算机视觉、机器学习方向的研究者、学生及竞赛团队,聚焦解决花卉这类非刚性物体因形态多样而难以自动分类的问题。论文基于ImageNet数据库中的花卉图像样本完成训练与测试&…

阅读更多 →
Linux常用命令详解:文件操作、进程排查与日志检索速查手册 2026/9/30 8:33:18

Linux常用命令详解:文件操作、进程排查与日志检索速查手册

简介:Linux系统常用命令与操作详解是一份面向终端操作员、技术支持工程师及Linux初学者的速查型参考资料,覆盖文件与目录管理、系统状态查看、进程控制、网络配置、压缩解压等核心场景,可帮助读者按需查找命令,提升Shell操作效率。…

阅读更多 →
基于S7-200PLC与组态王的自动灌溉系统设计解析 2026/9/30 8:33:11

基于S7-200PLC与组态王的自动灌溉系统设计解析

西门子S7-200这套PLC,按现在的眼光看确实有点老了,CPU处理速度不算快,通讯速率也只有9.6k到187.5k,但在自动灌溉这种环境不苛刻、点数不多、对成本敏感的场景里,它反而是一套非常可靠且容易上手的方案。我最近整理了一…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉