likes
comments
collection
share

Vue3 另一种异常处理Hook: 可冒泡的onErrorCaptured异常处理有多重要? 前两天领导急冲冲跑过来跟我

作者站长头像
站长
· 阅读数 14

异常处理有多重要? 前两天领导急冲冲跑过来跟我说,新客户反馈说我们平台太慢,数据半天都不显示,叫我赶紧排查下性能问题。前、后端同学花了一小时,看请求日志、看代码,硬是没找出任何问题,实在没办法,几经周折找到客户联系方式,远程看现场情况,结果是浏览器兼容问题: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效果:页面正常运行,当用户点击按钮时发生未知异常,则跳转到错误页并显示截屏、客户端等信息:

Vue3 另一种异常处理Hook: 可冒泡的onErrorCaptured异常处理有多重要? 前两天领导急冲冲跑过来跟我

发生异常时,提醒用户

当发生严重异常时,为了让用户有感知,可通过将页面切换到错误提示页,防止继续误操作。

由于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组件替换其内容,这里需要定义两个插槽defaulterror

<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环境,生产环境仅显示提示信息,例如:

Vue3 另一种异常处理Hook: 可冒泡的onErrorCaptured异常处理有多重要? 前两天领导急冲冲跑过来跟我

而DEV可直接显示用户画像数据,例如:

Vue3 另一种异常处理Hook: 可冒泡的onErrorCaptured异常处理有多重要? 前两天领导急冲冲跑过来跟我

把用户画像需要的所有信息,统一定义在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类型变量isScreenShotSupportedisMemorySupported来预判断是否支持相应功能。

onErrorCaptured如何冒泡

相信大部分同学在Vue开发过程调试代码时,都有看到这样的堆栈信息:

Vue3 另一种异常处理Hook: 可冒泡的onErrorCaptured异常处理有多重要? 前两天领导急冲冲跑过来跟我

以前还被坑过,看到这里以为代码哪里发生异常了,还拼命的排查。但实际上,几乎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: 错误类型,标识错误源,例如可能是MOUNTEDERROR_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实体中去找定义:

Vue3 另一种异常处理Hook: 可冒泡的onErrorCaptured异常处理有多重要? 前两天领导急冲冲跑过来跟我

ComponentInternalInstance定义中看到ERROR_CAPTURED对应的枚举值就是ec,因此errorCapturedHooks拿到的列表就是外部注册的异常Hook。

每次执行完errorCapturedHooks[i](...)方法都得判断其结果是否为false,为false则中断冒泡,不继续往上一级抛异常。需要注意的是,仅为false才中断,返回undefined、null等其他值都不会中断。

总结

异常处理方式多种多样,onErrorCaptured也不失为一种好的方式,由于它的冒泡机制,可以在关键层级,例如支持使用自定义组件的场景,在外层通过<error-layout></error-layout>包裹,如果外部组件有异常可直接给捕获,并给出相应提示,限制影响范围,保证平台的整体稳定性。

我是前端下饭菜,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评轮!

转载自:https://juejin.cn/post/7419267723782062118
评论
请登录