Vue3 另一种异常处理Hook: 可冒泡的onErrorCaptured异常处理有多重要? 前两天领导急冲冲跑过来跟我
异常处理有多重要? 前两天领导急冲冲跑过来跟我说,新客户反馈说我们平台太慢,数据半天都不显示,叫我赶紧排查下性能问题。前、后端同学花了一小时,看请求日志、看代码,硬是没找出任何问题,实在没办法,几经周折找到客户联系方式,远程看现场情况,结果是浏览器兼容问题:list.findLast(...)中findLast未定义
。
对于未处理异常,界面无交互响应,用户懵逼,研发不知道具体问题,也懵逼。由于项目使用的是Vue框架,那咱们就趁机了解下Vue提供的异常处理Hook onErrorCaptured
,先看应用,再说原理。
onErrorCaptured定义:注册一个钩子,在捕获了后代组件传递的错误时调用。 当后代组件发生异常,父组件可通过onErrorCaptured
捕获,如下demo:
// child.vue:
<script lang="ts" setup>
throw new Error('获取数据发生未知异常!')
</script>
// parent.vue:
<sript lang="ts" setup>
onErrorCaptured((err) => {
console.error(err)
return false
})
</script>
parent.vue通过onErrorCaptured
捕获了后代组件异常,如果返回false
则中断异常,否则异常继续向上冒泡,直到通过app.config.errorHandler
处理异常。
onErrorCaptured在项目中的应用
前文的线上问题,一方面用户体验不好,另一方面研发也难定位,所以异常处理的前提是解决这两个问题。
先看Demo效果:页面正常运行,当用户点击按钮时发生未知异常,则跳转到错误页并显示截屏、客户端等信息:
发生异常时,提醒用户
当发生严重异常时,为了让用户有感知,可通过将页面切换到错误提示页,防止继续误操作。
由于onErrorCaptured
的冒泡特性,可以考虑定义Layout
,统一使用Layout来拦截异常,先定义文件error-layout.vue
,并添加onErrorCaptured
Hook:
// file: error-layout.vue
<script lang="ts" setup>
const errorHandle: any = async (e, instance) => {
}
onErrorCaptured(errorHandle)
</script>
onErrorCaptured仅能处理组件内异常
,当上下文不在组件时,onErrorCaptured
就捕获不到,例如:
setTimeout(() => {
throw new Error('这里的异常onErrorCaptured不会被捕获到')
}, 2000)
解决这类异常,可通过window
的error事件捕获,在error-layout.vue
添加如下代码,注意需要在onUnmounted
注销window上的error事件。
window.addEventListener('error', errorHandle)
onUnmounted(() => {
window.removeEventListener('error', errorHandle)
})
作为一个Layout
,当无异常时,嵌套的组件内容正常显示,当发生异常时,可通过Error组件替换其内容,这里需要定义两个插槽default
、error
。
<template>
<div class="error-layout">
<slot v-if="!hasError" name="default">
</slot>
<slot v-else name="error">
<error></error>
</slot>
</div>
</template>
代码中,默认显示的是default
插槽内容。当发生异常时会将hasError
设置为true,切换到error
插槽,并且缺省使用了error
组件。如果想自定义错误页面,直接覆盖error
插槽即可,例如:
<error-layout :level="1" @on-error="onError">
<child></child>
<template v-slot:error="{ data }">
<custom-error :data="data"></custom-error>
</template>
</error-layout>
并不是所有异常场景都需要切换到错误页面,因此可通过参数level
设置错误级别:
- 0: 仅提醒,通过
onError
事件将错误信息暴露给外部,让使用者自己决策如何处理; - 1:跳转至错误页面,但不上报错误;
- 2:跳转至错误页面,并上报错误;
在props
上定义level属性,有几个地方需要由level
控制。首先,仅当level为1或2时,才显示error
插槽。其次,当level为2
,需要上报错误信息,补充如下代码:
<script lang="ts" setup>
/**
* Error levels: 0: 仅抛出异常信息, 1: 跳转到错误页面, 2: 跳转到错误页面并上报错误信息
*/
type ErrorLevels = 0 | 1 | 2
const props = withDefaults(defineProps<{ level: ErrorLevels }>(), { level: 0 })
</script>
<template>
...
<slot v-else-if="level > 0" name="error">
<error :data="errorContext"></error>
</slot>
</template>
现在添加了error-layout和error
组件,接下来就得考虑上报什么内容,以及error
组件显示什么内容,也就是用户画像
。
用户现场画像
不同用户、不同问题需要对症下药,前提是得知道问题,因此一般得收集现场信息。假如我们需要收集的信息包括错误信息、客户端信息、内存使用情况、错误现场截屏,先定义数据类型:
export type ErrorContext = {
error: Error;
timestamp: number;
image?: string;
agent?: string;
memory?: { used: number; total: number };
}
缺省的error
组件,区分生产和DEV环境,生产环境仅显示提示信息,例如:
而DEV可直接显示用户画像数据,例如:
把用户画像需要的所有信息,统一定义在useErrors.ts
文件,使用时调用相关方法即可,先看使用方式,在error-layout.vue
,通过useErrors()
获取用户画像相关属性和方法。
const {
isMemorySupported,
getMemoryMeta,
isScreenShotSupported,
screenSnapshot,
getAgentMeta,
uploadError,
} = useErrors()
const errorHandle: any = async (e, instance) => {
const error = e?.error ?? e
hasError.value = true
const context: ErrorContext = {
error: error,
agent: getAgentMeta()?.userAgent,
}
if (isMemorySupported.value) {
context.memory = getMemoryMeta()
}
if (isScreenShotSupported.value) {
context.image = await screenSnapshot()
}
errorContext.value = context
emits('onError', context)
if (props.level > 1) {
uploadError(context)
}
}
在errorHandle中调用:
- getAgentMeta: 获取客户端信息;
- getMemoryMeta: 获取内存信息;
- screenSnapshot: 获取截屏;
当信息获取完毕后,uploadError(context)上传错误信息到服务器,当然上传到哪个服务器得使用者定,因此需要由外部来定义类型为(data: ErrorContext) => {}
的uploadError方法,可通过在error-layout.vue
组件使用defineExpose对外暴露设置方法:
defineExpose({
setUploadProxy
})
外部设置方式:
const errorRef = ref<InstanceType<typeof ErrorLayout>>()
onMounted(() => {
errorRef.value?.setUploadProxy((error) => {
...
})
})
接下来就得实现各种数据获取的方式。
- screenSnapshot
屏幕快照使用
html2canvas
三方库实现,也可以考虑navigator.mediaDevices.getDisplayMedia
,但不怎么好用,并且兼容性差。screenSnapshot
实现方式非常简单,代码如下:
const screenSnapshot = async () => {
if (!isScreenShotSupported.value){
return
}
const canvas = await html2canvas(document.body, { useCORS: true, backgroundColor: null })
const frame = canvas.toDataURL("image/png")
return frame
}
由于html2canvas
是基于DOM方式重新渲染,因此不能100%还原场景。自己也仅当着demo使用,线上使用还得具体问题具体分析。
- getMemoryMeta
直接使用
window.performance.memory
获取内存:
const getMemoryMeta = () => {
if (!isMemorySupported.value) {
return
}
const memory = window.performance.memory
return {
used: memory.usedJSHeapSize,
total: memory.jsHeapSizeLimit
}
}
- getAgentMeta
使用
navigator.userAgent
:
const getAgentMeta = () => {
return {
userAgent: navigator.userAgent
}
}
由于截屏、内存获取等实现方式多样,因此得考虑兼容性,所以可定义Ref类型变量isScreenShotSupported
、isMemorySupported
来预判断是否支持相应功能。
onErrorCaptured如何冒泡
相信大部分同学在Vue开发过程调试代码时,都有看到这样的堆栈信息:
以前还被坑过,看到这里以为代码哪里发生异常了,还拼命的排查。但实际上,几乎Vue所有的对外方法都会调用callWithErrorHandling
方法,其定义如下,其主要目的就是处理未知异常。
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null | undefined,
type: ErrorTypes,
args?: unknown[],
): any {
try {
return args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
}
如果有未知异常,则触发catch模块中的handleError
方法,而onErrorCaptured
的触发就包含在handleError
中,其函数签名如下:
export function handleError(
err: unknown,
instance: ComponentInternalInstance | null | undefined,
type: ErrorTypes,
throwInDev = true,
): void
参数说明:
- err: 错误信息;
- instance:组件上下文,也就是组件实体,可通过
getCurrentInstance()
方法获取; - type: 错误类型,标识错误源,例如可能是
MOUNTED
、ERROR_CAPTURED
等等;
接下来我们就看handleError
函数中非常重要的一段代码:
if (instance) {
let cur = instance.parent
while (cur) {
const errorCapturedHooks = cur.ec
if (errorCapturedHooks) {
for (let i = 0; i < errorCapturedHooks.length; i++) {
if (
errorCapturedHooks[i](err, exposedInstance, errorInfo) === false
) {
return
}
}
}
cur = cur.parent
}
}
首先获取组件的parent,当前组件的未知异常需要由父级去捕获,然后while循环,直到找不到parent为止。
errorCapturedHooks
应该是获取通过onErrorCaptured
注册的异常处理Hook列表?那cur.ec
是什么?那我们就到Instance实体中去找定义:
在ComponentInternalInstance
定义中看到ERROR_CAPTURED
对应的枚举值就是ec
,因此errorCapturedHooks
拿到的列表就是外部注册的异常Hook。
每次执行完errorCapturedHooks[i](...)
方法都得判断其结果是否为false,为false则中断冒泡,不继续往上一级抛异常。需要注意的是,仅为false
才中断,返回undefined、null等其他值都不会中断。
总结
异常处理方式多种多样,onErrorCaptured
也不失为一种好的方式,由于它的冒泡机制
,可以在关键层级,例如支持使用自定义组件的场景,在外层通过<error-layout></error-layout>
包裹,如果外部组件有异常可直接给捕获,并给出相应提示,限制影响范围,保证平台的整体稳定性。
我是
前端下饭菜
,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评轮!
转载自:https://juejin.cn/post/7419267723782062118