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

  • 全局统一处理异常;
  • 为开发者提示错误信息;
  • 方案降级处理等等。

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

本文 Vue3 版本为 3.0.11

一、前端常见异常

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

  • JS 语法异常;
  • Ajax 请求异常;
  • 静态资源加载异常;
  • Promise 异常;
  • iframe 异常;
  • 等等

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

最常用的比如:

1. window.onerror

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

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

函数参数:

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

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

2. try…catch 异常处理

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

  1. try {
  2. // do something
  3. } catch (error) {
  4. console.error(error);
  5. }

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

3. 思考

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

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

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

  1. const errorHandling = (fn, args) => {
  2. let result;
  3. try{
  4. result = args ? fn(...args) : fn();
  5. } catch (error){
  6. console.error(error)
  7. }
  8. return result;
  9. }

测试一下:

  1. const f1 = () => {
  2. console.log('[f1 running]')
  3. throw new Error('[f1 error!]')
  4. }
  5. errorHandling(f1);
  6. /*
  7. 输出:
  8. [f1 running]
  9. Error: [f1 error!]
  10. at f1 (/Users/wangpingan/leo/www/node/www/a.js:14:11)
  11. at errorHandling (/Users/wangpingan/leo/www/node/www/a.js:4:39)
  12. at Object.<anonymous> (/Users/wangpingan/leo/www/node/www/a.js:17:1)
  13. at Module._compile (node:internal/modules/cjs/loader:1095:14)
  14. at Object.Module._extensions..js (node:internal/modules/cjs/loader:1147:10)
  15. at Module.load (node:internal/modules/cjs/loader:975:32)
  16. at Function.Module._load (node:internal/modules/cjs/loader:822:12)
  17. at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
  18. at node:internal/main/run_main_module:17:47
  19. */

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

  1. const f1 = () => {
  2. console.log('[f1]')
  3. f2();
  4. }
  5. const f2 = () => {
  6. console.log('[f2]')
  7. f3();
  8. }
  9. const f3 = () => {
  10. console.log('[f3]')
  11. throw new Error('[f3 error!]')
  12. }
  13. errorHandling(f1)
  14. /*
  15. 输出:
  16. [f1 running]
  17. [f2 running]
  18. [f3 running]
  19. Error: [f3 error!]
  20. at f3 (/Users/wangpingan/leo/www/node/www/a.js:24:11)
  21. at f2 (/Users/wangpingan/leo/www/node/www/a.js:19:5)
  22. at f1 (/Users/wangpingan/leo/www/node/www/a.js:14:5)
  23. at errorHandling (/Users/wangpingan/leo/www/node/www/a.js:4:39)
  24. at Object.<anonymous> (/Users/wangpingan/leo/www/node/www/a.js:27:1)
  25. at Module._compile (node:internal/modules/cjs/loader:1095:14)
  26. at Object.Module._extensions..js (node:internal/modules/cjs/loader:1147:10)
  27. at Module.load (node:internal/modules/cjs/loader:975:32)
  28. at Function.Module._load (node:internal/modules/cjs/loader:822:12)
  29. at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
  30. */

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

三、Vue3 如何实现异常处理

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

1. 实现异常处理方法

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

  • callWithErrorHandling:处理同步方法的异常;
  • callWithAsyncErrorHandling:处理异步方法的异常。

使用方式如下:

  1. callWithAsyncErrorHandling(
  2. handler,
  3. instance,
  4. ErrorCodes.COMPONENT_EVENT_HANDLER,
  5. args
  6. )

代码实现大致如下:

  1. // packages/runtime-core/src/errorHandling.ts
  2. // 处理同步方法的异常
  3. export function callWithErrorHandling(
  4. fn: Function,
  5. instance: ComponentInternalInstance | null,
  6. type: ErrorTypes,
  7. args?: unknown[]
  8. ) {
  9. let res
  10. try {
  11. res = args ? fn(...args) : fn(); // 调用原方法
  12. } catch (err) {
  13. handleError(err, instance, type)
  14. }
  15. return res
  16. }
  17. // 处理异步方法的异常
  18. export function callWithAsyncErrorHandling(
  19. fn: Function | Function[],
  20. instance: ComponentInternalInstance | null,
  21. type: ErrorTypes,
  22. args?: unknown[]
  23. ): any[] {
  24. // 省略其他代码
  25. const res = callWithErrorHandling(fn, instance, type, args)
  26. if (res && isPromise(res)) {
  27. res.catch(err => {
  28. handleError(err, instance, type)
  29. })
  30. }
  31. // 省略其他代码
  32. }

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

