场景
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