爲XHR對象全部方法和屬性提供鉤子 全局攔截AJAX

摘要

✨長文 閱讀約需十分鐘jquery

✨跟着走一遍須要一小時以上git

✨約100行代碼github

前段時間打算寫一個給手機端用的假冒控制檯 能夠用來看console的輸出 這一塊功能目前已經完成了ajax

可是後來知道有一個騰訊團隊的項目vConsole 參考了一下發現他的功能豐富了不少 能夠看Network面板等設計模式

雖然是閹割過的面板 不過仍是能夠看到一些網絡相關的重要信息數組

因此本身也想着加上這個Network面板功能瀏覽器

想實現這個功能呢 想到過幾種方案 一種是封裝一個ajax 可是這種不太現實 要讓別人都用本身封裝的ajax 對於項目中途須要引入的狀況十分不友好 須要改動大量代碼bash

因此這種方案是不可取的網絡

以後選擇的方案 就像這裏就像實現console面板同樣 攔截了console下面的方法app

而攔截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關鍵字建立一個類

這裏採用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方法 同時把參數傳遞過去便可

聽起來有點繞 能夠畫畫圖再細細品味一下

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

相關文章
相關標籤/搜索