天天看點

為XHR對象所有方法和屬性提供鈎子 全局攔截AJAX

摘要

長文 閱讀約需十分鐘

跟着走一遍需要一小時以上

約100行代碼

前段時間打算寫一個給手機端用的假冒控制台 可以用來看

console

的輸出 這一塊功能目前已經完成了

但是後來知道有一個騰訊團隊的項目

vConsole

參考了一下發現他的功能豐富了很多 可以看

Network

面闆等

雖然是閹割過的面闆 不過還是可以看到一些網絡相關的重要資訊

是以自己也想着加上這個

Network

面闆功能

想實作這個功能呢 想到過幾種方案 一種是封裝一個ajax 但是這種不太現實 要讓别人都用自己封裝的ajax 對于項目中途需要引入的情況十分不友好 需要改動大量代碼

是以這種方案是不可取的

之後選擇的方案 就像這裡就像實作

console

面闆一樣 攔截了console下面的方法

而攔截

console

對象下的方法 實際上就是重寫了他的方法

比如

console.log

實際上就是

log

方法進行了重寫 在真正執行

log

前 先做一些自己想做的事情

思路

這一塊一開始想到的其實是

XMLHrrpRequest

對象有全局的勾子函數 查閱了文檔似乎沒有發現相關的内容

後來搜尋了一些别的實作方式 大多數就是辣雞站不知道從哪裡爬來的内容 也沒什麼有價值的東西

後來在github上找到了一個倉庫(

Ajax-hook

)

這個庫實作的是可以攔截全局的Ajax請求 可以同同名方法或者屬性作為勾子 達到攔截的效果

部分内容參考了他的實作思想

實際上實作方式和自定義的

console

很像 底層運作依然靠原生的xhr對象 但是在上層做了代理 重寫了

XMLHttpRequest

對象

大體實作思路可以列成下面這幾步

  1. 保留原生xhr對象
  2. XMLHttpRequest

    對象置為新的對象
  3. 對xhr對象的方法進行重寫後放入到新的對象中
  4. 對屬性進行重寫 然後放入到新對象中

重寫方法 做的主要就是在執行原生的方法前 提供一個鈎子 來執行自定義的内容

重寫屬性 主要是一些事件觸發的屬性 比如

onreadystatechange

同時還有别的坑點 xhr對象上很多屬性是隻讀的(這一點也是通過

了解到的)

如果在鈎子中對隻讀屬性進行修改了 那在往下走也是沒有用的 因為壓根沒有修改成功

不過既然大緻思路有了 那就嘗試着實作一下吧

實作

實作之前 先想一下怎麼使用吧 這樣實作過程也會有一個大緻的方向

先起個名字 就叫他

Any-XHR

使用的時候通過

new AnyXHR()

來建立執行個體

可以在構造函數中來進行鈎子的添加 鈎子有兩種 一種是

before

(執行前調用)的鈎子 一種是

after

(執行後調用)的鈎子

就像這樣 構造函數接受兩個參數 都是對象 第一個是調用前執行的鈎子 另一種是調用後

對象裡面的

key

對應了原生xhr的方法和屬性 保持一緻即可

let anyxhr = new AnyXHR({
		send: function(...args) {
			console.log('before send');
		}
	}, {
		send: function(...args) {
			console.log('after send');
		}	
	});
複制代碼           

構造函數

有了目标 現在就來實作一下吧 按照所想的 我們要用

new

關鍵字來執行個體化一個對象 就要用構造函數或者

class

關鍵字建立一個類

這裡随口提一下 ES6的提供的class并不是真正 類似Java或者C++中的類 可以了解成是一種文法糖 本質上還是原型繼承 或者說是基于原型代理的 了解的大佬這句話跳過就好

這裡采用class關鍵字來聲明

class AnyXHR {

		constructor(hooks = {}, execedHooks = {}) {
			this.hooks = hooks;
			this.execedHooks = execedHooks;
		}
		
		init() {
			// 初始化操作...
		}
		
