Vuejs 如何实现全局异常处理?

游客2024-07-11 08:45:01

大家好,我是若川。持续组织了近一年的,感兴趣的可以加我微信参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的包含20余篇源码文章。。另外:目前建有江西|湖南|湖北籍前端群,可加我微信进群。

开发组件库或者插件,经常会需要进行全局异常处理,从而实现:

那么如何实现上面功能呢?本文先简单实现一个异常处理方法,然后结合 Vue3 源码中的实现详细介绍,最后总结实现异常处理的几个核心。

本文 Vue3 版本为 3.0.11

一、前端常见异常

对于前端来说,常见的异常比较多,比如:

对于这些异常如何处理,可以阅读这两篇文章:

最常用的比如:

1. .

通过 .文档[2]可知,当 JS 运行时发生错误(包括语法错误),触发 .():

window.onerror = function(message, source, lineno, colno, error{
  console.log('捕获到异常:',{message, source, lineno, colno, error});
}

函数参数:

若该函数返回true,则阻止执行默认事件处理函数。

2. try...catch 异常处理

另外,我们也经常会使用 try...catch 语句[4]处理异常:

try {
  // do something
catch (error) {
  console.error(error);
}

更多处理方式,可以阅读前面推荐的文章。

3. 思考

大家可以思考下,自己在业务开发过程中,是否也是经常要处理这些错误情况?那么像 Vue3 这样复杂的库,是否也是到处通过 try...catch来处理异常呢?接下来一起看看。

二、实现简单的全局异常处理

在开发插件或库时,我们可以通过 try...catch封装一个全局异常处理方法,将需要执行的方法作为参数传入,调用方只要关心调用结果,而无需知道该全局异常处理方法内部逻辑。大致使用方法如下:

const errorHandling = (fn, args) => {
  let result;
  try{
    result = args ? fn(...args) : fn();
  } catch (error){
    console.error(error)
  }
  return result;
}

测试一下:

const f1 = () => {
    console.log('[f1 running]')
    throw new Error('[f1 error!]')
}

errorHandling(f1);
/*
 输出:
 [f1 running]
Error: [f1 error!]
    at f1 (/Users/wangpingan/leo/www/node/www/a.js:14:11)
    at errorHandling (/Users/wangpingan/leo/www/node/www/a.js:4:39)
    at Object. (/Users/wangpingan/leo/www/node/www/a.js:17:1)
    at Module._compile (node:internal/modules/cjs/loader:1095:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1147:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47
*/

可以看到,当需要为方法做异常处理时,只要将该方法作为参数传入即可。但是上面示例跟实际业务开发的逻辑差得有点多,实际业务中,我们经常会遇到方法的嵌套调用,那么我们试一下:

const f1 = () => {
    console.log('[f1]')
    f2();
}

const f2 = () => {
    console.log('[f2]')
    f3();
}

const f3 = () => {
    console.log('[f3]')
    throw new Error('[f3 error!]')
}

errorHandling(f1)
/*
  输出:
  [f1 running]
  [f2 running]
  [f3 running]
  Error: [f3 error!]
    at f3 (/Users/wangpingan/leo/www/node/www/a.js:24:11)
    at f2 (/Users/wangpingan/leo/www/node/www/a.js:19:5)
    at f1 (/Users/wangpingan/leo/www/node/www/a.js:14:5)
    at errorHandling (/Users/wangpingan/leo/www/node/www/a.js:4:39)
    at Object. (/Users/wangpingan/leo/www/node/www/a.js:27:1)
    at Module._compile (node:internal/modules/cjs/loader:1095:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1147:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
*/

这样也是没问题的。那么接下来就是在 方法的 catch分支实现对应异常处理即可。接下来看看 Vue3 源码中是如何处理的?

三、Vue3 如何实现异常处理

理解完上面示例,接下来看看在 Vue3 源码中是如何实现异常处理的,其实现起来也是很简单。

1. 实现异常处理方法

在 .ts 文件中定义了 g和 两个处理全局异常的方法。顾名思义,这两个方法分别处理:

使用方式如下:

callWithAsyncErrorHandling(
  handler,
  instance,
  ErrorCodes.COMPONENT_EVENT_HANDLER,
  args
)

代码实现大致如下:

// packages/runtime-core/src/errorHandling.ts

// 处理同步方法的异常
export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
{
  let res
  try {
    res = args ? fn(...args) : fn(); // 调用原方法
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

// 处理异步方法的异常
export function callWithAsyncErrorHandling(
  fn: Function | Function[],
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
): any[] 
{
  // 省略其他代码
  const res = callWithErrorHandling(fn, instance, type, args)
  if (res && isPromise(res)) {
    res.catch(err => {
      handleError(err, instance, type)
    })
  }
  // 省略其他代码
}

g方法处理的逻辑比较简单,通过简单的 try...catch 做一层封装。而 方法就比较巧妙,通过将需要执行的方法传入 g方法处理,并将其结果通过 .catch方法进行处理。

2. 处理异常

在上面代码中,遇到报错的情况,都会通过 ()处理异常。其实现大致如下:

// packages/runtime-core/src/errorHandling.ts

// 异常处理方法
export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  throwInDev = true
{
  // 省略其他代码
  logError(err, type, contextVNode, throwInDev)
}

function logError(
  err: unknown,
  type: ErrorTypes,
  contextVNode: VNode | null,
  throwInDev = true
{
  // 省略其他代码
  console.error(err)
}

保留核心处理逻辑之后,可以看到这边处理也是相当简单,直接通过 .error(err)输出错误内容。

3. 配置 自定义异常处理函数

在使用 Vue3 时,也支持「指定自定义异常处理函数」,来处理「组件渲染函数」和「侦听器执行期间」抛出的未捕获错误。这个处理函数被调用时,可获取错误信息和相应的应用实例。文档参考:《[5]》使用方法如下,在项目 main.js文件中配置:

// src/main.js

app.config.errorHandler = (err, vm, info) => {
  // 处理错误
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
}

那么 ()是何时执行的呢?我们继续看看源码中 () 的内容,可以发现:

// packages/runtime-core/src/errorHandling.ts

export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  throwInDev = true
{
  const contextVNode = instance ? instance.vnode : null
  if (instance) {
    // 省略其他代码
    // 读取 errorHandler 配置项
    const appErrorHandler = instance.appContext.config.errorHandler
    if (appErrorHandler) {
      callWithErrorHandling(
        appErrorHandler,
        null,
        ErrorCodes.APP_ERROR_HANDLER,
        [err, exposedInstance, errorInfo]
      )
      return
    }
  }
  logError(err, type, contextVNode, throwInDev)
}

通过 ...取到全局配置的自定义错误处理函数,存在时则执行,当然,这边也是通过前面定义的 g来调用。

4. 调用 生命周期钩子

在使用 Vue3 的时候,也可以通过 生命周期钩子来「捕获来自后代组件的错误」。文档参考:《[6]》入参如下:

(err: Errorinstance: Component, info: string) => ?boolean

此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false以「阻止该错误继续向上传播。」有兴趣的同学可以通过文档,「查看具体的错误传播规则」。使用方法如下,父组件监听 生命周期(示例代码使用 Vue3 setup 语法):


<script setup>
// App.vue
import { one rrorCaptured } from 'vue';

import Message from './components/Message.vue'

onErrorCaptured(function(err, instance, info){
console.log('[errorCaptured]', err, instance, info)
})
</script>

子组件如下:



<script setup>
// Message.vue
const sendMessage = () => {
throw new Error('[test one rrorCaptured]')
}
</script>

当点击「发送消息」按钮,控制台便输出错误:

[errorCaptured] Error: [test onErrorCaptured]
    at Proxy.sendMessage (Message.vue:36:15)
    at _createElementVNode.onClick._cache.._cache. (Message.vue:3:39)
    at callWithErrorHandling (runtime-core.esm-bundler.js:6706:22)
    at callWithAsyncErrorHandling (runtime-core.esm-bundler.js:6715:21)
    at HTMLButtonElement.invoker (runtime-dom.esm-bundler.js:350:13Proxy {sendMessage: ƒ, …} native event handler

可以看到 生命周期钩子正常执行,并输出子组件 .vue内的异常。

那么这个又是如何实现呢?还是看 .ts 中的 () 方法:

// packages/runtime-core/src/errorHandling.ts

export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  throwInDev = true
{
  const contextVNode = instance ? instance.vnode : null
  if (instance) {
    let cur = instance.parent
    // the exposed instance is the render proxy to keep it consistent with 2.x
    const exposedInstance = instance.proxy
    // in production the hook receives only the error code
    const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type
    while (cur) {
      const errorCapturedHooks = cur.ec // ①取出组件配置的 errorCaptured 生命周期方法
      if (errorCapturedHooks) {
        // ②循环执行 errorCaptured 中的每个 Hook
        for (let i = 0; i < errorCapturedHooks.length; i++) {
          if (
            errorCapturedHooks[i](err, exposedInstance, errorInfo "i") === false
          ) {
            return
          }
        }
      }
      cur = cur.parent
    }
    // 省略其他代码
  }
  logError(err, type, contextVNode, throwInDev)
}

这边会先获取 .作为当前处理的组件实例进行递归,每次将取出组件配置的 生命周期方法的数组并循环调用其每一个钩子,然后再取出当前组件的父组件作为参数,最后继续递归调用下去。

5. 实现错误码和错误消息

Vue3 还为异常定义了错误码和错误信息,在不同的错误情况有不同的错误码和错误信息,让我们能很方便定位到发生异常的地方。错误码和错误信息如下:

// packages/runtime-core/src/errorHandling.ts

export const enum ErrorCodes {
  SETUP_FUNCTION,
  RENDER_FUNCTION,
  WATCH_GETTER,
  WATCH_CALLBACK,
  // ... 省略其他
}

export const ErrorTypeStrings: Record<number | stringstring> = {
  // 省略其他
  [LifecycleHooks.RENDER_TRACKED]: 'renderTracked hook',
  [LifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook',
  [ErrorCodes.SETUP_FUNCTION]: 'setup function',
  [ErrorCodes.RENDER_FUNCTION]: 'render function',
  // 省略其他
  [ErrorCodes.SCHEDULER]:
    'scheduler flush. This is likely a Vue internals bug. ' +
    'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-next'
}

当不同错误情况,根据错误码 来获取 错误信息进行提示:

// packages/runtime-core/src/errorHandling.ts

function logError(
  err: unknown,
  type: ErrorTypes,
  contextVNode: VNode | null,
  throwInDev = true
{
  if (__DEV__) {
    const info = ErrorTypeStrings[type]
    warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`)
    // 省略其他
  } else {
    console.error(err)
  }
}

6. 实现 Tree

关于 Vue3 实现 Tree 的介绍,可以看我之前写的高效实现框架和 JS 库瘦身[7]。其中, 方法中就使用到了:

// packages/runtime-core/src/errorHandling.ts

function logError(
  err: unknown,
  type: ErrorTypes,
  contextVNode: VNode | null,
  throwInDev = true
{
  if (__DEV__) {
    // 省略其他
  } else {
    console.error(err)
  }
}

当编译成 环境后,分支的代码不会被打包进去,从而优化包的体积。

四、总结

到上面一部分,我们就差不多搞清楚 Vue3 中全局异常处理的核心逻辑了。我们在开发自己的错误处理方法时,也可以考虑这几个核心点:

支持同步和异步的异常处理;

设置业务错误码、业务错误信息;

支持自定义错误处理方法;

支持开发环境错误提示;

支持 Tree 。

这几点在你设计插件的时候,都可以考虑进去的~

参考资料

[1]

《如何优雅处理前端异常?》:

[2]

文档:

[3]

Error对象:

[4]

语句:

[5]

: #

[6]

: #

[7]

高效实现框架和 JS 库瘦身:

·················若川简介·················

你好,我是,毕业于江西高校。现在是一名前端开发“工程师”。写有《》20余篇,在知乎、掘金收获超百万阅读。

从2014年起,每年都会写一篇年度总结,已经坚持写了8年,点击查看。

同时,最近组织了,帮助4000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

扫码加我微信 、拉你进源码共读群

今日话题

目前建有江西|湖南|湖北籍 前端群,想进群的可以加我微信 进群。分享、收藏、点赞、在看我的文章就是对我最大的支持~