scenario

Vconsole is an open source mobile debugging tool that allows you to view basic network requests and storage information. Network request is very important for mobile debugging. But after some scaffolding was packed, we found that the network request was missing and that debugging was ok in a computer development environment.

To track down the source code

Vconsole source: github.com/Tencent/vCo… When I traced the source code, I found that the implementation relies on xmlHttpRequest, hijacks its methods, and appends its own record array object.

  /** * 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);
    };

  };
Copy the code

For the open method, create a new request object, record its request address, parameters, method name, generate a unique ID, timer, start 10ms each time, update its status; At the same time, when the status of the request object changes, it updates the time, status and data information of the request object according to the unique ID. Result: After the result is packaged, the processing of this part is lost. The reason is not considered for the time being, and we will first find an alternative plan.

Transition solution: Use PostMessage to append interface data

Module design

Considering that eventually we need to replace the installed VConsole module without affecting the packaging of the project business code, the main design is as follows: 1. Copy the current dependent version of vconsole into liBS for maintenance and modify the source code. 2. Use the package command of vconsole and pack vconsole before the original project. The way we reference and load vConsole is still the way JS introduces the core module. In order to ensure the third step, we need to replace the packaged product of the second step with dist file in Node and Modules (as a common sense, we actually use the output of dist when using third-party modules) package.json add command:

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

The replacevconsole. sh content is as follows: Switch directory + perform vconsole package + replace file + switch to home directory (the main package path will not be affected)

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

Data synchronization: Use postMessage

Received data in 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)||'Reference pre-request'.isExtra:true
      }
      that.updateRequest(requestId,item)

   })
 }

mockAjax(){
  this.onMessage()
}
Copy the code

API request sender: records of various other parameters are removed. In order to save time and cost, the request body and return body are not unified, which will be optimized later.

// Encapsulates a method to send a message

const postApiMessage = ({ type, data }) = > { 
    const target = The '*';
    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'})}Copy the code

More and more

Remaining issues

1. Why is the request correlation lost in the prototype chain after packaging? 2. How to bind the part of the request body and return body to an object in a library like AXIos

Rich plug-ins for vConsole

If you are interested, you can also extend and implement a plugin for your own vConsole to display the information panel you wish to display. The plugin is written as follows:

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;
Copy the code

Possible technical challenges: after the plug-in is initialized, there is no hook function in the process, so you need to make your own technical plan in this aspect. How to modify the displayed text information when the data changes, timers, storage change, etc., are good directions.

How to implement a webpack vconsole plug-in to simplify the cost of using the team

Reference address: github.com/diamont1001…

My document

Language finch 30+ albums, 1000+ articles are constantly updated in the system: www.yuque.com/robinson