		overwrite(proxyXHR) {
			// 處理xhr對象下屬性和方法的重寫
		}
	}
複制代碼           

這裡目前隻關注

構造函數

構造函數

接受的兩個對象 就是表示執行前的鈎子和執行後的鈎子 對應的挂到執行個體的

hooks

execedHooks

屬性上

按照剛才說的步驟 要保留原生的xhr對象 把這個步驟丢到構造函數中

然後做一些初始化操作 初始化的時候要對xhr對象重寫

constructor(hooks = {}, execedHooks = {}) {
+		this.XHR = window.XMLHttpRequest;

		this.hooks = hooks;
		this.execedHooks = execedHooks;
		
+		this.init();
	}
複制代碼           

init 方法

init裡 要做的就是對

XMLHttpRequest

進行重寫 一個xhr執行個體的每個方法和屬性進行重寫

這樣在

new XMLHttpRequest()

的時候

new

出來的就是我們自己重寫的xhr執行個體了

init() {
		
		window.XMLHttpRequest = function() {

		}
			
	}
複制代碼           

像這樣 把

XMLHttpRequest

指派為一個新的函數 這樣

XMLHttpRequest

就不是原來的那個了 使用者全局通路到的就是我們自己寫的這個新的函數

接着就是去實作重寫像

open

onload

send

這些原生xhr執行個體的方法和屬性了

可是怎麼重寫呢 現在是個空函數 怎麼讓使用者在

new XMLHttpRequest

後 可以使用

send

open

方法呢

其實之前有提到了 重寫方法 做的主要就是在執行原生的方法前 提供一個鈎子 來執行自定義的内容

既然要執行原生的方法 那我們還是需要用到原生的xhr對象

就拿

open

來說

在使用者

new XMLHttpRequest

的同時 需要再

new

一個我們保留下來的 原生的

XMLHttpRequest

然後在自己重寫的

XMLHttpRequest

執行個體上 挂一個

send

方法 他做的 就是先執行自定義的内容 完了之後 去執行我們

new

出來的 保留下來的

XMLHttpRequest

對象的執行個體下的open方法 同時把參數傳遞過去即可

聽起來有點繞 可以畫畫圖再細細品味一下

init() {
+		let _this = this;		
		window.XMLHttpRequest = function() {
+ 		this._xhr = new _this.XHR();   // 在執行個體上挂一個保留的原生的xhr執行個體   

+			this.overwrite(this);
		}
			
	}
複制代碼           

這樣在使用者

new XMLHttpRequest

的時候 内部就建立一個保留下來的 原生的

XMLHttpRequest

執行個體

比如當使用者調用

send

方法的時候 我們先執行自己想要做的事情 然後再調用

this._xhr.send

執行原生xhr執行個體的

send

方法就可以了 是不是很簡單

完了之後我們進入

overwrite

方法 這個方法做的事情就是上一句所說的 對每個方法和屬性進行重寫 其實就是在執行之前和執行之後動點手腳而已

調用

_this.overwrite

的時候我們需要把this傳遞過去 這裡的this 指向的是

new XMLHttpRequest

後的執行個體

屬性和方法都是要挂到執行個體上的 供使用者調用 是以把this傳遞過去 友善操作

這裡在用新函數覆寫

window.XMLHttpRequest

的時候 不可以使用箭頭函數 箭頭函數不能當作構造函數來使用 是以這裡要用this保留的做法

overwrite 方法

要重寫方法和屬性 要重寫的就是

send

open

responseText

onload

onreadystatechange

等這些

那要一個一個實作嗎... 肯定是不現實的

要重寫的方法和屬性 我們可以通過便利一個原生的xhr執行個體來擷取

原生的xhr執行個體 已經保留在了覆寫後的

XMLHttpRequest

對象的執行個體的

_xhr

是以通過周遊

_xhr

這個原生的xhr執行個體 就可以拿到需要所有要重寫的方法和屬性 的

key

value

overwrite(proxyXHR) {
+		for (let key in proxyXHR._xhr) {

+		}
	}
