一个 Hook 拦截所有 AJAX 请求:ajax-hooker 使用指南与原理

一个 Hook 拦截所有 AJAX 请求:ajax-hooker 使用指南与原理

开发调试时想改接口返回值?油猴脚本里想劫持网络请求?Chrome 扩展里想做请求监控?ajax-hooker 用一套 API 搞定 XMLHttpRequest 和 Fetch 的拦截,还支持流式响应。

痛点:为什么需要 AJAX 拦截?

作为前端开发者,你一定遇到过这些场景:

  • 接口联调:后端接口没开发完,想 mock 返回数据继续写页面
  • 线上调试:生产环境接口有 bug,想临时修改返回值定位问题
  • 请求监控:给所有请求统一加上 Token、追踪日志
  • 油猴脚本:修改第三方网站的接口行为,比如去广告、改数据展示
  • Chrome 扩展:开发网络调试工具

现有方案的问题是:XHR 和 Fetch 是两套完全不同的 API。你要么写两套拦截逻辑,要么找一个库帮你抹平差异。

ajax-hooker 就是做这件事的 ------ 一个钩子函数同时拦截 XHR 和 Fetch,统一的请求/响应数据结构,还支持流式响应拦截

快速上手

安装

bash 复制代码
# 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

三步完成拦截

typescript 复制代码
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/,你可以在前端无感切换:

typescript 复制代码
interceptor.hook((request) => {
  if (request.url.includes('/api/v1/')) {
    request.url = request.url.replace('/api/v1/', '/api/v2/');
  }
});

甚至可以切换域名:

typescript 复制代码
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 数据:

typescript 复制代码
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:请求日志 & 性能监控

typescript 复制代码
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:统一添加公共参数

typescript 复制代码
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 可以逐块拦截:

typescript 复制代码
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-ndjson
  • application/stream+json
  • application/jsonl
  • application/json-seq

场景 6:多个钩子协作

钩子按注册顺序链式执行,可以做职责分离:

typescript 复制代码
// 钩子 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:

javascript 复制代码
// 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 中需要声明:

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`
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 实例:

scss 复制代码
new XMLHttpRequest()
  → proxyXhr() 构造函数
    → 创建真实 xhr 实例
    → 返回 Proxy(xhr, handler)

Proxy 的 get trap 拦截了属性读取:

  • 方法调用open/send/setRequestHeader):在调用原生方法前后执行钩子逻辑
  • 响应属性response/responseText/status):在响应处理完成后返回可能被修改的值
  • 事件监听addEventListener/removeEventListener):包装响应事件的 listener,确保在触发前执行响应处理

关键设计点:

  • 使用 Symbol 在实例上附加状态,避免属性名冲突
  • 如果钩子修改了 urlmethod,会自动 reopen (重新调用原生 open
  • responseProcessor 有幂等守卫,即使 onloadonreadystatechange 同时触发也只执行一次

Fetch 拦截

替换全局 window.fetch 为代理函数:

scss 复制代码
fetch(url, options)
  → proxyFetch()
    → 规范化请求参数
    → 执行钩子链
    → 调用原生 fetch
    → Proxy 包裹 Response

Fetch 拦截更复杂的地方在于:

  • fetch() 支持三种调用形式(string / URL / Request 对象),需要统一规范化
  • 使用 sourceMap 追踪每个属性的来源(Request 对象 / options / 默认值),确保还原请求时的精确性
  • 响应处理分两条路径:普通响应(并行解析 5 种格式)和流式响应(TransformStream 管道)

为什么用 Proxy 而不是直接覆写原型?

常见的 XHR 拦截方案是修改 XMLHttpRequest.prototype 上的方法。ajax-hooker 选择 Proxy 的原因:

  1. 更细粒度的控制 :Proxy 可以拦截属性读取(get)和写入(set),不仅仅是方法调用
  2. 不污染原型链:每个实例独立代理,互不干扰
  3. 响应属性拦截response/responseText 是只读属性,覆写原型无法拦截它们的 getter,而 Proxy 的 get trap 可以
  4. 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 项目内部
油猴/扩展 适合 不适合 不适合
拦截第三方代码

项目信息


写在最后

ajax-hooker 目前还在持续迭代中,后续计划支持 EventSource (SSE) 拦截等更多能力。

如果你在使用过程中遇到问题,欢迎到 GitHub Issues 提反馈;如果这个库对你有帮助,希望你能去 GitHub 仓库 点个 Star,这对开源作者来说是最大的鼓励,也能让更多有需要的人看到这个项目。感谢!

相关推荐
摸鱼的春哥2 小时前
惊!黑客靠AI把墨西哥政府打穿了,海量数据被黑
前端·javascript·后端
小兵张健2 小时前
Playwright MCP 截图标注方案调研(推荐方案1)
前端·javascript·github
小兵张健3 小时前
AI 页面与交互迁移流程参考
前端·ai编程·mcp
小兵张健3 小时前
掘金发布 SOP(Codex + Playwright MCP + Edge)
前端·mcp
小兵张健4 小时前
Mac 上 Antigravity 无法调用 browser_subagent?一次 400 报错排查记录
前端
张拭心4 小时前
编程最强的模型,竟然变成了国产的它
前端·ai编程
爱勇宝5 小时前
2026一人公司生存指南:用AI大模型,90天跑出你的第一条现金流
前端·后端·架构
fe小陈5 小时前
简单高效的状态管理方案:Hox + ahooks
前端
我叫黑大帅5 小时前
Vue3和Uniapp的爱恨情仇:小白也能懂的跨端秘籍
前端·javascript·vue.js