Ajax-hook原理解析

wendux發表於2017-03-16

自上一篇《js 攔截全域性 ajax 請求》釋出之後,很多人對實現原理非常感興趣,好,今天我們講內涵!

如果你還不知道ajax-hook,請先了解一下:
github : github.com/wendux/Ajax…
中文介紹:www.jianshu.com/p/9b634f1c9…

我們首先在github上拉取原始碼。納尼,這麼屌炸天的功能,原始碼算上註釋和換行總共才67行!?,下面我們來一步步揭開其神祕面紗。

整體思路-代理模式

Ajax-hook實現的整體思路是實現一個XMLHttpRequest的代理物件,然後覆蓋全域性的XMLHttpRequest,這樣一但上層呼叫 new XMLHttpRequest這樣的程式碼時,其實建立的是Ajax-hook的代理物件例項。具體原理圖如下:

Ajax-hook原理解析
ajax-hook原理圖

上圖中青色部分為Ajax-hook實現的代理XMLHttpRequest,內部會呼叫真正的XMLHttpRequest。我們看一下hookAjax的部分原始碼:


ob.hookAjax = function (funs) {
  //儲存真正的XMLHttpRequest物件
  window._ahrealxhr = window._ahrealxhr || XMLHttpRequest
  //1.覆蓋全域性XMLHttpRequest,代理物件
  XMLHttpRequest = function () {
    //建立真正的XMLHttpRequest例項
    this.xhr = new window._ahrealxhr;
    for (var attr in this.xhr) {
      var type = "";
      try {
        type = typeof this.xhr[attr]
      } catch (e) {}
      if (type === "function") {
        //2.代理方法
        this[attr] = hookfun(attr);
      } else {
        //3.代理屬性
        Object.defineProperty(this, attr, {
          get: getFactory(attr),
          set: setFactory(attr)
        })
      }
    }
  }
  ......複製程式碼

Ajax-hook 一開始先儲存了真正的XMLHttpRequest物件到一個全域性物件,然後在註釋1處,Ajax-hook覆蓋了全域性的XMLHttpRequest物件,這就是代理物件的具體實現。在代理物件內部,首先建立真正的XMLHttpRequest例項,記為xhr,然後遍歷xhr所有屬性和方法,在2處hookfun為xhr的每一個方法生成一個代理方法,在3處,通過defineProperty為每一個屬性生成一個代理屬性。下面我們重點看一看代理方法和代理屬性的實現。

代理方法

代理方法通過hookfun函式生成,我們看看hookfun的具體實現:

function hookfun(fun) {
 return function () {
     var args = [].slice.call(arguments)
     //1.如果fun攔截函式存在,則先呼叫攔截函式
    if (funs[fun] && funs[fun].call(this, args, this.xhr)) {
        return;
    }
   //2.呼叫真正的xhr方法
   this.xhr[fun].apply(this.xhr, args);
 }
}複製程式碼

為了敘述清晰,我們假設fun為 send函式,其中funs為使用者提供的攔截函式物件。程式碼很簡單,首先會根據使用者提供的funs判斷使用者是否要攔截send, 如果提供了send的攔截方法,記為send_hook, 則上層呼叫代理物件send方法時,則會先呼叫send_hook,同時將呼叫引數和當前的xhr物件傳遞給send_hook,如果send_hook返回了true, 則呼叫終止,直接返回,相當於呼叫被終止了,如果沒有返回或返回的是false,則會走到註釋2處,此處呼叫了xhr的send方法,至此ajax send被呼叫成功。 所以,我們在send_hook中可以拿到呼叫的引數並修改,因為引數是以陣列形式傳遞,改變會被記錄,當然,我們也可以返回true直接終止呼叫。

代理屬性

屬性如onload、onreadystatechange等,上層在呼叫ajax時通常要設定這些回撥以處理請求到的資料,Ajax-hook也能夠實現在請求返回時先拿到資料第一個進行處理,然後將處理過的資料傳遞給使用者提供的回撥。要實現這個功能,直接的思路就是使用者設定回撥時將使用者提供的回撥儲存起來,然後設定成代理回撥,當資料返回時,代理回撥會被呼叫,然後在代理回撥中首先將返回的資料提供給攔截函式處理,然後再將處理後的資料傳遞給使用者真正的回撥。那麼問題來了,如何捕獲使用者設定回撥的動作?一段典型的使用者呼叫程式碼如下:

var xh=new XMLHttpRequest;
xh.open("https://xxx")
xh.onload=function(data){ //1
  //處理請求到的資料
}複製程式碼

也就是說上面程式碼1處的賦值時機代理物件怎麼捕獲?如果在賦值的時候有機會執行程式碼就好了。我們回過頭來看看上面原理圖,有沒有注意到proxy props後面的小括號裡的 es5,答案就在這裡! es5中對於屬性引入了setter、getter,詳細內容請參考:
Javascript getter: developer.mozilla.org/en-US/docs/…
Javascript setter: developer.mozilla.org/en-US/docs/…

Ajax-hook通過getFactory和setFactory生成setter、getter方法。我們來看看它們的實現:

function getFactory(attr) {
    return function () {
        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;
        }
    }
}複製程式碼

程式碼比較簡單,值得注意的是裡面的屬性加下劃線是什麼意思?請繼續往下看。

屬性修改

如果需要對返回的資料進行加工處理,比如返回的資料是json字串,如果你想將它轉化為物件再傳遞給上層,你會在onload回撥中這麼寫:

xhr.responseText = JSON.parse(xhr.responseText)複製程式碼

但是,這裡有坑,因為xhr的responseText屬性並不是writeable的(詳情請移步 developer.mozilla.org/en-US/docs/… ),這也就意味著你無法直接更改xhr.responseText的值,而Ajax-hook也代理了這些原始屬性,內部生成了一下原始屬性名+下滑線的代理屬性。

至此,Ajax-hook原始碼分析完畢。下面我們總結一下:

Ajax-hook使用代理的方式對原生XMLHttpRequest的方法及屬性進行代理,然後覆蓋全域性XMLHttpRequest,實現攔截所有Ajax-hook的功能。從程式碼角度來看,邏輯清晰,思維巧妙,簡潔優雅,值得學習。

最後

如果你喜歡,就去 github star一下吧,地址 github.com/wendux/Ajax…

本文章允許免費轉載,但請註明原作者及原文連結。