複制代碼           

這裡的

proxyXHR

參數指向的是一個修改後的

XMLHttpRequest

方法和屬性要挂到執行個體上

現對

proxyXHR

下的

_xhr

屬性進行周遊 可以拿到他下面的所有可周遊的屬性和方法

然後區分方法和屬性 做不同的處理即可

overwrite(proxyXHR) {
		for (let key in proxyXHR._xhr) {
		
+			if (typeof proxyXHR._xhr[key] === 'function') {
+				this.overwriteMethod(key, proxyXHR);
+				continue;
+			}

+			this.overwriteAttributes(key, proxyXHR);

		}
	}
複制代碼           

通過

typeof

來判斷目前周遊到的是屬性還是方法 如果是方法 則調用

overwriteMethod

對這個方法進行改造重寫

如果是屬性 則通過

overwriteAttributes

對這個屬性進行改造重寫

同時把周遊到的屬性名和方法名 以及修改後的

XMLHttpRequest

執行個體傳遞過去

那接下來就來實作一下這裡兩個方法

overwriteMethod

在類中添加這個方法

class AnyXHR {
	
+		overwriteMethod(key, proxyXHR) {
			// 對方法進行重寫
+		}	
	
	}
複制代碼           

這個方法做的 就是對原生的xhr執行個體下的方法進行重寫處理

其實嚴格來說 把這個操作稱作重寫是不嚴格的

send

方法來說 并不會對原生的xhr執行個體下的

send

方法進行修改 而寫新寫一個方法 挂到自己實作的xhr執行個體上 來代替原生的xhr執行個體來執行 最終

send

的過程 還是調用原生的

send

方法

隻是在調用前和調用後 多做了兩件别的事情

是以這裡就是對每個方法 做一個包裝

overwriteMethod(key, proxyXHR) {
+		let hooks = this.hooks;	
+		let execedHooks = this.execedHooks;
	
+		proxyXHR[key] = (...args) => {

+		}
	}
複制代碼           

首先保留了

hooks

execedHooks

等下會頻繁用到

然後我們往新的xhr執行個體上挂上同名的方法 比如原生的xhr有個

send

周遊到

send

的時候 這裡進來的key就是

send

是以就會往新的xhr執行個體上挂上一個

send

來替代原生的

send

當方法被調用的時候 會拿到一堆參數 參數是js引擎(或者說浏覽器)丢過來的 這裡用剩餘參數把他們都接住 組成一個數組 調用鈎子的時候 或者原生方法的時候 可以再傳遞過去

那這裡面具體做些什麼操作呢

其實就三步

  • 如果目前方法有對應的鈎子 則執行鈎子
  • 執行原生xhr執行個體中對應的方法
  • 看看還有沒有原生xhr執行個體對應的方法執行後需要執行的鈎子 如果有則執行
overwriteMethod(key, proxyXHR) {
		let hooks = this.hooks;	
		let execedHooks = this.execedHooks;
	
		proxyXHR[key] = (...args) => {

+	      // 如果目前方法有對應的鈎子 則執行鈎子
+	      if (hooks[key] && (hooks[key].call(proxyXHR, args) === false)) {
+				return;
+	      }
	
+	      // 執行原生xhr執行個體中對應的方法
+	      const res = proxyXHR._xhr[key].apply(proxyXHR._xhr, args);
	
+	      // 看看還有沒有原生xhr執行個體對應的方法執行後需要執行的鈎子 如果有則執行
+	      execedHooks[key] && execedHooks[key].call(proxyXHR._xhr, res);
	
+	      return res;

		}
	}
複制代碼           

首先第一步

hooks[key]

就是判斷目前的方法 有沒有對應的鈎子函數

hooks對象裡面儲存的就是所有的鈎子函數 或者說需要攔截的方法 是在我們執行

new AnyXHR(..)

的時候傳進來的

如果有 則執行 并把參數傳遞給鈎子 如果這個鈎子函數傳回了false 則中止 不往下走了 這樣可以起到攔截的效果

