从axios源码角度解决bug的过程记录
chenjianzhong 人气:0现象
公司的一个 H5 站点在头条 App 里白屏,在手百、QQ 浏览器、Safari、Chrome 等都正常
排查思路
1. 引入 vConsole 在移动端调试
因为移动端没有 PC 里那样方便的调试工具可以清晰的查看 log 和 network 之类有用的信息,只能借助 vConsole
、eruda
这类移动端调试工具,可以在移动端实现类似 PC 浏览器里的调试功能
2. 从大范围到小范围的 log
因为移动端无法 debugger,只能逐步的 log 去定位问题。我们采用的是 vue 技术栈,可以从 main.js 到路由组件的生命周期里去添加 log,通过这种方式定位到了在一个接口请求的方法之后的代码都不会请求,奇怪的是也没有抛出任何异常,并且在 vConsole
里的 network 下找到了该请求也是正常的响应,如下代码:
try { console.log('start fetch') const res = await FetchXXX(); console.log(res) } catch (e) { console.log(e) }
只打印出了start fetch
,请求的结果和捕获的异常里都没有打印出东西,至此又缩小了排查的范围,一定是请求的响应处理部分出现问题了。
因为我们的请求接口方法是基于 axios 统一封装的,本以为是封装的哪个环节有不兼容头条的代码,通过不断的 log,发现 request interceptor
都执行了,但是 response interctpor
一个都没执行,好家伙,看着不像我们封装的问题,应该是 axios
内部处理的问题。
3. axios 源码一览
通过上面的一顿操作,基本可以确认是 axios
内部处理响应数据时可能有部分兼容性问题,去 github 上去找 issue 也没找到相关的问题。 没办法只能去看 axios
的源码了,我们前面定位到是请求发送没问题,在响应的时候应该是遇到了啥异常,并且还没有抛出。
项目里 axios
用的版本是 0.24.0
,我直接在 node_modules
里看 axios
的源码,因为 npm 下载的 axios
包没有用 webpack
或者 rollup
之类的编译过,所以在 node_modules
里看和看源码无异,并且更可靠(因为项目里是直接引用的这个代码)。打开 axios
源码目录
axios
的源码不算复杂,目录光看命名基本也能猜到是干啥的,几个主要目录如下:
adapters
:针对不同的宿主环境使用不同的请求 api,目前只有浏览器端和 nodejs 端,adapters
目录下的 xhr.js
和 http.js
分别对这两种环境进行了实现。
cancel
:取消请求的相关源码
core
:axios
的核心源码
helpers
:工具方法
排查角度 - interceptor
之前定位到所有的 response interceptor
都没执行,根据这个现象我先找到 interceptor
相关的代码,在 core/Axios.js
里找到以下代码:
// 请求拦截器列表 var requestInterceptorChain = []; var synchronousRequestInterceptors = true; // 遍历所有的请求拦截器并放入到列表中 this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { return; } synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; // 请求拦截器后进先出(栈) requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); }); // 响应拦截器列表 var responseInterceptorChain = []; // 遍历所有的响应拦截器并放入到列表中 this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { // 请求拦截器先进先出(队列) responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); }); var promise; if (!synchronousRequestInterceptors) { // axios执行队列,包含所有的请求拦截器、请求方法、响应拦截器,并按顺序排列 // 这里的dispatchRequest就是实际的请求方法 var chain = [dispatchRequest, undefined]; // 将所有的请求拦截器放请求的前面 Array.prototype.unshift.apply(chain, requestInterceptorChain); // 将所有的响应拦截器放请求的后面 chain = chain.concat(responseInterceptorChain); promise = Promise.resolve(config); // 依次执行整个执行队列,直至队列为空 while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } return promise; }
以上代码就是 axios
对 interceptor
核心的处理,一开始怀疑是不是所有的响应拦截器没有被加入到执行队列中,log 后发现没有问题,所有响应拦截器都在队列中。
排查角度 - xhr
上面排除了 interceptor
的问题,我又怀疑 axios
封装的 xhr
在响应的时候有啥兼容性问题,于是查看 adapters/xhr.js
,并找到处理响应相关的代码:
// 省略了许多不必要的代码 var request = new XMLHttpRequest(); function onloadend() { console.log('onloadend') // ... } if ('onloadend' in request) { console.log('use onloadend') request.onloadend = onloadend; } else { console.log('use onreadystatechange') request.onreadystatechange = function handleLoad() { if (!request || request.readyState !== 4) { return; } setTimeout(onloadend); }; }
axios
处理响应的代码大致如上,如果浏览器 xhr
对象包含 onloadend
事件就监听 onloadend
事件,否则监听 onreadystatechange
来实现请求响应的回调,通过 log 我发现打印了use onreadystatechange
,但是没打印onloadend
。
至此终于找到问题所在,头条 iOS 该版本的 xhr 对象虽然声明了 onloadend 事件但是请求结束后并未回调该事件!
4. 解决问题
因为我们其他站点在头条上是可以正常访问的,我看了那些站点的代码,发现他们用的 axios
版本是 0.18.1
,看下 0.18.1
版本的 adapters/xhr.js
文件对于响应的处理:
他是直接使用的 onreadystatechange
方法来监听的,所以没有问题。
至此整个排查结束,最后通过降级 axios 版本解决该问题。
加载全部内容