导读: 前端白屏指页面在加载过程中长时间无法正常展示内容,内容区空白,使用户无法进行查看、保存等一切操作,这是非常严重的问题。如果能尽早检测到白屏问题,就可以及时处理,避免或降低负面影响。白屏的检测手段有两种。一是真实用户端的检测,通过接入白屏检测 SDK 实现,无法在用户端白屏报错前发现问题,是被动监控的方式;另一种是自动化检测,在团队内部通过自动化工具模拟用户行为主动检测,可以提前发现问题。本文为前端白屏检测的上篇,主要讲真实用户端的检测,即 SDK 的设计与实现。
文 | 网易云商
白屏的表现与原因
白屏的通常表现为:
页面空白或仅显示背景色,没有实际内容。
页面一直展示骨架屏,包括页面 loading 状态。
页面只展示导航菜单,内容区空白,包括微前端或 iframe 嵌套子页面的场景。
导致白屏的原因分两种:资源加载错误、代码执行错误。
检测方案对比
方案 | 原理 | 优点 | 缺点 |
检测根节点是否渲染 | SPA框架渲染的 DOM 一般挂载在一个根节点下,监听onload、onerror事件,检测根节点下是否挂载 DOM | 开发成本低 | 通用性差,只兼容主流 SPA 框架 |
监听 DOM 变化 | 利用 Mutation Observer API 监听DOM变化 | 开发成本低 | 准确度低,无法检测未渲染、始终渲染骨架屏等情况,如果用户长时间未操作DOM可能会误判白屏 |
页面截图对比 | 对页面截图,将截图与纯白的图片做对比 | 技术栈无关,通用性好 | 准确度低,无法检测纯背景色、骨架屏的白屏场景 |
前端框架内置ErrorBoundary组件捕获异常 | 利用ErrorBoundary组件捕获JS执行异常检测白屏 | 开发成本低 | 无法检测资源异常导致的白屏,只兼容于特定框架应用,接入时对业务代码侵入大 |
页面关键点采样对比 | 在页面中垂直/交叉取多个采样点,用 elementsFromPoint API 获取采样点下的 HTML 元素,判断采样点元素是否与容器元素相同 | 准确度高,技术栈无关,通用性好 | 开发成本稍高 |
以下是 elementsFromPoint API 的 MDN 介绍:
elementsFromPoint(x, y) 方法返回指定坐标(相对于视口)处的所有元素的数组。元素从视口的最顶部到最底部的盒模型排序。
通过以上对比发现,采用「页面关键点采样对比」的实现方案较好。
需要注意的是,对于主应用内嵌入的 iframe 的场景,因为每次采样取到的都是整个 iframe 元素,所以无法在主应用侧判断 iframe 是否白屏,需要在 iframe 应用内接入白屏检测 SDK。
流程图
数据采集
屏幕采样点选取
采样点的选取有三种方式:垂直采样、交叉采样、垂直交叉采样。
垂直采样
垂直采样时屏幕的采样点坐标
交叉采样
交叉采样时屏幕的采样点坐标
垂直交叉采样
垂直交叉采样时屏幕的采样点坐标
很明显,采样点越多判断越准确,但计算量稍大一点,不过我们利用 requestIdleCallback 在浏览器空闲时计算,因此,我们选择垂直交叉的采样方式。
白屏的判断标准与检测时机
有骨架屏和无骨架屏应用的检测方式不一样,检测时机也有细微差别。
无骨架屏场景
1. 检测时机
document.readyState 在 complete 时或 load 事件触发时。
全局 error 事件触发时。
全局 unhandledrejection 事件触发时。
2. 检测方式
初始化 SDK 时,我们需要配置哪些是根容器,如果根容器为空则说明是白屏。
具体实现方式为,根据屏幕的宽度(window.innerWidth)和高度(window.innerHeight)算出每个采样点的具体坐标,再用 elementsFromPoint 获取每个坐标的 dom 元素,对比获取的元素是否为配置的根容器元素。
仔细想一下,上面的判断方式其实会有问题。
因为在微前端与 iframe 场景下,子应用白屏时,应该也需要上报才对。如果按上述方式判断,主应用(一般包含导航或者一级菜单)如果没有白屏,子应用永远不会被检测出白屏。因此,需要兼容此类场景。
兼容方式也很简单,我们只要判断内容区内的采样点满足白屏条件即可。大部分后台类的应用,会有顶部导航或左侧的一级菜单,因此我们选定右下方为内容区。
3. 内容区内的采样点
如上图所示,整个屏幕共 33个 采样点,其中内容区有 28 个。简单起见,检测白屏时,我们判断空白的采样点是否大于等于 28 个。采样点坐标的获取如下:
for (let i = 1; i <= 9; i++) {
// x轴采样点
const xElements = document?.elementsFromPoint((window.innerWidth * i) / 10, window.innerHeight / 2);
// y轴采样点
const yElements = document?.elementsFromPoint(window.innerWidth / 2, (window.innerHeight * i) / 10);
// 上升的对角线采样点
const upDiagonalElements = document?.elementsFromPoint(
(window.innerWidth * i) / 10,
(window.innerHeight * i) / 10,
);
// 下降的对角线采样点
const downDiagonalElements = document?.elementsFromPoint(
(window.innerWidth * i) / 10,
window.innerHeight - (window.innerHeight * i) / 10,
);
if (this.isContainer(xElements[0] as HTMLElement)) emptyPoints++;
// 中心点只计算一次
if (i !== 5) {
if (this.isContainer(yElements[0] as HTMLElement)) emptyPoints++;
if (this.isContainer(upDiagonalElements[0] as HTMLElement)) emptyPoints++;
if (this.isContainer(downDiagonalElements[0] as HTMLElement)) emptyPoints++;
}
}
有骨架屏场景
1. 检测时机
document.readyState在complete 之前。
全局 error 事件触发时。
全局 unhandledrejection 事件触发时。
2. 检测方式
如果应用内有骨架屏,继续用无骨架屏应用的白屏检测方式已经无法判断白屏,因为骨架屏也是有效的 dom 元素。
有骨架屏应用的检测方式为:对比初次采样前后获取的 dom 元素是否一致。因为在页面加载完成前可能已经渲染完骨架屏,为了获取对照组数据,初次采样的时间要在页面加载完成前。
// 项目有骨架屏
if (this.isSkeletonApp) {
if (document.readyState !== 'complete') {
this.idleCallback({
type: ErrorType.BEFORE_COMPLETE,
message: '骨架屏场景白屏',
});
}
} else {
// 页面加载完毕
window.addEventListener(
'load',
this.idleCallback.bind(this, {
type: ErrorType.LOAD,
message: '页面加载完毕白屏',
}),
);
}
const errorHandler = (e: any) => {
if (this.debug) {
console.log('[WhiteScreenSDK] 捕获 error 错误: ', e);
}
this.idleCallback({
type: e.type,
message: e.message,
filename: e.filename,
lineno: e.lineno,
colno: e.colno,
error: e.error,
});
};
const unhandledRejectionHandler = (e: any) => {
if (this.debug) {
console.log('[WhiteScreenSDK] 捕获 unhandledrejection 错误: ', e);
}
this.idleCallback({
type: e.type,
reason: e.reason?.msg || e.reason,
message: 'Promise 异常',
});
};
const rejectionHandledHandler = (e: any) => {
if (this.debug) {
console.log('[WhiteScreenSDK] 捕获 rejectionhandled 错误: ', e);
}
this.idleCallback({
type: e.type,
reason: e.reason?.msg || e.reason,
message: 'Promise 异常',
});
};
window.addEventListener('error', errorHandler, true);
window.addEventListener('unhandledrejection', unhandledRejectionHandler);
window.addEventListener('rejectionhandled', rejectionHandledHandler);
window.addEventListener('beforeunload', () => {
window.removeEventListener('load', this.idleCallback);
window.removeEventListener('error', errorHandler, true);
window.removeEventListener('unhandledrejection', unhandledRejectionHandler);
window.removeEventListener('rejectionhandled', rejectionHandledHandler);
});
数据上报
检测出白屏问题后,就要上报白屏信息到数据后台了。一般数据后台需要有数据清洗、存储、消费、告警等功能。此外,还需要区分不同的产品与环境,控制上报数据并发量、上报用户浏览器信息、用户行为数据、方便排查问题的 Sourcemap,告警方式与规则等细节问题。如果要将数据后台做得全面细致,实现成本是比较高的。
因为目前我们在使用的云音乐部门同事研发的前端错误监控平台 Corona,可以满足我们的需求,因此我们将 Corona 作为了白屏检测上报的数据后台。我们要做的就是将上报白屏错误到 Corona 的逻辑,内置到白屏检测 SDK 中。
SDK 的接入方式
白屏检测 SDK 支持以外链方式接入前端 Web 应用,除业务方使用的监控后台脚本外(如云音乐的 Corona SDK),不依赖其他资源加载,一般只需要改动模板文件,不侵入业务代码。
注意,不建议通过 npm 包的方式接入白屏检测 SDK,因为这种方式在入口资源加载后才会初始化 SDK,无法检测到入口资源加载异常导致的白屏。
SDK API
配置项
字段名 | 类型 | 说明 | 是否必须 | 默认值 |
containers | String[] | 需要检测白屏的容器选择器列表,除html和body直接使用标签名优先级最高外,其他元素选择器的优先级:id > class > nodeName。字符全小写,如body,#id,.class等 | 否 | ['html', 'body', '#app', '#root'] |
onWhiteScreen | (err) => void | 发现白屏后的回调函数,默认上报到云音乐的Corona平台 | (err) => { window.corona.error('白屏错误', err); } | |
isSkeletonApp | Boolean | 是否是有骨架屏的应用 | 否 | FALSE |
autoInit | Boolean | 是否自动初始化SDK | 否 | TRUE |
debug | Boolean | 是否开启调试模式,开启后会打印日志 | 否 | FALSE |
interval | Number | 白屏检测间隔,单位毫秒 | 否 | 1000 |
thresholdOnLoad | Number | 页面加载完成时,上报白屏错误的阈值,达到此白屏次数后才上报,避免页面加载慢的场景误报白屏 | 否 | 5 |
方法
方法 | 类型 | 说明 | 参数 | 返回值 |
init | () => void | 初始化SDK | 无 | 无 |
SDK 接入示例
<script>
window.env = 'test'; // 当前运行环境
// Corona错误监控SDK
!function(e,n,t,s,c){var r=void 0!==t&&t.resolve,a=e[s];(a=e[s]=function(){this.modules={}}).callbacks=[],a.ready=r?function(){return a.instance?t.resolve(a.instance.vars()):new t(function(e){return a.callbacks.push(e)})}:function(e){return a.instance?e(a.instance.vars()):a.callbacks.push(e)};var i=n.createElement(c),u=n.getElementsByTagName(c)[0];i.async=!0,i.src=`https://s6.music.126.net/puzzle/puzzle@xxxxxx.js`,u.parentNode.insertBefore(i,u)}(window,document,window.Promise,"puzzle","script");
// 白屏检测SDK配置
window.WHITE_SCREEN = {
containers: ['html', 'body', '#app', '#react-content'],
debug: window.env !== 'prod'
};
// Corona初始化完成后,再加载白屏检测SDK
document.addEventListener('CoronaInited', function () {
var wsSDK = document.createElement('script');
wsSDK.async = !0;
wsSDK.src = 'https://res.qiyukf.net/umd/white-screen-sdk/prod_v1.0.0.js';
var firstScript = document.getElementsByTagName('script')[0];
firstScript.parentNode.insertBefore(wsSDK, firstScript);
});
</script>
SDK 的能力说明
本实现方案有一定的局限性,部分场景无法检测到白屏,部分能力需要借助外部的错误捕获。
无法检测到的白屏场景
需复用外部错误捕获才可实现的能力
因为业务代码中未抛出的错误,无法通过常规的全局监听完成错误捕获,跨域的脚本运行错误时因浏览器限制无法获取详细堆栈信息。
为解决上述问题,对接入错误监控的业务方侵入较小的方式是,通过劫持 prototype 做错误捕获,但此方式对性能有影响。而错误监控平台一般包含此能力,为减少对性能影响,SDK 内不再内置此能力。
总结
本文首先介绍了前端白屏表现、白屏原因,以及修复白屏问题的业务价值。然后对比几种常见的白屏检测方案,并介绍了采样点检测方案的具体实现,包括采样点如何选取、白屏的判断标准与检测时机、微前端与 iframe 场景的兼容等。
白屏检测工具完善现有的质量保障体系。使我们能尽早发现、及时处理白屏问题,减少线上重大故障机率,降低白屏问题对客户的负面影响。下一篇文章将介绍如何自动化的检测白屏,模拟用户的行为,主动发现问题。
参考资料
MDN:DocumentOrShadowRoot.elementsFromPoint():
https://developer.mozilla.org/zh-CN/docs/Web/API/Document/elementsFromPoint
前端白屏的检测方案,让你知道自己的页面白了:
https://juejin.cn/post/7176206226903007292
推荐好友活动
关于我们