否則就往下走 執行原生xhr執行個體中對應的方法

原生的xhr執行個體放在了

_xhr

這個屬性裡 是以通過

proxyXHR._xhr[key]

就可以通路到 同時把參數用

apply

拍散 傳遞過去就好了 同時接住傳回值

完了之後走第三步

看看有沒有執行完原生xhr方法後還要執行的鈎子 如果有則執行 然後把傳回值傳遞過去

之後傳回原生xhr執行個體對應的方法執行後反過來的傳回值就好了

到這了方法的代理、攔截就完成了 可以去嘗試一下了

記得注釋一下

this.overwriteAttributes(key, proxyXHR);

這一行

第一次調試

雖然到現在代碼不多 不過一下子沒繞過來 還是很累的 可以先倒杯水休息一下

到目前為止的完整代碼如下

class AnyXHR {

		constructor(hooks = {}, execedHooks = {}) {
			this.XHR = window.XMLHttpRequest;
	
			this.hooks = hooks;
			this.execedHooks = execedHooks;
			
			this.init();
		}
		
		init() {
			let _this = this;		
			window.XMLHttpRequest = function() {
	 			this._xhr = new _this.XHR();   // 在執行個體上挂一個保留的原生的xhr執行個體   
	
				_this.overwrite(this);
			}
		}
		
		overwrite(proxyXHR) {
			for (let key in proxyXHR._xhr) {
			
				if (typeof proxyXHR._xhr[key] === 'function') {
					this.overwriteMethod(key, proxyXHR);
					continue;
				}
	
				// this.overwriteAttributes(key, proxyXHR);
	
			}
		}
		
		overwriteMethod(key, proxyXHR) {
			let hooks = this.hooks;	
			let execedHooks = this.execedHooks;
		
			proxyXHR[key] = (...args) => {
	
		      // 如果目前方法有對應的鈎子 則執行鈎子
		      if (hooks[key] && (hooks[key].call(proxyXHR, args) === false)) {
					return;
		      }
		
		      // 執行原生xhr執行個體中對應的方法
		      const res = proxyXHR._xhr[key].apply(proxyXHR._xhr, args);
		
		      // 看看還有沒有原生xhr執行個體對應的方法執行後需要執行的鈎子 如果有則執行
		      execedHooks[key] && execedHooks[key].call(proxyXHR._xhr, res);
		
		      return res;
	
			}
		}
	}
複制代碼           

嘗試一下第一次調試

new AnyXHR({
		open: function () {
			console.log(this);
		}
	});
	
	var xhr = new XMLHttpRequest();
	
	xhr.open('GET', '/abc?a=1&b=2', true);
	
	xhr.send();
複制代碼           

可以打開控制台 看看是否有輸出 同時可以觀察一下對象 和

_xhr

屬性下内容坐下對比看看

overwriteAttributes 方法

這個方法實作稍微麻煩些 内容也會繞一些

可能有一個想法 為什麼要監聽 或者說代理 再或者說給

屬性

提供鈎子呢 有什麼用嗎

responseText

這種屬性 其實不是目标

目标是給像

onreadystatechange

onload

這樣的屬性包裝一層

這些屬性有點類似事件 或者說就是事件

使用的時候需要手動給他們指派 在對應的時刻會自動調用 可以說是原生

xhr

提供的鈎子

send

open

可以在請求發出去的時候攔截

onreadystatechange

onload

可以用來攔截服務的響應的請求

是以有必要對這些屬性進行包裝

知道了為什麼需要對屬性做個包裝 問題就到了怎麼去包裝屬性了

onload

做例子

xhr.onload = function() {
	...
};
複制代碼           

在使用者這樣指派的時候 我們應該做出一些響應

捕獲到這個指派的過程

然後去看看

hooks

這個數組中有沒有

onload

的鈎子呀

如果有的話 那就在執行原生的xhr執行個體的onload之前 執行一下鈎子就ok了

那問題來時 普通的屬性怎麼辦 比如

responseType

