vconsole打包之后丢失网络请求(一)

2,832 阅读4分钟

场景


vconsole是开源的一款移动调试工具,可以查看基本的网络请求,以及相关的storage信息。其中网络请求对移动端的调试来说至关重要。

但是在某些脚手架打包之后,我们发现其网络请求丢失了,而用电脑开发环境下调试是可以的。

追查源码


vconsole的源码:github.com/Tencent/vCo…

追查源码时,发现其实现原理是依赖于xmlHttpRequest,对其方法进行了劫持,然后追加执行自己的记录数组对象。

  /**
   * mock ajax request
   * @private
   */
  mockAjax() {

    let _XMLHttpRequest = window.XMLHttpRequest;
    if (!_XMLHttpRequest) { return; }

    let that = this;
    let _open = window.XMLHttpRequest.prototype.open,
        _send = window.XMLHttpRequest.prototype.send;
    that._open = _open;
    that._send = _send;

    // mock open()
    window.XMLHttpRequest.prototype.open = function () {  
     
      let XMLReq = this;
      let args = [].slice.call(arguments),
          method = args[0],
          url = args[1],
          id = that.getUniqueID();
      let timer = null;

      // may be used by other functions
      XMLReq._requestID = id;
      XMLReq._method = method;
      XMLReq._url = url;
      // mock onreadystatechange
      let _onreadystatechange = XMLReq.onreadystatechange || function() {};
      let onreadystatechange = function() {

        let item = that.reqList[id] || {};

        // update status
        item.readyState = XMLReq.readyState;
        item.status = 0;
        if (XMLReq.readyState > 1) {
          item.status = XMLReq.status;
        }
        item.responseType = XMLReq.responseType;

        if (XMLReq.readyState == 0) {
          // UNSENT
          if (!item.startTime) {
            item.startTime = (+new Date());
          }
        } else if (XMLReq.readyState == 1) {
          // OPENED
          if (!item.startTime) {
            item.startTime = (+new Date());
          }
        } else if (XMLReq.readyState == 2) {
          // HEADERS_RECEIVED
          item.header = {};
          let header = XMLReq.getAllResponseHeaders() || '',
              headerArr = header.split("\n");
          // extract plain text to key-value format
          for (let i=0; i<headerArr.length; i++) {
            let line = headerArr[i];
            if (!line) { continue; }
            let arr = line.split(': ');
            let key = arr[0],
                value = arr.slice(1).join(': ');
            item.header[key] = value;
          }
        } else if (XMLReq.readyState == 3) {
          // LOADING
        } else if (XMLReq.readyState == 4) {
          // DONE
          clearInterval(timer);
          item.endTime = +new Date(),
          item.costTime = item.endTime - (item.startTime || item.endTime);
          item.response = XMLReq.response;
        } else {
          clearInterval(timer);
        }

        if (!XMLReq._noVConsole) {
          that.updateRequest(id, item);
        }
        return _onreadystatechange.apply(XMLReq, arguments);
      };
      XMLReq.onreadystatechange = onreadystatechange;

      // some 3rd libraries will change XHR's default function
      // so we use a timer to avoid lost tracking of readyState
      let preState = -1;
      timer = setInterval(function() {
        if (preState != XMLReq.readyState) {
          preState = XMLReq.readyState;
          onreadystatechange.call(XMLReq);
        }
      }, 10);

      return _open.apply(XMLReq, args);
    };

    // mock send()
    window.XMLHttpRequest.prototype.send = function () {
      let XMLReq = this;
      let args = [].slice.call(arguments),
          data = args[0];

      let item = that.reqList[XMLReq._requestID] || {};
      item.method = XMLReq._method.toUpperCase();

      let query = XMLReq._url.split('?'); // a.php?b=c&d=?e => ['a.php', 'b=c&d=', '?e']
      item.url = query.shift(); // => ['b=c&d=', '?e']

      if (query.length > 0) {
        item.getData = {};
        query = query.join('?'); // => 'b=c&d=?e'
        query = query.split('&'); // => ['b=c', 'd=?e']
        for (let q of query) {
          q = q.split('=');
          item.getData[ q[0] ] = decodeURIComponent(q[1]);
        }
      }

      if (item.method == 'POST') {

        // save POST data
        if (tool.isString(data)) {
          let arr = data.split('&');
          item.postData = {};
          for (let q of arr) {
            q = q.split('=');
            item.postData[ q[0] ] = q[1];
          }
        } else if (tool.isPlainObject(data)) {
          item.postData = data;
        }

      }

      if (!XMLReq._noVConsole) {
        that.updateRequest(XMLReq._requestID, item);
      }

      return _send.apply(XMLReq, args);
    };

  };

