天天看点

为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

本文来源

掘金

,如需转载请紧急联系作者