决策源于数据,数据源于采集。 一个项目上线后线上运行情况,用户使用情况,pv/uv,设备分布情况,用户城市分布,线上异常情况等等信息都需要知晓,便于产品及运营同学进行下一步决策,同时也便于开发同学及时发现线上代码异常等问题。对于开发来讲,异常监控便是其中最重要的,今天就先来聊聊前端代码异常的数据采集。
如何捕获异常
JS异常的异常的特点是即使出现异常,一般不会导致浏览器崩溃,最多只是当前任务被迫终止。比如一个页面多个按钮,其中一个按钮报错了,其他按钮还可以正常使用。又或者一部分代码报错,其他代码还能依旧运行。如下:
如果用户不打开控制台,就根本不会知道第一部分的代码报错了。那么错误该如何采集呢。
try-catch
最容易想到的便是try-catch语句:
由上可以看见 被try-catch语句包裹的错误会被捕获,并打印出来。但是异步代码块(如setTimeout)内部的错误不会被捕获。其实这里对于一个插件或者第三方包而言,try-catche内部自己捕获的 就是自己捕获了,第三方包无法获得,因为从代码运行角度来看,这个错误已经被内部处理了,不算是代码异常了。 不过,对于异步的那个错误,我们是可以处理的,请看下面。
window.onerror
window.onerror 最大的好处就是可以同步任务还是异步任务都可捕获。
这里会发现 虽然异常被捕获了,但是错误还是会出现在控制台。这是因为window.onerror需要一个返回值,添加上 return true即可。
如图,错误消失。
window.addEventListener
window.onerror固好,也有其办不到的事,如网络异常。
<img src="https://ctrip.com/zhangxuefeng.png">
上面这张图片地址为404,控制台报错如下:
这个时候就需要请出window.addEventListener了。
对于promise以及async/await的异常捕获,window.addEventListener也可以处理,只不过要注意的是,这个时候监听的不是error了,而是unhandledrejection,如下:
window.addEventListener('unhandledrejection',function(){...},true)
React项目异常采集
react官方给我们提供了错误处理的方案:
error—— 抛出的错误。info—— 带有componentStackkey 的对象,其中包含有关组件引发错误的栈信息。
需要注意的是,React 的开发和生产构建版本在 componentDidCatch() 的方式上有轻微差别。
-
在开发模式下,错误会冒泡至
window,这意味着任何window.onerror或window.addEventListener('error', callback)会中断这些已经被componentDidCatch()捕获的错误。 -
相反,在生产模式下,错误不会冒泡,这意味着任何根错误处理器只会接受那些没有显式地被
componentDidCatch()捕获的错误。
为了对错误做出相应的降级处理,static getDerivedStateFromError()生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// 更新 state 使下一次渲染可以显降级 UI
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
// 渲染任何自定义的降级 UI
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
getDerivedStateFromError会在渲染阶段调用,因此不允许出现副作用。如遇此类情况,请改用componentDidCatch()。
小程序错误采集(以微信小程序为例)
小程序相对PC H5页面等相对封闭些,也没有window对象,其基于App()与Page()方法来注册页面,发送请求也是封装了一个wx.request。我们要实现全局的错误采集,可以扩展Page及wx.request方法,在自己的小程序里试验了一下,项目结构如图:
用ZPage代替Page,zwx代替wx对象,代码如下:
// ZPage.js
import util from './utils/util';
import zwx from './zwx'
const ZPage = (options)=>{
const pageData = {
onLoad: (onloadOptions)=>{
console.log('在这里统计pv uv cid 等信息')
console.log(zwx)
options.onLoad.call(this, onloadOptions)
}
}
if(options.data) {
pageData.data = util.deepCopy(options.data)
delete options.data
}
if(options.onShareAppMessage) {
try {
let _onShareAppMessage = options.onShareAppMessage
pageData.onShareAppMessage = (res) =>{
let shareData = _onShareAppMessage.call(this, res)
console.log(shareData)
}
delete options.onShareAppMessage
} catch(err) {
}
}
for(let key in options) {
if(typeof options[key] == 'function' &&
pageData[key] == undefined) {
pageData[key] = function() {
try{
return options[key].call(this, ...arguments)
}catch(e){
const errInfo = '发生了错误!页面:' + this.__route__ + ',
方法:' + key + ',错误信息:' + e.message
console.log(errInfo)
// 将信息上报
//...
}
}
}
}
Page(pageData)
}
module.exports = {
ZPage
}
// zwx.js
let zwx=Object.assign({},wx);
zwx.request = (cfg) =>{
let _header = cfg.header || {}
// 处理header
_header.cookie = 'name=zxf;' + (_header.cookie || '')
let head = {
cid: 666,
auth: 'sss',
sence: 1024
}
let zcfg = JSON.parse(JSON.stringify(cfg))
zcfg.header = _header
if(!zcfg.data) {
zcfg.data = {}
}
if(typeof zcfg.data == 'string') {
// 处理传入参数为字符串的情况
}
Object.assign(zcfg.data, {
head: head
})
zcfg.success = (res) =>{
console.log('请求成功了',res)
return cfg.success(res)
}
zcfg.fail = (err) =>{
console.log('请求失败了',err)
console.log('此处记录下失败的请求内容 及返回内容')
console.log(cfg)
return cfg.fail(err)
}
wx.request(zcfg)
}
module.exports = zwx
使用如下:
// index.js
import {ZPage} from '../../zwx/ZPage'
import zwx from '../../zwx/zwx'
const app = getApp()
ZPage({
data: {
name:'zxf'
},
onLoad: function(options) {
console.log(options)
zwx.request({
url:'https://baidu.com',
success: res =>{
},
fail: err =>{
}
})
},
test() {
console.log(123)
},
buttonTest(e) {
this.setData({
name:'zxf222'
})
err
}
})
对页面内所有方法进行了错误监听,对请求进行了错误监听及对请求参数可以进行统一的处理。
错误上报
错误上报一般采用新建一个图片,src属性上赋值需要上报的数据进行上报。因为这种方式无跨域问题,兼容性,适用性较好。如下:
tips
如果当前页面需要采集信息还没上报完毕,页面以及跳转走,比如用户点击按钮,需要上报一些信息(不一定是错误),但是这个按钮是跳转性质的,该如何处理?
- onunload: 但是可能导致页面卡住或者卡顿
- navigator.sendBeacon(url, data):使用
sendBeacon()方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。(目前大部分浏览器都成功实现了该功能)