为了优化首屏加载渲染速度,减小首屏包体积,项目中很多代码是通过懒加载动态导入(dynamic import
)的。
但是在使用时并没有对动态导入的失败做处理,我们通过项目的监控平台发现了上百例因「包资源下载失败导致的页面白屏」,用户无法正常使用。
该情况通常只会在慢网或者 CDN 故障的时候出现,在开发过程中不会注意到这种边界场景
因此,需要一个机制来兜底动态导入失败的场景。
动态导入的使用
使用 webpack 的项目,通常使用动态导入的方法是:
代码语言:javascript复制() => import('./path/to/component')
动态导入会返回一个 promise
对象,并且导入成功时这个 promise 需要 resolve 一个具有默认导出(default exprot)
的模块,但是 reject 并没有得到处理。
以 React 为例,通常我们搭配 React.lazy 来使用动态导入,React.lazy 接受一个返回 promise 的函数。
代码语言:javascript复制const OurComponent = React.lazy(() => import('./OurComponent'));
本文虽然是以 React 为例,但在 Vue 项目的动态导入也可以使用下方的优化方案。
动态导入失败时,lazy 不会隐式处理异常。
所以,我们需要在中间额外引入一层捕获异常并处理。
异常处理
这一层需要做的事有:
- 成功时需要返回一个具有默认导出的模块
- 失败时捕获错误并上报日志
function componentLoader(componentImport) {
return React.lazy(() => {
return new Promise(resolve => {
componentImport()
.then(resolve)
.catch(e => {
// 上报错误日志
});
});
})
}
在这里,有些同学可能会使用比较粗暴的做法,catch 到错误后执行 location.reload()
让页面刷新。
对网络请求这些可能由于非代码逻辑问题导致错误的功能,最好的做法是「增加重试机制提升稳定性」。
如果是慢网的问题,我们需要能进行重试
代码语言:javascript复制function retry(componentImport, retryLeft) {
return new Promise(resolve => {
componentImport()
.then(resolve)
.catch(e => {
if (retryLeft > 1) { // 尝试重新加载
retry(componentImport, retryLeft - 1).then(resolve);
} else {
// 上报错误日志
return;
}
});
});
}
function lazyRetry(componentImport, retryLeft = 3) {
return React.lazy(() => retry(componentImport, retryLeft));
}
引入后使用方式变更为:
代码语言:javascript复制const OurComponent = lazyRetry(() => import('./OurComponent'));
如果是 CDN 故障,我们需要能换 CDN 重试
webpack 懒加载的原理,是在需要时,向 dom 插入一个 script
标签,在 script 加载成功时(onload)调用动态导入 promise 的 resolve,并带上加载的资源,在失败时(onerror
)调用 reject。
webpack 懒加载源码
换 CDN 的原理其实就是在 scriptA 加载失败后插入新的 scriptB,scriptC ... 重新加载。
所以在 scriptA 加载失败时,「要让原本的 onerror 不执行」,避免让 promise 改态(因为 promise 是不可逆的),「将本来该执行的 onerror 赋给 scriptB」。
因为生产环境会使用 mini-css-extract-plugin
将样式单独提取为一个 css 文件,所以样式加载失败时需要做类似 script 的处理,「也不能触发 link 标签的 onerror」。
https://github.com/Nikaple/assets-retry 这个 package 实现了「无侵入式的静态资源自动重试」,它的原理是 hook 原生的 document.createElement
,script.onerror
等方法,不侵入原生逻辑的同时插入重试相关代码,并捕获监听 document 的 error 事件做相应处理,我们可以在项目中直接引入这个包来实现 CDN 重试。
仍然无法加载回资源
有了以上的处理,但资源仍然无法加载回来,此时错误并不会抛出,只是页面上不展示资源对应的功能,用户仍然可以正常使用页面,不会白屏。
总结
通过针对业务优化场景中遇到的懒加载失败问题,我们分析了 webpack 源码,借助了 import()
网络重试加载机制,提高了程序的鲁棒性,降低前端白屏率,一定程度上提升了用户体验,对于前端工程的收益较为明显。
❤️感谢
如果本文对你有帮助,点赞