思路:针对open方法,创建一个新的请求对象,记录其请求地址,参数,方法名,生成唯一id,计时器,开启10ms每次的计时,更新其状态;同时,请求对象状态变更时,根据唯一id去更新其请求对象的时间,状态,数据信息。

结果:结果打包之后,丢失了对这部分的处理,原因暂时不去考虑,我们先找替代方案。

过渡方案:使用postmessage追加发送接口数据

模块设计


考虑到最终我们需要替换安装的vconsole模块,并保证不会影响项目业务代码的打包,因此主要设计如下:
1 把vconsole的当前依赖版本复制一份维护在libs中,修改源代码
2 利用vconsole的打包命令,并在原来的项目打包前进行vconsole的打包
3 要让vconsole的改动生效,同时不影响主体项目的打包配置,我们引用以及加载vconsole的方式还是js引入核心模块的方式。
4 为了保证第三步,需要将第二步打包之后的产物,替换到node modules中的dist文件中(作为常识要了解我们使用第三方模块时,实际使用的是其dist中的产出)

package.json新增命令:

  "scripts": {
    "vconsole": "sh scripts/replaceVconsole.sh",
    "buildp-v": "npm run vconsole && ai build patientInvite --rem",
    "buildp": "ai build patientInvite --rem"
  },

replaceVconsole.sh 内容如下:切换目录 + 执行vconsole打包 + 替换文件 + 切换到主目录(不影响继续执行主打包路径)

cd libs/vconsole
npm i 
npm run build 
cp ./dist/vconsole.min.js ../../node_modules/vconsole/dist
cd ../../

数据同步:使用postMessage


network.js中接受数据 :

onMessage() { 
  let that = this;
  window.addEventListener('message', function (e) { 
      const { data: { time,method,type,data} } = e 
    const requestId = Math.random(10)
    if (type === 'webpackOk') { 
      return 
    }
      const item = {
        method:type,
        time,
        responseType: 'json',
        response: data,
        status:200,
        url:(data&&data.url)||'参考前置请求',
        isExtra:true
      }
      that.updateRequest(requestId,item)

   })
 }

mockAjax(){
  this.onMessage()
}

api请求发送方:去掉了各种其他参数的记录,同时为了节省时间成本,没有把请求体与返回体统一,后续会优化。

//封装一个发送消息的方法

const postApiMessage = ({ type, data }) => { 
    const target = '*';
    const transfer = [];
    window.postMessage &&  window.postMessage( { data,type,time:moment().format('YYYY-MM-DD HH:MM:SS') },target,transfer)
}

function get(url, data) {
    if (data) {
        let dataString = qs.stringify(data)
    postApiMessage({ data: {url:`${url}?${dataString}`,data,method:'get'},type:'request'})
        return request(`${url}?${dataString}`, {
            method: 'get'
        })
    } else {
        return request(url, {
            method: 'get'
        })
    }
}

function post(url, data) {
    postApiMessage({ data: {url,data,method:'post'},type:'request' })
    return request(`${url}`, {
        method: 'post',
        body: JSON.stringify(data)
    })
}


function getData(data) {
    postMessage({ data, type: 'response' })
}

更多

遗留的问题


1 为什么打包之后,原型链中的请求相关丢失了
2 如何在axios这样的库中,吧请求体与返回体的部分进行绑定到一个对象中

丰富vconsole的插件


有兴趣,还可以去拓展实现一个自己的vconsole的插件,去展示你希望展示的信息面板。

它的插件写法如下:

import VConsole from 'vconsole'
let requestPlugin = new VConsole.VConsolePlugin('request', 'request Plugin');

let dataList = []


requestPlugin.on('renderTab', function(callback) {
   let html = '<ul>';
   for (let i = 0, len = dataList.length; i < len; i++) { 
      html+= `<li>${dataList[i].type}(${dataList[i].time}):${dataList[i].data}</li>`
   }
   html += '</ul>'
	callback(html);
});



requestPlugin.on('addTool', function(callback) {
	var button1 = {
		name: 'clear request',
		onClick: function(event) {
			sessionStorage.setItem('requests','')
		}
   };
   var button2 = {
		name: 'Reload',
		onClick: function(event) {
			window.location.reload();
		}
	};
	callback([button1,button2]);
});


export default requestPlugin;


可能存在的技术挑战:插件在初始化之后,没有过程中的钩子函数,因此这方面需要自己去做技术方案,如何在数据变更时,修改展示的文案信息,计时器,storage change等都是不错的方向。

如何实现一个webpack的vconsole的插件,简化团队的使用成本


可参考地址:github.com/diamont1001…

我的文档

语雀30+专辑,1000+文章不断更新系统整理中 : www.yuque.com/robinson