ajax-hook.js 剖析

问题

今天某群里有人提了这样一个问题
2.jpg
1.jpg

就是在chrome调试器明明打印出来了xhr,能看到response的值,而在代码里面直接打印response却看不见这个值

结论

直接上结论,console.log打印的是xhr的引用,也就是说,open在打印的时候,xhr其实是对的,xhr.response也确实是空字符串的。但是后来请求完毕过后,xhr.response这个值被修改了,接着才是点开chrome dev-tool里面的XMLHttpRequest,所以这时候才能看到response,产生了一种错觉。

ajax-hook.js

发现这个库代码写得十分简洁
这里是demo代码

    hookAjax({
        onreadystatechange:function(xhr){
            console.log("onreadystatechange called: %O",xhr)
            //return true
        },
        onload:function(xhr){
            console.log("onload called: %O",xhr)
            xhr.responseText="hook"+xhr.responseText;
            //return true;
        },
        open:function(arg,xhr){
          console.log("open called: method:%s,url:%s,async:%s",arg[0],arg[1],arg[2],xhr)
          arg[1]+="?hook_tag=1";
          //统一添加请求头
        },
        send:function(arg,xhr){
          console.log("send called: %O",arg[0])
          xhr.setRequestHeader("_custom_header_","ajaxhook")
        },
        setRequestHeader:function(arg,xhr){
            console.log("setRequestHeader called!",arg)
        }
    })

对于原生的XMLHttpRequest对象的一些方法,比如open,send都是支持“重载”的(暂时用这个词)
也就是说它能够控制ajax的报文

原理其实重新了XMLHttpRequest对象

// 用._ahrealxhr保存原始的XMLHttpRequest对象
window._ahrealxhr = window._ahrealxhr || XMLHttpRequest

// 重写XMLHttpRequest对象
XMLHttpRequest = function () {

    // 每次new XMLHttpRequest(假设创建对象为A)的时候
    // 都偷偷地new一个原始的XMLHttpRequest,挂在A.xhr这个变量里面
    this.xhr = new window._ahrealxhr;

    // 遍历XMLHttpRequest的属性,都复制一份到对象A的属性上
    for (var attr in this.xhr) {
        var type = "";
        try {
            type = typeof this.xhr[attr]
        } catch (e) {}
        if (type === "function") {

            // "成员方法"(暂时这么叫)
            this[attr] = hookfun(attr);
        } else {

           // "成员变量"(也是暂时这么叫)
            Object.defineProperty(this, attr, {
                get: getFactory(attr),
                set: setFactory(attr)
            })
        }
    }
}

一、 接下来,看看 "成员方法" 是怎么实现"重载"

function hookfun(fun) {  
    return function () {

        // [].slice.all(arguments)就是把arguments对象转化成array
        var args = [].slice.call(arguments)

        // 比方说,open函数,假如原本open的参数是arguments
        // 那么重载后open的参数是[arguments, this.xhr]
        if (funs[fun] && funs[fun].call(this, args, this.xhr)) {
            return;
        }
        this.xhr[fun].apply(this.xhr, args);
    }
}


如果不理解[].slice.call(arguments),那就试试下面的代码

function A() {  
    console.log("%O", arguments)
    for(let i in arguments) {
        console.log(i)
    }

    let args = [].slice.call(arguments);
    console.log("%O", args)
    for(let i in args) {
        console.log(i)
    }
}

从对比输出结果(我这里没有列出来),可以看到数组对象比arguments多出很多方法


二、 接下来,看看 "成员变量" 是怎么实现"重载"

Object.defineProperty(this, attr, {  
    get: getFactory(attr),
    set: setFactory(attr)
})

function getFactory(attr) {  
    return function () {

        // 这个this.xhr[attr]的其中一个原因是为了防止死循环暴栈而亡
        // 这个this[attr + "_"] 要配合下面set方法才能明白
        return this[attr + "_"] || this.xhr[attr]
    }
}

function setFactory(attr) {  
    return function (f) {
        var xhr = this.xhr;
        var that = this;
        if (attr.indexOf("on") != 0) {
            this[attr + "_"] = f;
            return;
        }
        if (funs[attr]) {
            xhr[attr] = function () {
                funs[attr](that) || f.apply(xhr, arguments);
            }
        } else {
            xhr[attr] = f;
        }
    }
}

1.上面说暴栈,如果不理解,可以试试下面代码

var obj2 = {}  
Object.defineProperty(obj2, "val", {  
    get : function(){ return this["val"] },
    set : function(newValue){ this["val"] = newValue}
})
var b = obj2.val  
//上面执行 = 的时候,会调用get,但是 get 函数里面,会去索引"val",如此循环下去

2.创建XMLHttpRequest的时候,它的大量回调函数,比方说onload、onprogress、onreadystatechange、ontimeout还是null值,在上层被赋值成函数,就会走到xhr[attr] = function那个地方的代码,首先执行我们的重载代码funsattr,再执行上层f.apply(xhr, arguments)

3.下滑线的主要目的是为了解决类似于responseText不可写,所以当修改responseText值的时候,实际修改的是responseText_


未经授权,禁止转载
by chainhelen

参考

  1. Ajax-hook 原理解析
  2. Js 拦截全局ajax请求
  3. JS中的双向数据绑定及Object.defineProperty方法
  4. Object.defineproperty语法
  5. 探讨跨域请求资源的几种方式