想知道最后如何解决的可以直接拖到后面看解决方案~~~
案例说明
问题背景: 某次版本升级后有用户反馈APP中的H5模块会白屏,分布在不特定用户、机型、模块。开发、测试使用多台手机都没法复现,无法定位到故障原因,即使多次尝试优化,都未能彻底解决此问题。直到对lerna分包管理机制、babel插件配置方式的充分了解,才找到此类白屏问题的解决方案。
问题定位:
【问题排查阶段】
背景:报障之后问题始终无法复现,只能根据经验初步处理。
- 排查问题前后代码改动:排查版本前后框架较大改动(H5、原生端)。
H5构建配置变动较大:脚手架从webpack 4升级到 webpack 5
原生端升级了底层交互通用库:运维反馈大多数白屏用户清除APP缓存后页面恢复正常(当时有很大精力误导放在原生交互的调整上。)
- 排查原生交互问题:针对报障最多的页面进行原生交互排查、改造
由于被用户APP清除缓存,页面不再白屏等现象误导,对用户报障过的模块进行原生交互、url携带参数较长可能截取的可能性进行排查,优化改造后反馈仍然没有效果。
- 参考过往案例解决方案:排查H5文件是否下载完整
过去发生过原生端H5热更新机制存在异常,导致H5资源下载不完整,从而页面打开空白,经排查无问题。
【问题定位阶段】
- 找到复现手机进行定位:发现可能和babel编译的JS兼容性有关。
后面多找了几台手机试了下,有台OPPO手机也出现了白屏现象,因此使用该手机进行定位。找到控制台堆栈报错信息指向的构建后文件,把该文件代码放到babel官网在线工具进行默认编译后替换,发现不再白屏,因此确定是babel编译问题。
- 定位未转义代码的源码 搜索dists文件中发现还是存在ES6的语法(【...】扩展运算符、【?.】链判断运算符)
【问题解决阶段】
背景:找到问题根源,开发尝试解决
- 研究项目构建机制:找出项目中的babel配置失效的原因。
- 优化项目构建配置:构建工具babel配置调优,提高项目构建生成的代码兼容性。
案例经验
- 要熟练掌握项目构建机制(不同分包管理框架差异)和构建插件(如 babel-loader)的配置技巧,否则在生产问题定位、各类组件/框架开发、项目性能优化等工作中容易缺乏解决能力和使用不当。
- 要和运维同事商量建立记录、归类生产问题(特别是复杂、难以复现的问题)报障信息的规范机制(如用户身份、影响业务范围、报障次数、手机型号、可能恢复正常的操作行为等),比如后面收集数据发现这些用户基本上都使用的是oppo和华为旧版本的手机,于是我们尝试用oppo测试机复现,第一部oppo手机可能是版本不够旧没法复现问题,直到后面找到了一部“82年”的oppo终于成功复现了白屏现象。
- 可以考虑借助Sentry针对无法复现问题进行主动埋点设计,尝试收集用户侧有效数据。
此问题和babel相关,先简单介绍下babel
babel是什么?
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
@babel/preset-env
@babel/preset-env
是一个“聪明”的预设(preset),它是一个能将最新的 JavaScript 语法转化为目标环境所支持的语法的语法转换插件。(也就是默认帮你配置好了转换规则,让你可以开箱即用babel)
@babel/preset-env
还与Browserslist集成,你可以.browserslistrc
文件来指定目标环境,指定目标环境有什么用?就比如如果你的用户群体的手机浏览器版本都比较低,这时候你就可以在.browserslistrc文件里面配置较低的浏览器环境来支持目标环境。@babel/preset-env预设默认的配置是defaults
他等价于> 0.5%, last 2 versions, Firefox ESR, not dead
,这个默认的配置可以满足大部分用户的使用
- 0.5% 就是包含浏览器市场份额大于0.5%的浏览器 。
- last 2 versions 。就是包含浏览器的最新两个版本,这样子配置是因为最新的两个版本可能不会被包含在市场份额> 0.5% 之中,毕竟刚更新可能没那么多人用,所以这个配置很重要,就算你想自定义browserslist最好也要包含这个配置
- last 2 versions 。就是包含浏览器的最新两个版本,这样子配置是因为最新的两个版本可能不会被包含在市场份额> 0.5% 之中,毕竟刚更新可能没那么多人用,所以这个配置很重要,就算你想自定义browserslist最好也要包含这个配置
- not dead 就是不包含不再安全更新的浏览器,比如ie浏览器。这个配置可以减小打包体积,要知道兼容越多浏览器引入的兼容代码越多包也会越大
需要特别注意的是,Browserslist配置是有优先级的
,默认情况下,@babel/preset-env
将使用 browserslist 配置文件, 除非 在@babel/preset-env配置中设置了 targets 或 ignoreBrowserslistConfig 参数。Browserslist也可以在package.json文件中配置,不过优先级比.browserslist要低。
推荐一个查询浏览器支持率的一个网址:
browsersl.ist/#q=%3E0.3%2…
@babel/preset-env
的一些关键配置
示例图
target
用来配置支持的目标环境,由上文可知优先级比Browserslist高。modules
设置 ES 模块语法到另一个模块类型的转换。(一开始不理解modules的作用,其实这个选项只是决定了在 Babel 处理过程中模块语法的转换方式,最终的输出格式还是由打包工具决定,我们最终打包一般会转为umd格式
)。具体来说,modules
选项有以下几种可能的取值:
-
auto
: 默认值。自动转换,如果环境支持原生 ES6 模块,"auto"
会将模块转换设置为false
,即不进行模块转换。否则,会将模块转换设置为"commonjs"
。这样做可以确保在支持 ES6 模块的环境中不会进行不必要的模块转换,而在不支持 ES6 模块的环境中进行适当的转换以确保代码能够正确运行。false
:表示不进行模块转换。这适用于当你的代码运行在支持 ES6 模块的环境中,比如现代的浏览器或者 Node.js 的一些版本。"commonjs"/"cjs"
:表示将 ES6 模块转换为 CommonJS 模块,适用于在 Node.js 等支持 CommonJS 模块的环境中使用。"amd"
:表示将 ES6 模块转换为 AMD (Asynchronous Module Definition) 格式的模块,适用于一些遗留的浏览器环境或者一些特定的前端工具。"umd"
:表示将 ES6 模块转换为 UMD (Universal Module Definition) 格式的模块,它可以在 AMD、CommonJS 和全局变量引入的环境中运行。"systemjs"
:表示将 ES6 模块转换为 SystemJS 格式的模块。
include
选项用于指定哪些文件需要被@babel/preset-env
进行转译。通常,你可以通过include
选项指定一个匹配文件路径的正则表达式或者一个文件路径的数组,以确保只有特定的文件或者文件夹中的文件会被转译。exclude
选项用于指定哪些文件不需要被@babel/preset-env
进行转译。例如,如果你想要排除项目中的node_modules
文件夹中的 JavaScript 文件不被转译,你可以这样配置:useBuiltIns
用于配置是否使用内置的 polyfill 来按需加载 ECMAScript 新特性的兼容性代码。
为什么要用polyfill? 因为babel-prest-env
仅仅只会转化最新的es
语法,并不会转化对应的Api
和实例方法,比如说ES 6
中的Array.from
静态方法。babel
是不会转译这个方法的,如果想在低版本浏览器中识别并且运行Array.from
方法达到我们的预期就需要额外引入polyfill
进行在Array
上添加实现这个方法。语法层面的转化preset-env
完全可以胜任。但是一些内置方法模块,仅仅通过preset-env
的语法转化是无法进行识别转化的,所以就需要一系列类似”垫片“的工具进行补充实现这部分内容的低版本代码实现。这就是所谓的polyfill
的作用。polyfill在以前我们一般都是使用@babel/polyfill
来实现。
@babel/polyfill 与 @babel/plugin-transform-runtime
@babel/polyfill
库包含 core-js 和一个自定义的 regenerator runtime 来模拟完整的 ES2015+ 环境,使用这个库可以增加兼容性代码。
但是从 Babel 7.4.0 版本开始,@babel/polyfill
包已经不建议使用了,因为他会造成全局污染,我们一般使用 transform runtime (@babel/plugin-transform-runtime)插件代替。
当 useBuiltIns
被设置为 false
时,Babel 将不会自动引入任何 polyfill
。这意味着如果你的代码中使用了某些 ECMAScript 新特性,而目标环境不支持这些特性,那么你需要手动引入对应的 polyfill,以确保代码能够在目标环境中正确运行。
当 useBuiltIns
被设置为 "entry"
时,Babel 会根据你的入口文件(entry file)来确定需要引入的 polyfill。这意味着 Babel 将根据入口文件中使用的 ECMAScript 特性来决定需要引入哪些 polyfill。
由于@babel/polyfill在babel7.4.0被弃用了,因此要想在在入口全部引入的话,可以通过npm install core-js@3 --save然后在入口文件引入import 'core-js/stable'
和import "regenerator-runtime/runtime"
,
如果你旧版babel,而且是core-js@2,则需要import "@babel/polyfill";
此外,useBuiltIns
还可以设置为 'usage'
。这个选项告诉 Babel 根据实际的代码使用情况来确定需要引入的 polyfill。这样可以进一步减小构建后代码的体积,因为只会引入实际使用到的特性的 polyfill,当使用usage
时,我们不需要额外在项目入口中引入polyfill
了,它会根据我们项目中使用到的进行按需引入。
corejs
他只在useBuiltIns: usage
或者useBuiltIns: entry
的时候才有效,其实它就是提供了不同版本的polyfill代码,他的默认值是2,你也可以设置3.x等版本,不过这需要安装对应的core-js版本。
core-js 2.0
版本是跟随@babel/preset-env
一起安装的,不需要单独安装
@babel/plugin-syntax-dynamic-import
是 Babel 的一个插件,用于解析和转换 JavaScript 中的动态 import 语法。如果你的项目中使用了动态 import 语法,那么你需要在 Babel 的配置文件中添加 @babel/plugin-syntax-dynamic-import
插件,以确保 Babel 能够正确解析和处理这些语法。
我们目前项目中的配置为什么是这样子上面已经都做了解释:
解决方案
lerna公共分包的ES6编译问题
为什么lerna项目中的babel配置不生效?
为什么就我们项目中的babel配置不生效?不生效是因为我们的项目采用lerna这种monorepo分包管理,它的babel配置与非monorepo项目有所不同。在babel7.x之后babel增加了一个root根目录的概念,babel会尝试在根目录下找babel.config.js及其它扩展名文件来决定如何编译文件。
优点
- 配置文件与需要处理的实践文件是分离的,所以使用范围非常广
- 通过项目级配置文件可以轻松处理node_modules下或者软链的文件
缺点
- 它依赖于工作目录,如果工作目录不是 monorepo 根目录,在 monorepos 中使用会更痛苦。
babel提供了两种配置文件的方式
- 项目级配置文件
-
babel.config.json
files, with the different extensions (.js
,.cjs
,.mjs
)
- 相对于文件路径的配置文件
-
.babelrc.json
files, with the different extensions (.babelrc
,.js
,.cjs
,.mjs
)package.json
files with a"babel"
key
对于monorepo项目,应该要在整个项目的根目录(root)配置babel.config.json文件,在各个子包中配置.babelrc.json文件,需要注意这种配置需要在babel.config.json中配置babelrcRoots选项子包中的.babelrc.json文件才会生效,下图是相关的解释。
babel-loader仍然无法正常处理公共分包
按照文档说的方式配置完后,发现公共分包的文件还是没有被babel处理到,只能编译当前分包的文件。后面经过一系列尝试,发现在子包的.babelrc中如果不配置rootMode:upward
将无法处理子包外层的目录,rootMode:upward
可以让子包向上级root目录查找 babel.config.js 全局配置,也就能编译上级的目录。
这里还需要特别注意babel配置是有优先级之分的,如果是webpack项目,一般我们都会使用babel-loader对代码进行babel处理,但是我们项目一般会在.babelrc或者babel.config.json文件中配置@babel/preset-env
,如果你在babel-loader中配置了只编译当前子包目录,而在@babel/preset-env
中配置了包含子包目录和外层目录,这时候外层目录是否会生效呢? 后面经过尝试发现babel-loader的优先级会更高,外层目录将不会生效,它的流程应该是webpack -> babel-loader -> preset-env-> babel-core。因此只能修改babel-loader的配置,而babel-loader我们是集成在脚手架里面的,所以只能修改脚手架的配置了,下面是webpack配置链式写法,有需要可以自行转为常规写法。
通过上面的修改之后就可以正常编译公共分包了。
贴一下整体配置
node_modules的ES6编译问题
如何解决项目依赖中代码未转义
本来到这以为大功告成了,打包发布后发现页面还是白屏,于是去找了打包后的dist包看了下,发现公共分包的ES6语法已经被转义了,但是其他文件中还是有...扩展运算符,它指向了mtapp-cache库(一个我们自己封装的内部库),于是开始研究起mtapp-cache,发现mtapp-cache库中已经配置了babel-loader,已经配置了babel-loader为什么还会出现扩展运算符呢?后来在mtapp-cache库生成的min.js文件中发现它引用的一个依赖库object-scan
中包含扩展运算符,object-scan
库没有对代码进行babel处理,要知道babel6是会默认处理node_modules里面的内容的,而在babel7是不是处理node_modules里面的内容的,如果你需要处理node_modules里面的库,你需要像这样单独引入
这个故事告诉我们,如果我们自己开发库,要注意将代码babel处理下,这也是库开发者默认的会做的事情 !!
项目中cjs和esm语法混用可能导致白屏问题
处理完object-scan后,生成的代码也没有扩展运算符了,再次提交代码发包发现还是白屏。控制台报Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'
,经过一番研究发现是mtapp-cache库中存在import和modules.exports也就是esm和cjs混用的情况。
在a文件中使用module.exports导出,在b文件中import a文件,这种操作就会导致上面的报错,解决方法要么就将a文件的module.exports改为export defaults导出,要么就通过配置babel的overrides
,给混入的文件配置sourceType:"unambiguous"
,,让babel
自己去决定是否转换import
语法,该方法通常需要和override
配置配合使用,以避免潜在的问题.
配置完后,项目终于不再白屏。