阅读 378

Javascript 如何全面接管xhr请求

背景及思考

为什么需要接管xhr请求?这就需要我们了解它的一些应用场景。我们如何统一项目中xhr请求的行为,监控请求的整个生命周期、如何自定义拦截请求并返回mock数据、如何制定完全可控的控制台(如vconsole那样) 等等!

有一种最常见的情况。比如项目中发起请求的方式不一,有的在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 = 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)
  </script>复制代码

以上,我们重新定义了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
     }
   }
  </script>复制代码

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 = 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 = 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实现全局拦截


作者:tager
链接:https://juejin.cn/post/7019704757556084750


文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