乐趣区

关于javascript:Vue3-如何实现全局异常处理

在开发组件库或者插件,常常会须要进行全局异样解决,从而实现:

  • 全局对立解决异样;
  • 为开发者提醒错误信息;
  • 计划降级解决等等。

那么如何实现下面性能呢?
本文先简略实现一个异样解决办法,而后联合 Vue3 源码中的实现具体介绍,最初总结实现异样解决的几个外围。

本文 Vue3 版本为 3.0.11

一、前端常见异样

对于前端来说,常见的异样比拟多,比方:

  • JS 语法异样;
  • Ajax 申请异样;
  • 动态资源加载异样;
  • Promise 异样;
  • iframe 异样;
  • 等等

对于这些异样如何解决,能够浏览这两篇文章:

  • 《你不晓得的前端异样解决》
  • 《如何优雅解决前端异样?》

最罕用的比方:

1. window.onerror

通过 window.onerror文档可知,当 JS 运行时产生谬误(包含语法错误),触发 window.onerror()

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

函数参数:

  • message:错误信息(字符串)。可用于 HTML onerror=""处理程序中的 event
  • source:产生谬误的脚本 URL(字符串)
  • lineno:产生谬误的行号(数字)
  • colno:产生谬误的列号(数字)
  • error:Error 对象(对象)

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

2. try…catch 异样解决

另外,咱们也常常会应用 try...catch 语句解决异样:

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.<anonymous> (/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.<anonymous> (/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)
*/

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

三、Vue3 如何实现异样解决

了解完下面示例,接下来看看在 Vue3 源码中是如何实现异样解决的,其实现起来也是很简略。

1. 实现异样解决办法

errorHandling.ts 文件中定义了 callWithErrorHandlingcallWithAsyncErrorHandling两个解决全局异样的办法。
顾名思义,这两个办法别离解决:

  • callWithErrorHandling:解决同步办法的异样;
  • callWithAsyncErrorHandling:解决异步办法的异样。

应用形式如下:

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)
    })
  }
  // 省略其余代码
}

callWithErrorHandling办法解决的逻辑比较简单,通过简略的 try...catch 做一层封装。
callWithAsyncErrorHandling 办法就比拟奇妙,通过将须要执行的办法传入 callWithErrorHandling办法解决,并将其后果通过 .catch办法进行解决。

2. 解决异样

在下面代码中,遇到报错的状况,都会通过 handleError()解决异样。其实现大抵如下:

// 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)
}

保留外围解决逻辑之后,能够看到这边解决也是相当简略,间接通过 console.error(err)输入谬误内容。

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

在应用 Vue3 时,也反对 指定自定义异样处理函数 ,来解决 组件渲染函数 侦听器执行期间 抛出的未捕捉谬误。这个处理函数被调用时,可获取错误信息和相应的利用实例。
文档参考:《errorHandler》
应用办法如下,在我的项目 main.js文件中配置:

// src/main.js

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

那么 errorHandler()是何时执行的呢?咱们持续看看源码中 handleError() 的内容,能够发现:

// 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)
}

通过 instance.appContext.config.errorHandler取到全局配置的自定义谬误处理函数,存在时则执行,当然,这边也是通过后面定义的 callWithErrorHandling来调用。

4. 调用 errorCaptured 生命周期钩子

在应用 Vue3 的时候,也能够通过 errorCaptured生命周期钩子来 捕捉来自后辈组件的谬误
文档参考:《errorCaptured》
入参如下:

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

此钩子会收到三个参数:谬误对象、产生谬误的组件实例以及一个蕴含谬误起源信息的字符串。
此钩子能够返回 false 阻止该谬误持续向上流传。
有趣味的同学能够通过文档,查看具体的谬误流传规定
应用办法如下,父组件监听 onErrorCaptured生命周期(示例代码应用 Vue3 setup 语法):

<template>
  <Message></Message>
</template>
<script setup>
// App.vue  
import {onErrorCaptured} from 'vue';
  
import Message from './components/Message.vue'
  
onErrorCaptured(function(err, instance, info){console.log('[errorCaptured]', err, instance, info)
})
</script>

子组件如下:

<template>
  <button @click="sendMessage"> 发送音讯 </button>
</template>

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

当点击「发送音讯」按钮,控制台便输入谬误:

[errorCaptured] Error: [test onErrorCaptured]
    at Proxy.sendMessage (Message.vue:36:15)
    at _createElementVNode.onClick._cache.<computed>._cache.<computed> (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:13) Proxy {sendMessage: ƒ, …} native event handler

能够看到 onErrorCaptured生命周期钩子失常执行,并输入子组件 Message.vue内的异样。

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

// 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)
}

这边会先获取 instance.parent作为以后解决的组件实例进行递归,每次将取出组件配置的 errorCaptured 生命周期办法的数组并循环调用其每一个钩子,而后再取出以后组件的父组件作为参数,最初持续递归调用上来。

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 | string, string> = {
  // 省略其余
  [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'
}

当不同谬误状况,依据错误码 ErrorCodes来获取 ErrorTypeStrings错误信息进行提醒:

// 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 Shaking

对于 Vue3 实现 Tree Shaking 的介绍,能够看我之前写的高效实现框架和 JS 库瘦身。
其中,logError 办法中就应用到了:

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

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

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

四、总结

到下面一部分,咱们就差不多搞清楚 Vue3 中全局异样解决的外围逻辑了。咱们在开发本人的错误处理办法时,也能够思考这几个外围点:

  1. 反对同步和异步的异样解决;
  2. 设置业务错误码、业务错误信息;
  3. 反对自定义错误处理办法;
  4. 反对开发环境谬误提醒;
  5. 反对 Tree Shaking。

这几点在你设计插件的时候,都能够思考进去的~

退出移动版