那這些屬性就不處理了 直接挂到新的xhr執行個體上去

又有問題了 怎麼區分普通的屬性 和事件一樣的屬性呢

其實觀察一下就知道

on

打頭的屬性 就是事件一樣的屬性了

是以總結一下

  • 看看屬性是不是on打頭
  • 如果不是 直接挂上去
  • 如果是 則看看有沒有要執行的鈎子
  • 如果有 則包裝一下 先執行鈎子 再執行本體
  • 如果沒有 責直接指派挂上去

邏輯理清楚了 是不是發現很簡單

好 那就動手吧

??等等 怎麼監聽使用者給

onload

這樣的屬性指派啊???

可以停一下 仔細想想

這一塊就可以用到ES5中提供的

getter

setter

這個知識點肯定是很熟悉的 不熟悉可以去翻一下MDN

通過這兩個方法 就可以監聽到使用者對一個屬性指派和取值的操作 同時可以做一些額外的事情

那怎麼給要插入的屬性設定get/set方法呢

ES5提供了

Object.defineProperty

可以給一個對象定義屬性 同時可以指定她的屬性描述符 屬性描述符中可以描述一個屬性是否可寫 是否可以枚舉 還有他的set/get等

當然使用字面量的方式 為一個屬性定義get/set方法也是可以的

扯了這麼多 那就實作一下這個方法吧

overwriteAttributes(key, proxyXHR) {
		Object.defineProperty(proxyXHR, key, this.setProperyDescriptor(key, proxyXHR));
	}
複制代碼           

這裡就一行代碼 就是用

Object.defineProperty

給自己實作的xhr執行個體上挂個屬性 屬性名就是傳遞過來的key 然後用

setProperyDescriptor

方法生成屬性描述符 同時把key和執行個體傳遞過去

描述符裡面會生成get/set方法 也就是這個屬性指派和取值時候的操作

setProperyDescriptor 方法

同樣的 往類裡面加入這個方法

setProperyDescriptor(key, proxyXHR) {
	
	}
複制代碼           

可以拿到要添加的屬性名(key)和自己實作的xhr對象執行個體

屬性都要挂到這個執行個體上

setProperyDescriptor(key, proxyXHR) {
+		let obj = Object.create(null);
+		let _this = this;
		
	}
複制代碼           

屬性描述符實際上是個對象

這裡用

Object.create(null)

來生成一個絕對幹淨的對象 防止有一些亂起八糟的屬性出現 陰差陽錯變成描述

然後保留一下this

之後就實作一下set

setProperyDescriptor(key, proxyXHR) {
		let obj = Object.create(null);
		let _this = this;
		
+		obj.set = function(val) {

+		}
	}
複制代碼           

set方法會在屬性被指派的時候(比如

obj.a = 1

)被調用 他會拿到一個參數 就是指派的值(等号右邊的值)

然後在裡面 就可以做之前羅列的步驟了

setProperyDescriptor(key, proxyXHR) {
		let obj = Object.create(null);
		let _this = this;
		
		obj.set = function(val) {

+			// 看看屬性是不是on打頭 如果不是 直接挂上去
+			if (!key.startsWith('on')) {
+				proxyXHR['__' + key] = val;
+				return;
+			}

+			// 如果是 則看看有沒有要執行的鈎子
+			if (_this.hooks[key]) {
				
+				// 如果有 則包裝一下 先執行鈎子 再執行本體
+				this._xhr[key] = function(...args) {
+					(_this.hooks[key].call(proxyXHR), val.apply(proxyXHR, args));
+				}
				
+				return;
+			}

+			// 如果沒有 責直接指派挂上去
+			this._xhr[key] = val;
		}
		
+		obj.get = function() {
+			return proxyXHR['__' + key] || this._xhr[key];
+		}
	
+		return obj;
	}
複制代碼           

第一步的時候 判斷了是不是on打頭 不是則原模原樣挂到執行個體上

然後去看看目前的這個屬性 在鈎子的清單中有沒有 有的話 就把要賦的值(val)和鈎子一起打包 變成一個函數

