一个 Hook 拦截所有 AJAX 请求:ajax-hooker 使用指南与原理
开发调试时想改接口返回值?油猴脚本里想劫持网络请求?Chrome 扩展里想做请求监控?
ajax-hooker用一套 API 搞定 XMLHttpRequest 和 Fetch 的拦截,还支持流式响应。
痛点:为什么需要 AJAX 拦截?
作为前端开发者,你一定遇到过这些场景:
- 接口联调:后端接口没开发完,想 mock 返回数据继续写页面
- 线上调试:生产环境接口有 bug,想临时修改返回值定位问题
- 请求监控:给所有请求统一加上 Token、追踪日志
- 油猴脚本:修改第三方网站的接口行为,比如去广告、改数据展示
- Chrome 扩展:开发网络调试工具
现有方案的问题是:XHR 和 Fetch 是两套完全不同的 API。你要么写两套拦截逻辑,要么找一个库帮你抹平差异。
ajax-hooker 就是做这件事的 —— 一个钩子函数同时拦截 XHR 和 Fetch,统一的请求/响应数据结构,还支持流式响应拦截。
快速上手
安装
# npm
npm install ajax-hooker
# pnpm
pnpm add ajax-hooker
# CDN(IIFE 全局变量 AjaxHooker)
# https://unpkg.com/ajax-hooker
# https://cdn.jsdelivr.net/npm/ajax-hooker
三步完成拦截
import AjaxInterceptor from 'ajax-hooker';
// 1. 获取单例
const interceptor = AjaxInterceptor.getInstance();
// 2. 注入拦截(替换原生 XMLHttpRequest 和 fetch)
interceptor.inject();
// 3. 注册钩子
interceptor.hook((request) => {
console.log(`[${request.type}] ${request.method} ${request.url}`);
// 修改请求:统一添加 Token
request.headers.set('Authorization', 'Bearer my-token');
// 修改响应
request.response = (resp) => {
if (request.url.includes('/api/user')) {
resp.json = { name: '测试用户', id: 1 };
}
};
});
这段代码会拦截页面上所有的 XHR 和 Fetch 请求。不管第三方库用的是 axios(基于 XHR)还是原生 fetch,都会被捕获。
实战场景
场景 1:接口版本迁移
后端正在将 /api/v1/ 迁移到 /api/v2/,你可以在前端无感切换:
interceptor.hook((request) => {
if (request.url.includes('/api/v1/')) {
request.url = request.url.replace('/api/v1/', '/api/v2/');
}
});
甚至可以切换域名:
interceptor.hook((request) => {
if (request.url.includes('old-api.example.com')) {
request.url = request.url.replace(
'old-api.example.com',
'new-api.example.com'
);
}
});
场景 2:Mock 接口返回
后端接口还没好?直接拦截返回 mock 数据:
interceptor.hook((request) => {
request.response = (resp) => {
if (request.url.includes('/api/products')) {
// 对 XHR 修改 response/responseText
resp.response = JSON.stringify([
{ id: 1, name: '商品A', price: 99 },
{ id: 2, name: '商品B', price: 199 },
]);
resp.responseText = resp.response;
// 对 Fetch 修改 json
resp.json = [
{ id: 1, name: '商品A', price: 99 },
{ id: 2, name: '商品B', price: 199 },
];
resp.status = 200;
resp.statusText = 'OK';
}
};
});
场景 3:请求日志 & 性能监控
interceptor.hook((request) => {
const startTime = Date.now();
request.response = (resp) => {
const duration = Date.now() - startTime;
console.log(
`[${request.type.toUpperCase()}] ${request.method} ${request.url}`,
`| ${resp.status} | ${duration}ms`
);
// 慢接口报警
if (duration > 3000) {
console.warn(`慢接口: ${request.url} 耗时 ${duration}ms`);
}
};
});
场景 4:统一添加公共参数
interceptor.hook((request) => {
const url = new URL(request.url);
url.searchParams.set('app_version', '2.0.0');
url.searchParams.set('platform', 'web');
request.url = url.toString();
});
场景 5:拦截流式响应(SSE / NDJSON)
现在 AI 场景越来越多,接口经常返回流式数据。ajax-hooker 可以逐块拦截:
interceptor.hook((request) => {
if (request.url.includes('/api/chat/stream')) {
// onStreamChunk 会在每个数据块到达时被调用
request.onStreamChunk = (chunk) => {
console.log(`chunk #${chunk.index}:`, chunk.text);
// 返回修改后的文本(会替换原始数据)
return chunk.text.replace('敏感词', '***');
// 返回 void/undefined 则保持原数据不变
};
request.response = (resp) => {
console.log('流开始,status:', resp.status);
};
}
});
自动检测的流式 Content-Type 包括:
text/event-stream(SSE)application/x-ndjsonapplication/stream+jsonapplication/jsonlapplication/json-seq
场景 6:多个钩子协作
钩子按注册顺序链式执行,可以做职责分离:
// 钩子 1:认证
interceptor.hook((request) => {
request.headers.set('Authorization', 'Bearer token-xxx');
});
// 钩子 2:日志
interceptor.hook((request) => {
console.log('请求已携带 Auth:', request.headers.get('Authorization'));
});
// 钩子 3:只拦截 Fetch 的响应
interceptor.hook((request) => {
if (request.type === 'fetch') {
request.response = (resp) => {
// 只处理 fetch 响应
};
}
});
场景 7:在 Chrome 扩展中使用
Chrome 扩展的 Content Script 运行在隔离环境中,无法直接修改页面的 XMLHttpRequest。需要将代码注入到页面的 main world:
// content.js
const script = document.createElement('script');
script.src = chrome.runtime.getURL('vendor/ajax-hooker.iife.js');
script.onload = () => {
const init = document.createElement('script');
init.textContent = `
const interceptor = AjaxHooker.getInstance();
interceptor.inject();
interceptor.hook((request) => {
// 你的拦截逻辑
});
`;
document.documentElement.appendChild(init);
init.remove();
};
document.documentElement.appendChild(script);
script.remove();
manifest.json 中需要声明:
{
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}],
"web_accessible_resources": [{
"resources": ["vendor/ajax-hooker.iife.js"],
"matches": ["<all_urls>"]
}]
}
API 速查
| 方法 | 说明 |
|---|---|
AjaxInterceptor.getInstance() | 获取单例实例 |
interceptor.inject(type?) | 注入拦截。type 可选 'xhr' / 'fetch',不传则两者都注入 |
interceptor.uninject(type?) | 移除拦截,恢复原生对象 |
interceptor.hook(fn, type?) | 注册钩子函数。type 可选,用于只拦截特定类型 |
interceptor.unhook(fn?, type?) | 移除钩子。不传 fn 则清空所有钩子 |
Request 对象属性
| 属性 | 类型 | 可写 | 说明 |
|---|---|---|---|
type | 'xhr' | 'fetch' | 否 | 请求类型 |
method | string | 是 | HTTP 方法 |
url | string | 是 | 请求 URL |
headers | Headers | 是 | 请求头(标准 Headers 对象) |
data | any | 是 | 请求体 |
response | (resp) => void | 是 | 响应回调,在响应返回时触发 |
onStreamChunk | (chunk) => string | void | 是 | 流式响应逐块回调(仅 Fetch) |
responseType | string | 是 | XHR 专属 |
withCredentials | boolean | 是 | XHR 专属 |
timeout | number | 是 | XHR 专属 |
Response 对象属性
| 属性 | 适用于 | 可写 | 说明 |
|---|---|---|---|
status | 两者 | 是 | HTTP 状态码 |
statusText | 两者 | 是 | 状态文本 |
headers | 两者 | 否 | 响应头 |
finalUrl | 两者 | 否 | 最终 URL(重定向后) |
response | XHR | 是 | 原始响应 |
responseText | XHR | 是 | 文本响应 |
responseXML | XHR | 是 | XML 响应 |
json | Fetch | 是 | JSON 数据 |
text | Fetch | 是 | 文本数据 |
arrayBuffer | Fetch | 是 | ArrayBuffer 数据 |
blob | Fetch | 是 | Blob 数据 |
formData | Fetch | 是 | FormData 数据 |
ok | Fetch | 否 | 是否成功 (2xx) |
redirected | Fetch | 否 | 是否重定向 |
实现原理简述
如果你对底层实现感兴趣,这里简单介绍核心机制:
XHR 拦截
使用 ES6 Proxy 包裹原生 XMLHttpRequest 实例:
new XMLHttpRequest()
→ proxyXhr() 构造函数
→ 创建真实 xhr 实例
→ 返回 Proxy(xhr, handler)
Proxy 的 get trap 拦截了属性读取:
- 方法调用(
open/send/setRequestHeader):在调用原生方法前后执行钩子逻辑 - 响应属性(
response/responseText/status):在响应处理完成后返回可能被修改的值 - 事件监听(
addEventListener/removeEventListener):包装响应事件的 listener,确保在触发前执行响应处理
关键设计点:
- 使用
Symbol在实例上附加状态,避免属性名冲突 - 如果钩子修改了
url或method,会自动 reopen(重新调用原生open) responseProcessor有幂等守卫,即使onload和onreadystatechange同时触发也只执行一次
Fetch 拦截
替换全局 window.fetch 为代理函数:
fetch(url, options)
→ proxyFetch()
→ 规范化请求参数
→ 执行钩子链
→ 调用原生 fetch
→ Proxy 包裹 Response
Fetch 拦截更复杂的地方在于:
fetch()支持三种调用形式(string / URL / Request 对象),需要统一规范化- 使用 sourceMap 追踪每个属性的来源(Request 对象 / options / 默认值),确保还原请求时的精确性
- 响应处理分两条路径:普通响应(并行解析 5 种格式)和流式响应(TransformStream 管道)
为什么用 Proxy 而不是直接覆写原型?
常见的 XHR 拦截方案是修改 XMLHttpRequest.prototype 上的方法。ajax-hooker 选择 Proxy 的原因:
- 更细粒度的控制:Proxy 可以拦截属性读取(
get)和写入(set),不仅仅是方法调用 - 不污染原型链:每个实例独立代理,互不干扰
- 响应属性拦截:
response/responseText是只读属性,覆写原型无法拦截它们的 getter,而 Proxy 的gettrap 可以 - instanceof 兼容:通过
copyNativePropsAndPrototype确保xhr instanceof XMLHttpRequest仍然返回true
与其他方案的对比
| 特性 | ajax-hooker | Mock Service Worker (MSW) | axios interceptors |
|---|---|---|---|
| 拦截 XHR | 是 | 是 (Service Worker) | 仅 axios |
| 拦截 Fetch | 是 | 是 (Service Worker) | 否 |
| 修改响应 | 是(直接修改) | 是(需要定义 handler) | 是(仅 axios) |
| 流式响应 | 是 | 否 | 否 |
| 运行时依赖 | 0 | msw + worker 文件 | 内置于 axios |
| 使用场景 | 运行时拦截 | 测试/开发 mock | axios 项目内部 |
| 油猴/扩展 | 适合 | 不适合 | 不适合 |
| 拦截第三方代码 | 是 | 是 | 否 |
项目信息
- GitHub: github.com/Arktomson/a…
- npm: www.npmjs.com/package/aja…
- CDN:
https://unpkg.com/ajax-hooker/https://cdn.jsdelivr.net/npm/ajax-hooker - License: MIT
写在最后
ajax-hooker 目前还在持续迭代中,后续计划支持 EventSource (SSE) 拦截等更多能力。
如果你在使用过程中遇到问题,欢迎到 GitHub Issues 提反馈;如果这个库对你有帮助,希望你能去 GitHub 仓库 点个 Star,这对开源作者来说是最大的鼓励,也能让更多有需要的人看到这个项目。感谢!