当有人问起:你们的公司的这款应用用户体验怎么样呀?访问量怎么样?此时,你该怎么回答呢?你会回答:UV、PV 巴拉巴拉,秒开率、FP、TTI 巴拉巴拉。
那么,这些数据是哪里来的呢?显而易见,这些数据都来自前端监控系统。
前端监控的意义
当今时代,是一个快节奏的时代,应用的性能极大影响着用户的留存率,没有用户会忍受一个卡到爆的应用。而监控应用性能的重担,就由前端监控系统肩负着。
其次,对于线上应用来说,故障是不可避免的,对于高日活的应用来说,每次故障都意味着大量的损失。试想,如果是淘宝挂了一天,那么损失是多么惨痛。所以,对于开发人员来说,必须要尽早发现线上故障,而不是等到客户打爆客服的电话才发现。线上错误监控,也是前端监控的任务之一。
最后,作为商业公司,需要根据用户行为和数据进行分析,进一步制定各种策略,如果没有各种数据,那么 BI 会热情的找你谈谈人生。而这些数据,也是前端监控系统获取的。
总而言之,前端监控肩负着:性能监控、错误监控以及数据上报等功能,无论对于大公司还是小公司,可以说是必不可缺的了。
今天,我们先来聊聊前端监控中的错误监控。
错误监控概述
一般来说,按照错误监控错误监控可以分为:脚本错误监控、请求错误监控以及资源错误监控。
脚本错误监控
脚本错误大体可以分为两种:编译时错误以及运行时错误。其中,编译时错误一般在开发阶段就会发现,配合 lint 工具比如 eslint、tslint 等以及 git 提交插件比如 husky 等,基本可以保证线上代码不出现低级的编译时错误。大厂一般都有发布前置检测平台,能够在发布前提前发现编译时错误,当然,原理依然和之前所说的类似。
而发现并上报运行时错误就是前端检测平台的本质工作啦,一般来说,脚本错误监控指的就是运行时错误监控。
说到脚本错误监控,你想到的第一个是什么?对,就是 try catch
!
在编写 JavaScript 时,我们为了防止出现错误阻塞程序,我们会通过 try catch
捕获错误,对于错误捕获,这是最简单也是最通用的方案。
但是,try catch
捕获错误是侵入式的,需要在开发代码时即提前进行处理,而作为一个监控系统,无法做到在所有可能产生错误的代码片段中都嵌入 try catch
。所以,我们需要全局捕获脚本错误。
常规脚本错误
当页面出现脚本错误时,就会产生 onerror
事件,我们只需捕获该事件即可。
/**
* @description window.onerror 全局捕获错误
* @param event 错误信息,如果是
* @param source 错误源文件URL
* @param lineno 行号
* @param colno 列号
* @param error Error对象
*/
window.onerror = function (event, source, lineno, colno, error) {
// 上报错误
// 如果不想在控制台抛出错误,只需返回 true 即可
};
可以发现,各种错误监控所需的信息,如错误信息、错误源文件的 URL、错误行号、错误列号都被回调函数所传入。
但是,window.onload
有两个缺点:
- 只能绑定一个回调函数,如果想在不同文件中想绑定不同的回调函数,
window.onload
显然无法完成;同时,不同回调函数直接容易造成互相覆盖。 - 回调函数的参数过于离散,使用不方便
所以,一般情况下,我们使用 addEventListener
来代替。
/**
* @param event 事件名
* @param function 回调函数
* @param useCapture 回调函数是否在捕获阶段执行,默认是false,在冒泡阶段执行
*/
window.addEventListener('error', (event) => {
// addEventListener 回调函数的离散参数全部聚合在 error 对象中
// 上报错误
}, true)
tips:在一些特殊情况下,我们依然需要使用 window.onload
。比如,不期望在控制台抛出错误时,因为只有 window.onload
才能阻止抛出错误到控制台
Promise 错误
使用了这两种方法,是不是可以捕获所有脚本错误了呢?这个问题再几年前其实是正确的,但是随着前端技术的发展,出现了 Promise
这项技术,而使用这两种常规方法无法捕获 Promise
错误。
和常规脚本错误的捕获一样,我们只需捕获 Promise
对应的错误事件即可。而 Promise
错误事件有两种,unhandledrejection
以及 rejectionhandled
。
当 Promise
被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection
事件。
当 Promise
被 reject 且有 reject 处理器的时候,会触发 rejectionhandled
事件。
// unhandledrejection 推荐处理方案
window.addEventListener('unhandledrejection', (event) => {
console.log(event)
}, true);
// unhandledrejection 备选处理方案
window.onunhandledrejection = function (error) {
console.log(error)
}
// rejectionhandled 推荐处理方案
window.addEventListener('rejectionhandled', (event) => {
console.log(event)
}, true);
// rejectionhandled 备选处理方案
window.onrejectionhandled = function (error) {
console.log(error)
}
框架错误
由于我 React 使用的不多,所以在此只讨论下 Vue 的框架错误处理,如果有大佬了解 React 的框架错误处理,欢迎补充~
在 Vue 中,框架提供了 errorHandler 这个 API 来捕获并处理错误。
Vue.config.errorHandler = function (err, vm, info) {
// handle error
// `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
// 只在 2.2.0+ 可用
}
值得一提的是,框架错误指的不是框架层面的错误,而是指框架提供了 API 来捕获全局错误。
请求错误监控
一般来说,前端请求有两种方案,使用 ajax
或者 fetch
,所以只需重写两种方法,进行代理,即可实现请求错误监控。
代理的核心在于使用 apply
重新执行原有方法,并且在执行原有方法之前进行监听操作。在请求错误监控中,我们关心三种错误事件:abort
,error 以及 timeout
,所以,只需在代理中对这三种事件进行统一处理即可。
tips:如果能够统一使用一种请求工具,如 axios
等,那么不需要重写 ajax
或者 fetch
只需在请求拦截器以及响应拦截器进行处理上报即可
资源错误监控
资源错误监控本质上和常规脚本错误监控一样,都是监控错误事件实现错误捕获。
那么如果区分脚本错误还是资源错误呢?我们可以通过 instanceof
区分,脚本错误参数对象 instanceof
ErrorEvent
,而资源错误的参数对象 instanceof
Event
。
值得一提的是,由于 ErrorEvent
继承于 Event
,所以不管是脚本错误还是资源错误的参数对象,它们都 instanceof
Event
,所以,需要先判断脚本错误。
此外,两个参数对象之间有一些细微的不同,比如,脚本错误的参数对象中包含 message
,而资源错误没有,这些都可以作为判断资源错误或者脚本错误的依据。
/**
* @param event 事件名
* @param function 回调函数
* @param useCapture 回调函数是否在捕获阶段执行,默认是false,在冒泡阶段执行
*/
window.addEventListener('error', (event) => {
if (event instanceof ErrorEvent) {
console.log('脚本错误')
} else if (event instanceof Event) {
console.log('资源错误')
}
}, true);
tips:使用 addEventListener
捕获资源错误时,一定要将 useCapture 即第三个选项设为 true,因为资源错误没有冒泡,所以只能在捕获阶段捕获。同理,由于 window.onerror
是通过在冒泡阶段捕获错误,所以无法捕获资源错误。
补充:跨域脚本错误捕获
为了性能方面的考虑,我们一般会将脚本文件放到 CDN ,这种方法会大大加快首屏时间。但是,如果脚本报错,此时,浏览器出于于安全方面的考虑,对于不同源的脚本报错,无法捕获到详细错误信息,只会显示 Script Error
。那么,有解决该问题的方案吗?
- 方案一:所有的脚本全部放到同一源下,但是,该方案会放弃
CDN
,降低性能。 - 方案二:在
script
标签中,添加crossorigin
属性(推荐使用webpack
插件自动添加);同时,配置CDN
服务器,为跨域脚本配上CORS
。
可以发现,方案二基本可以完美解决跨域脚本错误捕获的问题。但是,其实该方案有一个隐藏的坑,即兼容性问题,crossorigin
属性对于 IE 以及 Safari 支持程度不高。
所以,该如何真正完美的解决跨域脚本错误捕获问题?
终极解决方案:对所有原生方法进行代理~
但是,一方面,很难覆盖所有的原生方法,另一方面,对原生方法进行代理容易出现无法预知的问题。
综合所有方案,看起来还是方案二最靠谱,至于低级浏览器,就让它们随风消逝吧~
如果有不同想法的同学,欢迎一起交流~
我的 github:github.com/KarthusLori…