函數中先執行鈎子 再執行賦的值 隻要用的人不瞎雞兒整 那這裡的值基本是函數沒跑 是以不判斷直接調用 同時參數傳遞過去

如果沒有鈎子呢 則直接指派就好了

這樣set方法就ok了

get方法就簡單一些了 就是拿值而已

可能有一個地方不太能了解 就是

proxyXHR['__' + key] = val;

get方法中也有

為什麼這裡有個

__

字首 其實這樣

考慮這麼一個場景 在攔截請求傳回的時候 可以拿到

responseText

屬性

這個屬性就是服務端傳回的值

可能在這個時候會需要根據

responseType

統一的處理資料類型

如果是JSON 那就

this.responseText = JSON.parse(this.response)

然後就滿心歡喜的去成功的回調函數中拿

responseText

結果在對他進行屬性或者方法通路的時候報錯了 列印一下發現他還是字元串 并沒有轉成功

其實就是因為

responseText

是隻讀的 在這個屬性的标簽中

writable

false

是以可以用到一個代理屬性來解決

this.responseText = JSON.parse(this.responseText)

首先根據get方法 去拿

responseText

這個時候還沒有

__responseText

屬性 是以回去原生的xhr執行個體拿 拿到的就是服務端回來的值

然後經過解析後 又被指派

複制的時候 在自己實作的xhr執行個體中 就會多一個

__responseText

屬性 他的值是經過處理後的

那之後再通過

responseText

取值 通過get方法拿到的就是

__responseText

的值

這樣就通過一層屬性的代理 解決了原生xhr執行個體屬性隻讀的問題

這樣大部分邏輯都完成了 記得把前面

this.overwriteAttributes(key, proxyXHR);

的注釋去掉

第二次調試

到這裡被繞暈是有可能的 不用太在意 仔細理一下畫畫圖就好了 倒杯水冷靜一下

這是到目前為止的完整代碼 可以去嘗試一下

class AnyXHR {

  constructor(hooks = {}, execedHooks = {}) {
    this.XHR = window.XMLHttpRequest;

    this.hooks = hooks;
    this.execedHooks = execedHooks;

    this.init();
  }

  init() {
    let _this = this;
    window.XMLHttpRequest = function () {
      this._xhr = new _this.XHR(); // 在執行個體上挂一個保留的原生的xhr執行個體   

      _this.overwrite(this);
    }
  }

  overwrite(proxyXHR) {
    for (let key in proxyXHR._xhr) {

      if (typeof proxyXHR._xhr[key] === 'function') {
        this.overwriteMethod(key, proxyXHR);
        continue;
      }

      this.overwriteAttributes(key, proxyXHR);

    }
  }

  overwriteMethod(key, proxyXHR) {
    let hooks = this.hooks;
    let execedHooks = this.execedHooks;

    proxyXHR[key] = (...args) => {

      // 如果目前方法有對應的鈎子 則執行鈎子
      if (hooks[key] && (hooks[key].call(proxyXHR, args) === false)) {
        return;
      }

      // 執行原生xhr執行個體中對應的方法
      const res = proxyXHR._xhr[key].apply(proxyXHR._xhr, args);

      // 看看還有沒有原生xhr執行個體對應的方法執行後需要執行的鈎子 如果有則執行
      execedHooks[key] && execedHooks[key].call(proxyXHR._xhr, res);

      return res;

    }
  }

  setProperyDescriptor(key, proxyXHR) {
    let obj = Object.create(null);
    let _this = this;

    obj.set = function (val) {

      // 看看屬性是不是on打頭 如果不是 直接挂上去
      if (!key.startsWith('on')) {
        proxyXHR['__' + key] = val;
        return;
      }

      // 如果是 則看看有沒有要執行的鈎子
      if (_this.hooks[key]) {

        // 如果有 則包裝一下 先執行鈎子 再執行本體
        this._xhr[key] = function (...args) {
          (_this.hooks[key].call(proxyXHR), val.apply(proxyXHR, args));
        }

        return;
      }

      // 如果沒有 責直接指派挂上去
      this._xhr[key] = val;
    }

    obj.get = function () {
      return proxyXHR['__' + key] || this._xhr[key];
    }

    return obj;
  }

