Home

Awesome

ajax-hook

npm version build status license

简介

ajax-hook是用于拦截浏览器 XMLHttpRequest 对象的轻量库,它可以在 XMLHttpRequest 对象发起请求之前、收到响应内容之后以及发生错误时获得处理权,通过它你可以提前对请求、响应以及错误进行一些预处理。Ajax-hook具有很好的兼容性,可以在任何支持ES5的浏览器上运行,因为它并不依赖 ES6 特性。

使用

安装

一个简单示例:

import { proxy } from "ajax-hook";
proxy({
    //请求发起前进入
    onRequest: (config, handler) => {
        console.log(config.url)
        handler.next(config);
    },
    //请求发生错误时进入,比如超时;注意,不包括http状态码错误,如404仍然会认为请求成功
    onError: (err, handler) => {
        console.log(err.type)
        handler.next(err)
    },
    //请求成功后进入
    onResponse: (response, handler) => {
        console.log(response.response)
        handler.next(response)
    }
})

现在,我们便拦截了浏览器中通过XMLHttpRequest发起的所有网络请求!在请求发起前,会先进入onRequest钩子,调用handler.next(config) 请求继续,如果请求成功,则会进入onResponse钩子,如果请求发生错误,则会进入onError 。我们可以更改回调钩子的第一个参数来修改修改数据。

点击查看更多项目示例

API介绍

proxy(proxyObject, [window])

拦截全局XMLHttpRequest

注意:proxy 是通过ES5的getter和setter特性实现的,并没有使用ES6 的Proxy对象,所以可以兼容ES5浏览器。

参数:

返回值: ProxyReturnObject

ProxyReturnObject 是一个对象,包含了 unProxyoriginXhr

钩子函数的签名

Handler

在钩子函数中,我们可以通过其第二个参数handler对象提供的方法来决定请求的后续流程,handler对象它有个方法:

  1. next(arg):继续进入后续流程;如果不调用,则请求链便会暂停,这种机制可以支持在钩子中执行一些异步任务。该方法在onResponse钩子中等价于resolve,在onError钩子中等价于reject
  2. resolve(response):调用后,请求后续流程会被阻断,直接返回响应数据,上层xhr.onreadystatechangexhr.onload会被调用。
  3. reject(err):调用后,请求后续流程会被阻断,直接返回错误,上层的xhr.onerrorxhr.ontimeoutxhr.onabort之一会被调用,具体调用哪个取决于err.type的值,比如我们设置err.type为"timeout",则xhr.ontimeout会被调用。

关于configresponseerr的结构定义请参考类型定义文件中的XhrRequestConfigXhrResponseXhrError

示例

const { unProxy, originXhr } = proxy({
    onRequest: (config, handler) => {
        if (config.url === 'https://aa/') {
            handler.resolve({
                config: config,
                status: 200,
                headers: {'content-type': 'text/text'},
                response: 'hi world'
            })
        } else {
            handler.next(config);
        }
    },
    onError: (err, handler) => {
        if (err.config.url === 'https://bb/') {
            handler.resolve({
                config: err.config,
                status: 200,
                headers: {'content-type': 'text/text'},
                response: 'hi world'
            })
        } else {
            handler.next(err)
        }
    },
    onResponse: (response, handler) => {
        if (response.config.url === location.href) {
            handler.reject({
                config: response.config,
                type: 'error'
            })
        } else {
            handler.next(response)
        }
    }
})

// 使用jQuery发起网络请求
function testJquery(url) {
    $.get(url).done(function (d) {
        console.log(d)
    }).fail(function (e) {
        console.log('hi world')
    })
}

//测试
testJquery('https://aa/');
testJquery('https://bb/');
testJquery(location.href)

// 取消拦截
unProxy();

运行后,控制台输出3次 "hi world"。

核心API - hook(hooks,[window])

Ajax-hook在1.x版本中只提供了一个核心拦截功能的库,在1.x中,我们通过hookAjax() 方法(2.x中改名为hook())实现了对XMLHttpRequest对象具体属性、方法、回调的细粒度拦截。而2.x中的proxy()方法则是基于hook的一个封装。

hook(Hooks,[window])

拦截全局XMLHttpRequest,此方法调用后,浏览器原生的XMLHttpRequest将会被代理,代理对象会覆盖浏览器原生XMLHttpRequest,直到调用unHook(...)后才会取消代理。

参数:

返回值: HookReturnObject

HookReturnObject 是一个对象,包含了 unHookoriginXhr

示例

下面我们看一下如何使用hook方法来拦截XMLHttpRequest对象:

import { hook } from "ajax-hook"
const { unHook, originXhr } = hook({
  //拦截回调
  onreadystatechange:function(xhr,event){
    console.log("onreadystatechange called: %O")
    //返回false表示不阻断,拦截函数执行完后会接着执行真正的xhr.onreadystatechange回调.
    //返回true则表示阻断,拦截函数执行完后将不会执行xhr.onreadystatechange. 
    return false
  },
  onload:function(xhr,event){
    console.log("onload called")
    return false
  },
  //拦截方法
  open:function(args,xhr){
    console.log("open called: method:%s,url:%s,async:%s",args[0],args[1],args[2])
    //拦截方法的返回值含义同拦截回调的返回值
    return false
  }
})

