自上一篇《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實現的代理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… 。
本文章允許免費轉載,但請註明原作者及原文連結。