  overwriteAttributes(key, proxyXHR) {
    Object.defineProperty(proxyXHR, key, this.setProperyDescriptor(key, proxyXHR));
  }
}

複制代碼           
new AnyXHR({
  open: function () {
    console.log('open');
  },
  onload: function () {
    console.log('onload');
  },
  onreadystatechange: function() {
    console.log('onreadystatechange');
  }
});


$.get('/aaa', {
  b: 2,
  c: 3
}).done(function (data) {
  console.log(1);
});

var xhr = new XMLHttpRequest();

xhr.open('GET', '/abc?a=1&b=2', true);

xhr.send();

xhr.onreadystatechange = function() {
  console.log(1);
}
複制代碼           

引入一下jquery 可以嘗試着攔截

觀察一下控制台 就能看到結果了

接下來還有一些内容需要完成 讓整個類更完善 不過剩下的都是很簡單的内容了

單例

因為是全局攔截 而且全局下隻有一個

XMLHttpRequest

對象 是以這裡應該設計成單例

單例是設計模式中的一種 其實沒那麼玄乎 就是全局下隻有一個執行個體

也就是說得動點手腳 怎麼

new AnyXHR

拿到的都是同一個執行個體

修改一下構造函數

constructor(hooks = {}, execedHooks = {}) {
		// 單例
+		if (AnyXHR.instance) {
+			return AnyXHR.instance;
+		}
		
		this.XHR = window.XMLHttpRequest;
		
		this.hooks = hooks;
		this.execedHooks = execedHooks;
		this.init();
	
+		AnyXHR.instance = this;
	}
複制代碼           

進入構造函數 先判斷

AnyXHR

上有沒有挂執行個體 如果有 則直接傳回執行個體 如果沒有 則進行建立

然後建立流程走完了之後 把

AnyXHR

上挂個執行個體就好了 這樣不管怎麼

new

拿到的都是都是同一個執行個體

同時再加一個方法 可以友善拿到執行個體

getInstance() {
	  return AnyXHR.instance;
	}
複制代碼           

動态加入鈎子

所有的鈎子都維護在兩個對象内 每一次方法的執行 都會去讀這兩個對象 是以隻要讓對象發生改變 就能動态的加鈎子

是以加入一個add方法

add(key, value, execed = false) {
	  if (execed) {
	    this.execedHooks[key] = value;
	  } else {
	    this.hooks[key] = value;
	  }
	  return this;
	}
複制代碼           

其中

key

value

兩個參數對應的就是屬性名 或者方法名和值

execed

代表是不是原生的方法執行後再執行 這個參數用來區分添加到哪個對象中

同樣的道理 去掉鈎子和清空鈎子就很簡單了

去掉鈎子

rmHook(name, isExeced = false) {
	  let target = (isExeced ? this.execedHooks : this.hooks);
	  delete target[name];
	}
複制代碼           

清空鈎子

clearHook() {
		this.hooks = {};
		this.execedHooks = {};
	}
複制代碼           

取消全局的監聽攔截

這一步其實很簡單 把我們自己實作的 重寫的

XMLHttpRequest

變成原來的就好了

原來的我們保留在了

this.XHR

unset() {
		window.XMLHttpRequest = this.XHR;
	}
複制代碼           

重新監聽攔截

既然是單例 重新開啟監聽 那隻要把單例清了 重新new就好了

reset() {
	  AnyXHR.instance = null;
	  AnyXHR.instance = new AnyXHR(this.hooks, this.execedHooks);
	}
複制代碼           

完整代碼

class AnyXHR {
/**
 * 構造函數
 * @param {*} hooks 
 * @param {*} execedHooks 
 */
  constructor(hooks = {}, execedHooks = {}) {
    // 單例
    if (AnyXHR.instance) {
      return AnyXHR.instance;
    }

    this.XHR = window.XMLHttpRequest;

    this.hooks = hooks;
    this.execedHooks = execedHooks;
    this.init();

    AnyXHR.instance = this;
  }