2. 处理异常

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

  1. // packages/runtime-core/src/errorHandling.ts
  2. // 异常处理方法
  3. export function handleError(
  4. err: unknown,
  5. instance: ComponentInternalInstance | null,
  6. type: ErrorTypes,
  7. throwInDev = true
  8. ) {
  9. // 省略其他代码
  10. logError(err, type, contextVNode, throwInDev)
  11. }
  12. function logError(
  13. err: unknown,
  14. type: ErrorTypes,
  15. contextVNode: VNode | null,
  16. throwInDev = true
  17. ) {
  18. // 省略其他代码
  19. console.error(err)
  20. }

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

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

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

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

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

  1. // packages/runtime-core/src/errorHandling.ts
  2. export function handleError(
  3. err: unknown,
  4. instance: ComponentInternalInstance | null,
  5. type: ErrorTypes,
  6. throwInDev = true
  7. ) {
  8. const contextVNode = instance ? instance.vnode : null
  9. if (instance) {
  10. // 省略其他代码
  11. // 读取 errorHandler 配置项
  12. const appErrorHandler = instance.appContext.config.errorHandler
  13. if (appErrorHandler) {
  14. callWithErrorHandling(
  15. appErrorHandler,
  16. null,
  17. ErrorCodes.APP_ERROR_HANDLER,
  18. [err, exposedInstance, errorInfo]
  19. )
  20. return
  21. }
  22. }
  23. logError(err, type, contextVNode, throwInDev)
  24. }

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

4. 调用 errorCaptured 生命周期钩子

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

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

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

  1. <template>
  2. <Message></Message>
  3. </template>
  4. <script setup>
  5. // App.vue
  6. import { onErrorCaptured } from 'vue';
  7. import Message from './components/Message.vue'
  8. onErrorCaptured(function(err, instance, info){
  9. console.log('[errorCaptured]', err, instance, info)
  10. })
  11. </script>

子组件如下:

  1. <template>
  2. <button @click="sendMessage">发送消息</button>
  3. </template>
  4. <script setup>
  5. // Message.vue
  6. const sendMessage = () => {
  7. throw new Error('[test onErrorCaptured]')
  8. }
  9. </script>

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

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

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

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

  1. // packages/runtime-core/src/errorHandling.ts
  2. export function handleError(
  3. err: unknown,
  4. instance: ComponentInternalInstance | null,
  5. type: ErrorTypes,
  6. throwInDev = true
  7. ) {
  8. const contextVNode = instance ? instance.vnode : null
  9. if (instance) {
  10. let cur = instance.parent
  11. // the exposed instance is the render proxy to keep it consistent with 2.x
  12. const exposedInstance = instance.proxy
  13. // in production the hook receives only the error code
  14. const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type
  15. while (cur) {
  16. const errorCapturedHooks = cur.ec // ①取出组件配置的 errorCaptured 生命周期方法
  17. if (errorCapturedHooks) {
  18. // ②循环执行 errorCaptured 中的每个 Hook
  19. for (let i = 0; i < errorCapturedHooks.length; i++) {
  20. if (
  21. errorCapturedHooks[i](err, exposedInstance, errorInfo) === false
  22. ) {
  23. return
  24. }
  25. }
  26. }
  27. cur = cur.parent
  28. }
  29. // 省略其他代码
  30. }
  31. logError(err, type, contextVNode, throwInDev)
  32. }

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

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

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

  1. // packages/runtime-core/src/errorHandling.ts
  2. export const enum ErrorCodes {
  3. SETUP_FUNCTION,
  4. RENDER_FUNCTION,
  5. WATCH_GETTER,
  6. WATCH_CALLBACK,
  7. // ... 省略其他
  8. }
  9. export const ErrorTypeStrings: Record<number | string, string> = {
  10. // 省略其他
  11. [LifecycleHooks.RENDER_TRACKED]: 'renderTracked hook',
  12. [LifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook',
  13. [ErrorCodes.SETUP_FUNCTION]: 'setup function',
  14. [ErrorCodes.RENDER_FUNCTION]: 'render function',
  15. // 省略其他
  16. [ErrorCodes.SCHEDULER]:
  17. 'scheduler flush. This is likely a Vue internals bug. ' +
  18. 'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-next'
  19. }

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

  1. // packages/runtime-core/src/errorHandling.ts
  2. function logError(
  3. err: unknown,
  4. type: ErrorTypes,
  5. contextVNode: VNode | null,
  6. throwInDev = true
  7. ) {
  8. if (__DEV__) {
  9. const info = ErrorTypeStrings[type]
  10. warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`)
  11. // 省略其他
  12. } else {
  13. console.error(err)
  14. }
  15. }

6. 实现 Tree Shaking

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

  1. // packages/runtime-core/src/errorHandling.ts
  2. function logError(
  3. err: unknown,
  4. type: ErrorTypes,
  5. contextVNode: VNode | null,
  6. throwInDev = true
  7. ) {
  8. if (__DEV__) {
  9. // 省略其他
  10. } else {
  11. console.error(err)
  12. }
  13. }

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

四、总结

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

  1. 支持同步和异步的异常处理;
  2. 设置业务错误码、业务错误信息;
  3. 支持自定义错误处理方法;
  4. 支持开发环境错误提示;
  5. 支持 Tree Shaking。

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