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

0 阅读6分钟

一个 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-ndjson
  • application/stream+json
  • application/jsonl
  • application/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'请求类型
methodstringHTTP 方法
urlstring请求 URL
headersHeaders请求头(标准 Headers 对象)
dataany请求体
response(resp) => void响应回调,在响应返回时触发
onStreamChunk(chunk) => string | void流式响应逐块回调(仅 Fetch)
responseTypestringXHR 专属
withCredentialsbooleanXHR 专属
timeoutnumberXHR 专属

Response 对象属性

属性适用于可写说明
status两者HTTP 状态码
statusText两者状态文本
headers两者响应头
finalUrl两者最终 URL(重定向后)
responseXHR原始响应
responseTextXHR文本响应
responseXMLXHRXML 响应
jsonFetchJSON 数据
textFetch文本数据
arrayBufferFetchArrayBuffer 数据
blobFetchBlob 数据
formDataFetchFormData 数据
okFetch是否成功 (2xx)
redirectedFetch是否重定向

实现原理简述

如果你对底层实现感兴趣,这里简单介绍核心机制:

XHR 拦截

使用 ES6 Proxy 包裹原生 XMLHttpRequest 实例:

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 为代理函数:

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-hookerMock Service Worker (MSW)axios interceptors
拦截 XHR是 (Service Worker)仅 axios
拦截 Fetch是 (Service Worker)
修改响应是(直接修改)是(需要定义 handler)是(仅 axios)
流式响应
运行时依赖0msw + worker 文件内置于 axios
使用场景运行时拦截测试/开发 mockaxios 项目内部
油猴/扩展适合不适合不适合
拦截第三方代码

项目信息


写在最后

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

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