// 取消拦截
unHook();

这样拦截就生效了,拦截的全局的XMLHttpRequest,所以,无论你使用的是哪种JavaScript http请求库,它们只要最终是使用XMLHttpRequest发起的网络请求,那么拦截都会生效。下面我们用jQuery发起一个请求:

// 获取当前页面的源码(Chrome中测试)
$.get().done(function(d){
    console.log(d.substr(0,30)+"...")
})

输出:

> open called: method:GET,url:http://localhost:63342/Ajax-hook/demo.html,async:true
> onload called
> <!DOCTYPE html>
  <html>
  <head l...

可以看到我们的拦截已经成功。通过日志我们可以发现,在请求成功时,jQuery是回调的onload(),而不是onreadystatechange(),由于这两个回调都会在返回响应结果时被调用,所以为了保险起见,如果你要拦截网络请求的结果,建议同时拦截onload()onreadystatechange(),除非你清楚的知道上层库使用的具体回调。

代理xhr对象和原生xhr对象

原生xhr对象”即浏览器提供的XMLHttpRequest对象实例,而“代理xhr对象”指代理了“原生xhr对象”的对象,用户请求都是通过“代理xhr对象”发出,而“代理xhr对象”中又会调用“原生xhr对象”发起真正的网络请求。那么如何获取“代理xhr对象”和“原生xhr对象”呢?

  1. XHR事件回调钩子函数(以"on"开头的),如onreadystatechangeonload等,他们的拦截函数的第一个参数都为"原生xhr对象" (注意:这个和1.x版本有区别,1.x中为代理xhr对象)。
  2. XHR方法钩子函数(如opensend等),它们的第二个参数为原生xhr对象
  3. 所有回调函数钩子、方法钩子中,this代理xhr对象
  4. 原生xhr对象和代理对象都有获取彼此的方法和属性,具体见下面示例
hook({
  // 参数xhr为原生xhr对象
  onload:function(xhr, event){
    // this 为代理xhr对象
    // 原生xhr对象扩展了一个`getProxy()`方法,调用它可以获取代理xhr对象
    this==xhr.getProxy() //true
    //可以通过代理xhr对象的`xhr`属性获取原生xhr对象
    this.xhr==xhr //true
    console.log("onload called")
    return false
  },
})

注意

拦截XMLHttpRequest的属性

除了拦截XMLHttpRequest回调和方法外,也可以拦截属性的读/写操作。比如设置超时timeout,读取响应内容responseText等。下面我们通过两个示例说明。

ajax-hook.core.js

如果你只想使用hook(...)方法(不需要使用proxy()),我们提供了只包含hook()方法的核心库,你可以在dist目录找到名为ajax-hook.core.js的文件,直接使用它即可。

proxy(...) vs hook(...)

proxy()hook()都可以用于拦截全局XMLHttpRequest。它们的区别是:hook()的拦截粒度细,可以具体到XMLHttpRequest对象的某一方法、属性、回调,但是使用起来比较麻烦,很多时候,不仅业务逻辑需要散落在各个回调当中,而且还容易出错。而proxy()抽象度高,并且构建了请求上下文(请求信息config在各个回调中都可以直接获取),使用起来更简单、高效。

大多数情况下,我们建议使用proxy() 方法,除非proxy() 方法不能满足你的需求。

拦截iframe

显示指定iframe 的 window 对象即可,比如:

var iframeWindow = ...;
const { unProxy } = proxy({...},iframeWindow)
unProxy(iframeWindow)
//或
const { unHook } = hook({...},iframeWindow)
unHook(frameWindow)      

完整示例见:拦截iframe中的请求

注意:只能拦截同源的iframe页面(不能跨域)。

原理解析

通过ES5 getter和setter特性实现对 XMLHttpRequest 对象的代理: image

具体原理解析请查看:ajax-hook 原理解析

最后

本库在2016年首次开源,最初只是个人研究所用,源码50行左右,实现了一个Ajax拦截的核心,并非一个完整可商用的项目。自开源后,有好多人对这个黑科技比较感兴趣,于是我便写了篇介绍的博客,由于代码比较精炼,所以对于JavaScript不是很精通的同学可能看起来比较吃力,之后专门写了一篇原理解析的文章,现在已经有很多公司已经将 ajax-hook 用于线上项目中,直到我知道美团、滴滴也用到之后,笔者对此库进行了修改和扩展以增强其健壮性和实用性,现在已经达到商用的标准,本库也将进行技术支持。如果你喜欢,欢迎Star,如果有问题,欢迎提Issue, 如果你觉得对自己有用想请作者喝杯咖啡的话,请扫描下面二维码:

<img width=300 src="https://doc.flutterchina.club/images/pay29.jpeg" />