  /**
   * 初始化 重寫xhr對象
   */
  init() {
    let _this = this;

    window.XMLHttpRequest = function() {
      this._xhr = new _this.XHR();

      _this.overwrite(this);
    }

  }

  /**
   * 添加勾子
   * @param {*} key 
   * @param {*} value 
   */
  add(key, value, execed = false) {
    if (execed) {
      this.execedHooks[key] = value;
    } else {
      this.hooks[key] = value;
    }
    return this;
  }

  /**
   * 處理重寫
   * @param {*} xhr 
   */
  overwrite(proxyXHR) {
    for (let key in proxyXHR._xhr) {
      
      if (typeof proxyXHR._xhr[key] === 'function') {
        this.overwriteMethod(key, proxyXHR);
        continue;
      }

      this.overwriteAttributes(key, proxyXHR);
    }
  }

  /**
   * 重寫方法
   * @param {*} key 
   */
  overwriteMethod(key, proxyXHR) {
    let hooks = this.hooks;
    let execedHooks = this.execedHooks;

    proxyXHR[key] = (...args) => {
      // 攔截
      if (hooks[key] && (hooks[key].call(proxyXHR, args) === false)) {
        return;
      }

      // 執行方法本體
      const res = proxyXHR._xhr[key].apply(proxyXHR._xhr, args);

      // 方法本體執行後的鈎子
      execedHooks[key] && execedHooks[key].call(proxyXHR._xhr, res);

      return res;
    };
  }

  /**
   * 重寫屬性
   * @param {*} key 
   */
  overwriteAttributes(key, proxyXHR) {
    Object.defineProperty(proxyXHR, key, this.setProperyDescriptor(key, proxyXHR));
  }

  /**
   * 設定屬性的屬性描述
   * @param {*} key 
   */
  setProperyDescriptor(key, proxyXHR) {
    let obj = Object.create(null);
    let _this = this;

    obj.set = function(val) {

      // 如果不是on打頭的屬性
      if (!key.startsWith('on')) {
        proxyXHR['__' + key] = val;
        return;
      }

      if (_this.hooks[key]) {

        this._xhr[key] = function(...args) {
          (_this.hooks[key].call(proxyXHR), val.apply(proxyXHR, args));
        }

        return;
      }

      this._xhr[key] = val;
    }

    obj.get = function() {
      return proxyXHR['__' + key] || this._xhr[key];
    }

    return obj;
  }

  /**
   * 擷取執行個體
   */
  getInstance() {
    return AnyXHR.instance;
  }

  /**
   * 删除鈎子
   * @param {*} name 
   */
  rmHook(name, isExeced = false) {
    let target = (isExeced ? this.execedHooks : this.hooks);
    delete target[name];
  }

  /**
   * 清空鈎子
   */
  clearHook() {
    this.hooks = {};
    this.execedHooks = {};
  }

  /**
   * 取消監聽
   */
  unset() {
    window.XMLHttpRequest = this.XHR;
  }

  /**
   * 重新監聽
   */
  reset() {
    AnyXHR.instance = null;
    AnyXHR.instance = new AnyXHR(this.hooks, this.execedHooks);
  }
}
複制代碼           

完成

到此呢整體就完成了 由于缺乏測試 指不定還有bug

另外有些缺陷 就是所有鈎子得是同步的 如果是異步順序會亂 這個問題之後再解決 如果感興趣可以自己也嘗試一下

另外這種攔截的方式 基本上适用任何對象 可以靈活的使用

源碼

隻要是使用

XMLHttpRequest

的ajax請求 都可以用他來攔截

如果可以請給個星星 過段時間找實習就靠了 萬分感謝!

原文釋出時間:2018年06月29日

原文作者:NISAL

本文來源

掘金

,如需轉載請緊急聯系作者