事情是这样的,上上上上周有个用户用手机访问我们的页面白屏了,客服还专门拉了沟通群。前端组长对我说,这个问题你来协助解决一下。说干就干,但这个问题有点麻烦,需要点时间才能解决。作为一个成熟的程序员,先让客服同学安抚一下用户,毕竟咱们公司以客户为中心嘛,另外也预留一些时间解决问题。万万没想到,我们正在加急处理问题,客户给技术总监打电话了。好家伙,总监发话了,这个问题很严重,要尽快解决。那咋整,继续加急处理呗。
从客服那得知,用户的手机是ios11系统,其他手机并不会出现白屏问题,初步判断是兼容性问题。要系统的分析白屏问题,先梳理一下整个页面的访问链路,也就是面试经常会问到的一个问题:从输入URL到页面显示中间发生了什么?请看下图。
1why:为什么会白屏呢?
首先,只有ios11才会出现,说明不是没有返回html,从现象来看可以证实,页面上显示了标题,只是没有内容。通过抓包发现,确实有返回html。基本可以肯定,问题出现在浏览器这一层,也就是上图的红色部分。
2why:为什么没有内容?
抓包发现有返回html,也返回了入口js,也就是app.xxx.js和chunk-vendors.xxx.js。有js,但没内容,那有两种可能:
- 入口js没有执行
- 入口js执行报错了,导致后面渲染页面的代码没有执行
那看看日志系统上面有没有上报错误信息。查了一下发现,没有任何错误信息。那有可能是上报系统没有上报。好吧,只能用vConsole(一款移动端h5调试神器)大法了。使用很简单,直接在html的head中加入以下代码(注意:一定要确保在入口js之前执行,否则可能捕获不到错误):
用ios11手机打开测试页面,发现链链vConsole按钮并没有出现。难道连vConsole也有兼容问题?经过几次测试,换了低版本的vConsole后,终于可以调试了。
刷新页面,打开vConsole,找不到任何报错的具体信息,只有简单的Script error。但到这一步确定了是js报错导致白屏,那下一步就要想办法获取报错的具体信息。
3why:为什么只显示Script error?
原因其实很简单,js、css等静态资源是放在CND上的,静态资源域名和页面域名不同,存在跨域问题。了解过跨域的同学都知道,由于浏览器安全策略限制,跨域脚本报错时,无法直接获取错误的详细信息,只能得到一个Script Error。
找到了原因,解决问题就简单了,配置两样东西:
- 配置响应头:Access-Control-Allow-Origin
- 给script标签增加crossorigin属性,白屏页面使用的是vue框架,只要在vue.config.js中增加
crossorigin:""
即可
配置完毕,再看看报错信息,发现了入口js的getCurriculumTime方法报错了。上面我们判断到是兼容问题,是否这个方法有兼容问题呢?根据报错信息搜索入口js,竟然是一个async方法。
也就是说,es6语法并没有转译成es5。但.browserslistrc
已经配置了ios >= 8
,为啥没有转译?
4why:为什么es6语法没有转译成es5?
在vue.config.js中有一个transpileDependencies配置,Vue Cli官方文档是这样解释的:
Type: boolean | Array
Default: false
默认情况下 babel-loader 会忽略所有 node_modules 中的文件。你可以启用本选项,以避免构建后的代码中出现未转译的第三方依赖。不过,对所有的依赖都进行转译可能会降低构建速度。如果对构建性能有所顾虑,你可以只转译部分特定的依赖:给本选项传一个数组,列出需要转译的第三方包包名或正则表达式即可。
经排查发现,getCurriculumTime确实是node_modules下一个依赖里的方法。白屏页面使用了这个依赖里的几个vue组件。原因找到了,那就好解决了,直接依赖包的包名放到transpileDependencies数组中,类似下面这样:
transpileDependencies:['@xxx/xxx']
这下以为可以把问题解决了,差点激动得去报告组长。慢着,作为一个成熟的程序员,咱们先自测一下。
好家伙,并没什么ruan用,白屏还是白屏,报错还是那个报错。莫非,transpileDependencies配置没生效,果然一搜编译后的代码,async等es6语法还在。
再看transpileDependencies配置,可以是boolean,可以是字符串、正则数组,反复试了true、正则都没有生效。用户又一直在问进展。不行,得先暂停深挖原因,得先找个简单方法解决了。既然默认不转译node_modules下的包,那就直接引用组件的vue代码,试一下果然生效了。好的,问题解决了,喝口水摸下鱼歇歇脚。
5why:为什么transpileDependencies配置没有生效?
虽然临时解决白屏问题,但是工程引了很多依赖,只要存在一个依赖没有转译成es5,就有可能出问题,像一颗不定时炸弹,一爆炸就让人熬夜加班掉头发。作为一个成熟的程序员,咱们得彻底解决问题。为了找到transpileDependencies配置没生效的问题,翻一下源码。vs code全局搜node_modueles/@vue文件夹,找到了蛛丝马迹。
// @vue/cli-plugin-babel\index.js
function genTranspileDepRegex (transpileDependencies) {
const deps = transpileDependencies.map(dep => {
if (typeof dep === 'string') {
const depPath = path.join('node_modules', dep, '/')
return isWindows
? depPath.replace(/\\/g, '\\\\') // double escape for windows style path
: depPath
} else if (dep instanceof RegExp) {
return dep.source
}
})
return deps.length ? new RegExp(deps.join('|')) : null
}
这段代码大家都看得懂,就不解释了,看看使用这个方法的地方:
两段代码的意思就是,在transpileDependencies配置里的依赖会进行转译,官方文档并没有骗人。问题是用我的电脑编译会转译,在服务器编译就不会转译。百思不得其解,搜一下看看有没有人遇到过。搜了很久,都说没有找到根本原因,曲线救国解决问题。不过有个哥们说不能用cnpm安装依赖,好家伙,我电脑上用的是npm,服务器上用的是cnpm。
6why:为什么cnpm安装依赖就不转译?
查了cnpm和npm的区别,都在说cnpm是国内源,没有其他有用消息。但从问题来看,cnpm和npm还有其他区别。为了找到根本原因,在源码里加点日志,调试一下。
//vue.config.js 转译node_modules下所有@xiaoe的依赖
transpileDependencies: [/[/\\]node_modules[/\\]@xiaoe/]
来看看结果:
- npm:@xiaoe依赖会正常转译
- cnpm:所有@xiaoe都没有转译
对比很容易发现,cnpm安装的包带上_字符,路径变了,正则匹配不了,所以通过cnpm安装的包都不会转译。
解决办法
找到了根本原因,问题迎刃而解。
方式一:只要将正则表达式改成可以匹配cnpm安装的依赖路径即可。
// 如果要匹配其他包,将@xiaoe改成其他包名即可
transpileDependencies: [/[/\\]node_modules[/\\]_?@xiaoe/]
方式二:使用npm安装依赖
方式三:将正则匹配node_modules即可,但是这会导致编译速度变慢,需要根据实际情况选择适合的方式。
transpileDependencies: [/[/\\]node_modules[/\\]/]
总结
上面使用了6w的思维方式彻底解决白屏问题,可能过程有点繁琐。实际操作过程中比文章描述还曲折,但基本算是彻底整明白了。
另外有一点想和大家分享的是,平时工作中由于比较忙,往往把问题解决就不继续深究根本原因了,这对于技术人来说并不是什么好事。刨根究底,虽然比较花时间,但可以提升看问题的深度和彻底解决问题的能力。
最后,整理这篇文章花了一天的时间,如果对大家有帮助,不妨来个一键三连。如果觉得有什么地方写的不对或者不详细的地方也欢迎在评论区讨论,我会花时间回复大家的,哈哈哈。