Js

vue-error-boundary

错误边界最早是由 React 提出的概念,但偶然发现 vue 有对应实现,那就来看看吧

错误边界简介

错误边界(Error Boundaries)是 17 年时 React 中出现的一种概念,它指出:组件渲染、生命周期时出现的错误不应导致整个应用的崩溃1。使用错误边界能捕获组件的错误并回退到特定组件。

Vue 中的错误捕获

VueJS 内置了两种选项用于捕获组件中出现的错误,errorHandler 和 errorCaptured(Vue@2.5+),分别用于设置全局的错误捕获与子孙组件的错误捕获。VueJS 自定义了错误的传播规则:只要 errorCaptured 没有返回 false,错误就会一直向上传播:

有两点需要注意:

  • errorHandler 会阻止错误冒泡到 window.error;
  • errorCaptured 不能捕获组件自身中的错误;

针对以上两点,一般会这么处理:

  • 使用自定义函数包装并增强 errorHandler 函数,组件内部不再使用 errorCaptured 处理异常。
  • 封装以 errorCaptured 为基础的 ErrorBoundary 组件,控制报错粒度,并防止 UI 崩溃。

错误边界组件

先看看 vue-error-boundary 最基本的使用。

<template>
  <ErrorBoundary :fall-back="uiFallBack">
    <UnstableComponent />
  </ErrorBoundary>
</template>

<script>
export default {
  data() {
    return {
      uiFallBack: {
        functional: true,
        render(h) {
          return h("div", [h("h1", "网络错误~"), h("p", "请稍后重试~")]);
        },
      },
    };
  },
};
</script>

源代码非常简单,直接贴上来啦。

<script>
/* 帮助函数 */
import { isObjectEmpty, warn, convertVNodeArray } from "utils";

export default {
  name: "ErrorBoundary",
  props: {
    fallBack: Object,
    onError: Function,
    params: Object,
    stopPropagation: Boolean,
    tag: String,
  },
  data() {
    return {
      err: "",
      info: "",
      hasError: null,
    };
  },
  // 当 ErrorBoundary 的子孙组件出错时,
  // 触发 errorCaptured 事件,
  // 并调用传入的 onError 处理异常
  errorCaptured(err, vm, info = "") {
    this.hasError = true;
    this.err = err;
    this.info = info;

    this.$emit("errorCaptured", { err, vm, info });
    if (this.onError) this.onError(err, vm, info);
    if (this.stopPropagation) return false;
  },
  // 渲染的流程:
  // 1. 有错误则尝试渲染 slots.boundary,
  //    没有 slots 时,回退到由 props 传入的 fallBack 组件
  // 2. 无错误则尝试渲染 slots.boundary,
  //    没有 slots 时,回退到 没有 slots.defaults
  render(h) {
    const content = this.$slots.default;
    const isScoped = this.$scopedSlots.boundary;
    let scopedSlot;

    if (isScoped) {
      scopedSlot = this.$scopedSlots.boundary({
        hasError: this.hasError,
        err: this.err,
        info: this.info,
      });
    }

    const fallbackOrScoped = isScoped
      ? scopedSlot
      : h(this.fallBack, {
          props: { ...this.params },
        });

    if (this.hasError) {
      return Array.isArray(fallbackOrScoped)
        ? convertVNodeArray(h, this.tag, fallbackOrScoped)
        : fallbackOrScoped;
    }

    if (isScoped) {
      if (!this.$scopedSlots.boundary()) {
        warn("ErrorBoundary component must have child components.");
        return null;
      }
      return Array.isArray(scopedSlot)
        ? convertVNodeArray(h, this.tag, scopedSlot)
        : scopedSlot;
    }

    if (isObjectEmpty(this.$slots)) {
      warn("ErrorBoundary component must have child components.");
      return null;
    }

    return Array.isArray(content)
      ? convertVNodeArray(h, this.tag, content)
      : content;
  },
};
</script>

其它的捕获错误思路

一般而言,只要代码中抛出的错误,没有被拦截,最终都会被 window.onerror 捕获到——所以需要强调以下特殊情况:

  • 像 errorHandler 这种 API 会拦截错误,所以使用自定义函数对框架 API 进行增强。
  • 特殊错误如 Promise 中的异常,需要使用 window.addEventListener('unhandledrejection', event => ···) 进行捕获。

Vue3 中的错误边界

Vue3 的写法也很简单,直接贴代码。

<script>
export default defineComponent({
  setup() {
    const hasError = ref(false);
    const error = ref(null);

    onErrorCaptured((err, instance, info) => {
      hasError.value = true;
      error.value = err;
    });

    return () =>
      this.hasError
        ? h("div", "Something went wrong: " + this.error.message)
        : h("div", this.$slots.default());
  },
});
</script>

阅读更多

Footnotes

  1. https://reactjs.org/docs/error-boundaries.html#gatsby-focus-wrapper

Copyright © 2024 Lionad - CC-BY-NC-CD-4.0