為XHR物件所有方法和屬性提供鉤子 全域性攔截AJAX

NISAL發表於2018-06-28

摘要

✨長文 閱讀約需十分鐘

✨跟著走一遍需要一小時以上

✨約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物件上很多屬性是隻讀的(這一點也是通過Ajax-hook瞭解到的)

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

不過既然大致思路有了 那就嘗試著實現一下吧

實現

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

先起個名字 就叫他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物件下屬性和方法的重寫
		}
	}
複製程式碼

這裡目前只關注建構函式

建構函式接受的兩個物件 就是表示執行前的鉤子和執行後的鉤子 對應的掛到例項的hooksexecedHooks屬性上

按照剛才說的步驟 要保留原生的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就不是原來的那個了 使用者全域性訪問到的就是我們自己寫的這個新的函式

接著就是去實現重寫像openonloadsend這些原生xhr例項的方法和屬性了

可是怎麼重寫呢 現在是個空函式 怎麼讓使用者在new XMLHttpRequest後 可以使用sendopen方法呢

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

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

就拿open來說

在使用者new XMLHttpRequest的同時 需要再new一個我們保留下來的 原生的XMLHttpRequest物件

然後在自己重寫的XMLHttpRequest例項上 掛一個send方法 他做的 就是先執行自定義的內容 完了之後 去執行我們new出來的 保留下來的XMLHttpRequest物件的例項下的open方法 同時把引數傳遞過去即可

為XHR物件所有方法和屬性提供鉤子 全域性攔截AJAX

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

	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 方法

要重寫方法和屬性 要重寫的就是sendopenresponseTextonloadonreadystatechange等這些

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

要重寫的方法和屬性 我們可以通過便利一個原生的xhr例項來獲取

原生的xhr例項 已經保留在了覆蓋後的XMLHttpRequest物件的例項的_xhr屬性上

所以通過遍歷_xhr這個原生的xhr例項 就可以拿到需要所有要重寫的方法和屬性 的keyvalue

	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) => {

+		}
	}
複製程式碼

首先保留了hooksexecedHooks 等下會頻繁用到

然後我們往新的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這種屬性 其實不是目標

目標是給像onreadystatechangeonload這樣的屬性包裝一層

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

使用的時候需要手動給他們賦值 在對應的時刻會自動呼叫 可以說是原生xhr提供的鉤子

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

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

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

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

onload做例子

xhr.onload = function() {
	...
};
複製程式碼

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

捕獲到這個賦值的過程

然後去看看 hooks這個陣列中有沒有onload的鉤子呀

如果有的話 那就在執行原生的xhr例項的onload之前 執行一下鉤子就ok了

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

那這些屬性就不處理了 直接掛到新的xhr例項上去

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

其實觀察一下就知道 on打頭的屬性 就是事件一樣的屬性了

所以總結一下

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

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

好 那就動手吧

??等等 怎麼監聽使用者給onload這樣的屬性賦值啊???

可以停一下 仔細想想

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

這個知識點肯定是很熟悉的 不熟悉可以去翻一下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)被呼叫 他會拿到一個引數 就是賦值的值(等號右邊的值)

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

  • 看看屬性是不是on打頭
  • 如果不是 直接掛上去
  • 如果是 則看看有沒有要執行的鉤子
  • 如果有 則包裝一下 先執行鉤子 再執行本體
  • 如果沒有 責直接賦值掛上去
	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是隻讀的 在這個屬性的標籤中 writablefalse

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

比如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請求 都可以用他來攔截

相關文章