一个 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 在实例上附加状态,避免属性名冲突
  • 如果钩子修改了 url 或 method,会自动 reopen (重新调用原生 open)
  • responseProcessor 有幂等守卫,即使 onload 和 onreadystatechange 同时触发也只执行一次

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 小时前
Postman 被曝自动往 Claude Code 和 Codex 装插件
前端·后端·程序员
Csvn5 小时前
性能监控
前端
Csvn6 小时前
构建工具
前端
FRing036 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
kyriewen8 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒9 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
工控丫丫9 小时前
帆软看板中经常使用的公式函数及经常搭的组合
前端·javascript·数据库
jerrywus10 小时前
我用 GPT-6.1-sol 做了台会讲解的 3D 售货机:60 罐饮料,一口没喝上
前端·agent·claude
孟陬11 小时前
Foobar 技术周刊#159:当看不懂代码时,如何高效排障调试。脏页陷阱:一条只读查询,写入数 TB 数据。推测解码:大模型加速利器
前端·人工智能·后端
Zhao-Jichao11 小时前
AI Prompting for Everyone 课程笔记 - P17 图像生成 Imaging Generation
前端