Javascript 如何全面接管xhr請求

語言: CN / TW / HK

背景及思考

為什麼需要接管xhr請求?這就需要我們瞭解它的一些應用場景。我們如何統一專案中xhr請求的行為,監控請求的整個生命週期、如何自定義攔截請求並返回mock資料、如何制定完全可控的控制檯(如vconsole那樣) 等等!

有一種最常見的情況。比如專案中發起請求的方式不一,有的在js sdk或私有npm庫中發起、有的在引入了第三方js cdn中發起、有的由專案中統一的ajax、axios發起。如果我們需要對專案中所有請求增加某些統一的行為該如何處理了?


原生XMLHttpRequest 回顧

使用xhr發起請求

注:以下只針對xhr的處理,不考慮使用ActiveXObject來處理相容性,不考慮使用fetch請求。

``` // 建立 XMLHttpRequest 物件 var xhr = new XMLHttpRequest ();

// 建立連線 xhr.open(method, url, async, username, password);

// 在open後,send前 可對報文進行處理,如設定請求頭 xhr.setRequestHeader('customId', 666)

// 對於非同步請求,繫結響應狀態事件監聽函式 xhr.onreadystatechange = function () { //監聽readyState狀態、http狀態碼 if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText); // 接收資料 } }

// 使用 send() 方法傳送請求 xhr.send(body);

//對於同步請求,可直接接收資料 console.log(xhr.responseText);

//中止請求 xhr.onreadystatechange = function () {}; //清理事件響應函式(IE、火狐相容性處理) xhr.abort(); ```


ES5實現區域性攔截

假設使用ajax、axios、原生xhr在請求時增加了自定義的請求頭custom-trace-id:'aa,bb'。 我們如何通過攔截獲取到其值,並增加兩個新的請求頭'custom-a': 'aa''custom-b': 'bb' (分割custom-trace-id的值獲取到'aa'和'bb')

攔截專案中所有xhr, 並給有'custom-trace'的頭增加新的自定義請求頭(僅攔截open和setRequestHeader)

(function(w){ w.rewriteXhr = { // 隨機生成uuid _setUUID: function () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }, // 儲存xhr原型 xhrProto:w.XMLHttpRequest.prototype, // 儲存需要攔截的區域性屬性或方法 tempXhrProto: function(){ this._open = this.xhrProto.open this._setRequestHeader = this.xhrProto.setRequestHeader }, // 攔截處理 proxy: function(){ var _this = this this.xhrProto.open = function () { this._open_args = [...arguments]; return _this._open.apply(this, arguments); } // 攔截setRequestHeader方法 this.xhrProto.setRequestHeader = function () { var headerKey = arguments[0] // 需要給所有請求增加的頭 var keys = ['custom-a', 'custom-b', 'custom-uuid'] // 可使用url做過濾處理 // var url = this.open_args && this.open_args[1] if(/^custom-trace-id$/.test(headerKey)){ var value = arguments[1] && arguments[1].split(',') value[2] = _this._setUUID() keys.forEach((key, index)=>{ // 也可以直接使用_this._setRequestHeader.apply(this, arguments) this.setRequestHeader(key, value[index]) }) return } return _this._setRequestHeader.apply(this, arguments) } }, init: function(){ this.tempXhrProto() this.proxy() } } w.rewriteXhr.init() })(window) </script>

以上,我們重新定義了opensetRequestHeader的原型方法(攔截open的目的在於只能在該方法的引數中獲取到url等資訊),同時也儲存了原始的opensetRequestHeader。在每次有請求呼叫到setRequestHeader時,實際呼叫的是我們自己重寫的setRequestHeader方法,在該方法裡面再去呼叫原始的setRequestHeader,從而實現攔截設定請求頭的目的。

瞭解了局部的xhr攔截,我們可以以此來思索如何封裝實現全域性的請求攔截?


ES5實現全域性攔截

在專案中使用xhrHook

xhrHook({ open: function (args, xhr) { console.log("open called!", args, xhr) }, setRequestHeader: function (args, xhr) { console.log("setRequestHeader called!", args, xhr) }, onload: function (xhr) { // 對響應結果做處理 this.responseText = xhr.responseText.replace('abc', '') } })

xhrHook 的實現

在全域性攔截中,我們需要考慮到例項的屬性、方法及事件的處理。

``` function xhrHook(config){ // 儲存真實的xhr構造器, 在取消hook時,可恢復 window.realXhr = window.realXhr || XMLHttpRequest

  // 重寫XMLHttpRequest建構函式
  XMLHttpRequest = function(){
    var xhr = new window.realXhr()
    // 遍歷例項及其原型上的屬性(例項和原型鏈上有相同屬性時,取例項屬性)
    for (var attr in xhr) {
        if (Object.prototype.toString.call(a) === '[object Function]') {
            this[attr] = hookFunction(attr); // 接管xhr function
        } else {
            Object.defineProperty(this, attr, { // 接管xhr attr、event
                get: getterFactory(attr),
                set: setterFactory(attr),
                enumerable: true
            })
        }
    }
    // 真實的xhr例項儲存到自定義的xhr屬性中
    this.xhr = xhr
 }

} ```

xhr中的方法攔截

// xhr中的方法攔截,eg: open、send etc. function hookFunction(fun) { return function () { var args = Array.prototype.slice.call(arguments) // 將open引數存入xhr, 在其它事件回撥中可以獲取到。 if(fun === 'open'){ this.xhr.open_args = args } if (config[fun]) { // 配置的函式執行結果返回為true時終止呼叫 var result = config[fun].call(this, args, this.xhr) if (result) return result; } return this.xhr[fun].apply(this.xhr, args); } }

xhr中的屬性和事件的攔截

// 屬性及回撥方法攔截 function getterFactory() { var value = this.xhr[attr] var getter = (proxy[attr] || {})["getter"] return getterHook && getterHook(value, this) || value } // 在賦值時觸發該工廠函式(如onload等事件) function setterFactory(attr) { return function (value) { var xhr = this.xhr; var _this = this; var hook = config[attr]; // 方法或物件 if (/^on/.test(attr)) { // 在真實的xhr上給事件繫結函式 xhr[attr] = function (e) { e = configEvent(e, _this) var result = hook && hook.call(_this, xhr, e) result || value.call(_this, e); } } else { var attrSetterHook = (hook || {})["setter"] value = attrSetterHook && attrSetterHook(value, _this) || value try { xhr[attr] = value; } catch (e) { console.warn('xhr的'+attr+'屬性不可寫') } } } }

解除xhr攔截,歸還xhr管理權

// 歸還xhr管理權 function unXhrHook() { if (window[realXhr]) XMLHttpRequest = window[realXhr]; window[realXhr] = undefined; }

ES6實現全域性攔截

夜已深,等待整理中......


總結

xhr的全域性攔截總體來說比較簡單,除了對事件的託管流程有點複雜。不管是區域性還是全域性處理,共同的特點是都要儲存原生的xhr, 但在執行原生的屬性、方法、事件時,會先執行自己的處理函式,在函式中執行一些操作,最後再去執行原生的方法。

對於事件的攔截,比如我們在定義xhr.onload = function(){}時,實際觸發的是自己定義的onloadsetter方法,在該方法中會去給真實的xhr繫結回撥函式onload,並在回撥函式中去執行config.onload中的邏輯、如果config.onload()沒有返回或返回false, 會繼續執行之前在外面繫結的xhr.onload函式。

如有不足之處、疑問或建議,歡迎大家留言指出。