Javascript|Javascript 如何全面接管xhr请求

Javascript 如何全面接管xhr请求
背景及思考 为什么需要接管xhr请求?这就需要我们了解它的一些应用场景。我们如何统一项目中xhr请求的行为,监控请求的整个生命周期、如何自定义拦截请求并返回mock数据、如何制定完全可控的控制台(如vconsole那样) 、如何监控所有api请求的健康状态 等等!
有一种最常见的情况。比如项目中发起请求的方式不一,有的在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 = https://www.it610.com/article/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)

以上,我们重新定义了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 = https://www.it610.com/article/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 = https://www.it610.com/article/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, 但在执行原生的属性、方法、事件时,会先执行自己的处理函数,在函数中执行一些操作,最后再去执行原生的方法。
【Javascript|Javascript 如何全面接管xhr请求】对于事件的拦截,比如我们在定义xhr.onload = function(){}时,实际触发的是自己定义的onloadsetter方法,在该方法中会去给真实的xhr绑定回调函数onload,并在回调函数中去执行config.onload中的逻辑、如果config.onload()没有返回或返回false, 会继续执行之前在外面绑定的xhr.onload函数。
如有不足之处、疑问或建议,欢迎大家留言指出。
作者:tager
链接:https://juejin.cn/post/7019704757556084750
说明:稀土掘金同步更新
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    推荐阅读