前端异常监控和容灾


异常就是程序出现了意料之外的情况,影响了程序最终的呈现结果。所以我们开发的时候就非常有必要未雨绸缪,进行异常监控,以应对突如其来的问题

既可以增强用户体验,我们开发者也能远程定位问题,尤其是移动端

尽管对 JS 而言,异常一般只会使当前执行的任务中止,基本不会导致崩溃

可异常监控却是一个完善的前端方案必须具备的

接下来就针对我们前端,需要做的异常一一说明

异常监控

JS 执行异常

  • 使用try-catch的话捕捉不到具体语法错误和异步错误,所以推荐用在可预见情况下的错误监控
  • 使用 window.onerror ,比 try-catch 强,不过也捕获不到资源加载异常或者接口异常,推荐用来捕获预料之外的错误

两者结合更好

image.png

收集到的错误信息打印出来是这样子的

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
window.onerror = function (msg, url, row, col, error) {
console.table({ msg, url, row, col, error: error.stack })
let errorMsg = {
type: 'javascript',
// msg错误消息,error是错误对象,这里拿的是error.stack(异常信息)
msg: error && error.stack ? error.stack || msg,
// 发生错误的行数
row,
// 列数,也就是第几个字符
col,
// 发生错误的页面地址
url,
// 发生错误的时间
time: Date.now()
}

// 然后可以把这个 errorMsg 存到一个数组里,统一上报
// 也可以直接上报
Axios.post({ 'https://xxxx', errorMsg })

// 如果return true,错误就不会抛到控制台
}

上报有两种方式,一种是如上面代码中的用 AJAX,会有跨域所以需要服务端支持;还有一种是用 Image 对象,这有一个好处就是图片请求没有跨域;注意 URL 长度不要超过限制就行。后面的例子中就不一一列举了

1
2
let url = 'https://xxx' + '错误信息'
new Image.src = url

资源加载异常

使用 addEventListener('error', callback, true) 在捕获阶段捕捉资源加载错误信息,然后上报服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
addEventListener('error', e => {
const targe = e.target
if(target != window){
//这里收集错误信息
let errorMsg = {
type: target.localName, // 错误来源名称。比如图片这里就是'img'
url: target.src || target.href, //错误来源的链接
// .... 还需要其他信息可以自己补充
}
// 把这个 errorMsg 存到一个数组里,然后统一上报
// 或者直接上报
Axios.post({ 'https://xxxx', errorMsg })
}
}, true)

Promise 异常

unhandledrejection

使用 addeventListener('unhandledrejection',callback)捕获 Promise 错误。不过捕捉不到行数,触发时间在被 reject 但没有 reject 处理的时候,可能发生在 window 下,也可能在 Worker 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
window.addEventListener("unhandledrejection", (e) => {
console.log(e)
let errorMsg = {
type: 'promise',
msg: e.reason.stack || e.reason
// .....
}
Axios.post({ 'https://xxxx', errorMsg })

// 如果return true,错误就不会抛到控制台
})
new Promise(() => {
s
})

打印出来是这么个东西

image.png

rejectionhandled

Promise 错误已被处理会触发这个

1
2
3
4
5
6
window.addEventListener("unhandledrejection", (e) => {
console.log("错误了")
})
window.addEventListener("rejectionhandled", (e) => {
console.log("错误已经处理了")
})

Vue 异常

errorHandle

Vue 为组件呈现函数和监视程序期间没有捕获的错误分配的一个处理程序。不过这个方法一旦捕获取错误后,错误就不会抛到控制台

1
2
3
4
5
6
7
8
Vue.config.errorHandler = (err, vm, info) => {
// err 错误处理
// vm vue实例
// info 是特定于vue的错误信息,比如哪个生命周期勾子

// 如果需要把错误抛到控制台,需要在这里加上这一行
console.error(err)
}

warnHandle

是 Vue 警告分配一个自定义处理程序。不过只在开发环境有效,生产环境会被自忽略

1
2
3
Vue.config.warnHandle = (msg, vm, trace){
// trace 是组件层次结构
}

renderError

默认的渲染函数遇到错误时,提供了一个代替渲染输出的。这个和热重新加载一起用会很棒

1
2
3
4
5
6
7
8
new Vue({
render(h) {
throw new Error("oops")
},
renderError(h, err) {
return h("per", { style: { color: red } }, err.stack)
},
}).$mount("#app")

errorCaptured

任何派生组件捕获错误时调用。它可以 return false 来阻止错误传播。可以在这个勾子里修改组件状态。不过如果是在模板或呈现函数里有条件语句,在捕获到错误时,这些条件语句会短路,可能进入一个无限渲染循环

1
2
3
4
5
6
7
8
9
Vue.component('ErrorBoundary',{
data: () => { ... }
errorCaptured(err, vm, info){
// err 错误信息
// vm 触发错误的组件实例
// info 错误捕获位置信息
return false
}
})

React 异常

getDerivedStateFromError

React 也有自带的捕获所有子组件中错误的方法,这个生命周期会在后代组件抛出错误时被调用。注意这个是在渲染阶段调用的,所以不允许出现副作用

1
2
3
4
5
6
7
8
9
10
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显降级 UI
return { hasError: true }
}
}

componentDidCatch

这个生命周期也会在后代组件抛出错误时被调用,但是不会捕获事件处理器和异步代码的异常。它会在【提交】阶段被调用,所以允许出现副作用

1
2
3
4
5
6
7
8
9
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
}
componentDidCatch(error, info) {
// error 错误信息
// info.componentStack 错误组件位置
}
}

前端容灾

前端容灾指的因为各种原因后端接口挂了(比如服务器断电断网等等),前端依然能保证页面信息能完整展示。

比如 banner 或者列表之类的等等数据是从接口获取的,要是接口获取不到了,怎么办呢?

LocalStorage

首先,使用 LocalStorage

在接口正常返回的时候把数据都存到 LocalStorage ,可以把接口路径作为 key,返回的数据作为 value

然后之次再请求,只要请求失败,就读取 LocalStorage,把上次的数据拿出来展示,并上报错误信息,以获得缓冲时间

CDN

同时,每次更新都要备份一份静态数据放到 CDN

在接口请求失败的时候,并且 LocalStorage 也没有数据的情况下,就去 CDN 摘取备份的静态数据

Service Worker

假如不只是接口数据,整个 html 都想存起来,就可以使用 Service Worker 做离线存储

利用 Service Worker 的请求拦截,不管是存接口数据,还是存页面静态资源文件都可以

1
2
3
4
5
6
7
8
9
10
11
12
// 拦截所有请求事件 缓存中有请求的数据就直接用缓存,否则去请求数据
self.addEventListener("fetch", (e) => {
// 查找request中被缓存命中的response
e.respondWith(
caches.match(e.request).then((response) => {
if (response) {
return response
}
console.log("fetch source")
})
)
})

做好这些,整个网站就完全可以离线运行了,但是问题也很明显,就是时效性较高的页面可能会有数据无法同步更新的问题(比如商家库存不足了显示不一致)

另外要注意的是要保证前端页面自身可发布更新,比如页面异常后,此时业务系统要发新版本进行修复和更新,要能确保新版本的资源可以全量替换旧版本的线上资源


文章作者: 沐华
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 沐华 !