vue/微信小程序/h5 实现react的boundary

发布于:2025-07-06 ⋅ 阅读:(16) ⋅ 点赞:(0)

react的boundary

react-boundary文档地址
如果没有找到 ErrorBoundary,错误会传播到全局,导致应用崩溃。

实现核心逻辑

在 React 中,当子组件抛出错误时,React 会执行以下步骤:

  1. 查找最近的 ErrorBoundary: React 会从抛出错误的组件向上查找其父组件树,寻找最近的 ErrorBoundary 组件。
  2. 调用 componentDidCatch: 如果找到 ErrorBoundary,React 会调用该组件的 componentDidCatch(error, info) 方法,传递错误信息和其他信息(如错误发生的组件树)。
  3. 更新状态: ErrorBoundary 可以根据捕获的错误更新自身的状态,通常用于显示备用 UI(如错误提示)。
  4. 渲染备用 UI: 一旦状态更新,ErrorBoundary 会重新渲染,展示备用 UI,而不是子组件的正常内容。

无法处理的情况

  • 事件处理函数(比如 onClick,onMouseEnter)
  • 异步代码(如 requestAnimationFrame,setTimeout,promise)
  • 服务端渲染
  • ErrorBoundary 组件本身的错误。

包含函数详细介绍getDerivedStateFromError和componentDidCatch

作用

getDerivedStateFromErrorcomponentDidCatch 都是 React 的错误边界方法,用于处理子组件的错误。这两个方法共同作用,确保组件能够优雅地处理和恢复错误。它们的触发时间点和方式如下:

  1. getDerivedStateFromError:
    • 触发时机: 当子组件抛出错误时,React 会首先调用这个静态方法。
    • 功能: 允许你更新状态以便在渲染错误界面之前准备新状态。
    • 返回值: 返回一个对象来更新组件状态,或者返回 null
  2. componentDidCatch:
    • 触发时机: 在 getDerivedStateFromError 之后调用,主要用于执行副作用,比如日志记录。
    • 功能: 可以处理错误信息,进行记录或其他操作。
    • 参数: 接收两个参数:错误信息和错误的组件栈。

为什么分开调用

分开调用 getDerivedStateFromErrorcomponentDidCatch 的原因主要有以下几点:

  1. 职责分离:
    • getDerivedStateFromError 专注于根据错误更新状态,以便渲染一个替代的 UI。
    • componentDidCatch 处理副作用(如日志记录),关注于错误的处理和反馈。
  2. 性能优化:
    • 分开调用允许 React 在渲染过程中优化性能,避免不必要的重新渲染。
  3. 灵活性:
    • 这种设计允许开发者在状态更新和副作用处理之间做出不同的决策,使得组件更具灵活性。
  4. 一致性:
    • getDerivedStateFromError 是一个静态方法,适用于类组件,而 componentDidCatch 是实例方法,保持了 API 的一致性。
      通过分开处理,React 能够提供更清晰的错误处理机制,提高了组件的可维护性和可读性。

代码实现(补充其他异常捕捉)

当你想补充异步等异常也同步到错误边界组件,如在一个 try-catch 语句中捕获异常并将其同步到错误边界组件,如下:

  • 事件处理函数(比如 onClick,onMouseEnter)
  • 异步代码(如 requestAnimationFrame,setTimeout,promise)
  • 服务端渲染
  • ErrorBoundary 组件本身的错误。
  1. 自定义错误边界:首先,创建一个错误边界组件,使用 componentDidCatch 捕捉错误。
class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }
    static getDerivedStateFromError(error) {
        return { hasError: true };
    }
    componentDidCatch(error, info) {
        // 你可以在这里记录错误信息
        console.error("Error caught in ErrorBoundary:", error, info);
    }
    render() {
        if (this.state.hasError) {
            return <h1>Something went wrong.</h1>;
        }
        return this.props.children;
    }
}
  1. 在组件中使用 try-catch:在你的组件中,使用 try-catch 捕获异常,并调用 setState 来更新错误状态。
    直接在render抛出异常throw this.state.error; // 抛出错误以让错误边界捕获
class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = { error: null };
    }
     handleClick = () => {
        try {
            // 可能抛出错误的代码
        } catch (error) {
            this.setState({ error: true });
        }
    };
    render() {
        try {
            // 可能抛出异常的代码
        } catch (error) {
            this.setState({ error });
        }
        if (this.state.error) {
            throw this.state.error; // 抛出错误以让错误边界捕获
        }
        return <div>正常内容button onClick={this.handleClick}>Click me</button>;</div>;
    }
}
  1. 包裹组件:在应用中使用错误边界包裹你的组件。
<ErrorBoundary>
    <MyComponent />
</ErrorBoundary>

函数组件与useErrorBoundary(需自定义Hook)

   function useErrorBoundary() {
     const [error, setError] = useState(null);
     const [info, setInfo] = useState(null);
     
     const handleError = (err, errorInfo) => {
       setError(err);
       setInfo(errorInfo);
       // 上报错误
     };
     
     useEffect(() => {
       const errorBoundary = React.useErrorBoundary(handleError);
       return () => errorBoundary.unsubscribe();
     }, []);
     
     if (error) {
       return <div>错误:{error.message}</div>;
     }
     return null;
   }

几个关键点,完善一下(还是建议官方的类方式处理):

  • 使用 useCallback 缓存错误处理函数,避免重复渲染
  • 通过动态创建类组件的方式实现错误边界功能
  • 提供了错误状态重置功能(重试按钮)
  • 展示了更友好的错误 UI,包括错误信息和组件堆栈
  • 实现了组件卸载时的资源清理
    在生产环境中使用时,还应该考虑添加以下功能:
  • 更完善的错误上报逻辑
  • 错误 UI 的样式定制
  • 错误边界的嵌套策略
  • 与 Suspense 的集成(处理异步加载错误)
import { useEffect, useState, useCallback } from 'react';

function useErrorBoundary() {
  const [hasError, setHasError] = useState(false);
  const [error, setError] = useState(null);
  const [errorInfo, setErrorInfo] = useState(null);
  
  // 错误处理函数
  const handleError = useCallback((err, info) => {
    setHasError(true);
    setError(err);
    setErrorInfo(info);
    
    // 上报错误到监控系统
    console.error('Error Boundary Captured:', err, info);
    reportErrorToService(err, info); // 假设这是一个错误上报函数
  }, []);
  
  // 用于捕获后代组件错误的 effect
  useEffect(() => {
    // 创建一个错误边界实例
    class ErrorBoundary extends React.Component {
      componentDidCatch(error, errorInfo) {
        handleError(error, errorInfo);
      }
      
      render() {
        return this.props.children;
      }
    }
    
    // 为当前组件创建一个错误边界包装器
    const ErrorBoundaryWrapper = ({ children }) => (
      <ErrorBoundary>{children}</ErrorBoundary>
    );
    
    // 将错误边界包装器挂载到当前组件
    const wrapperElement = document.createElement('div');
    document.body.appendChild(wrapperElement);
    
    // 渲染错误边界组件
    const root = ReactDOM.createRoot(wrapperElement);
    root.render(<ErrorBoundaryWrapper>{children}</ErrorBoundaryWrapper>);
    
    // 清理函数
    return () => {
      root.unmount();
      document.body.removeChild(wrapperElement);
    };
  }, [handleError]);
  
  // 重置错误状态
  const resetErrorBoundary = useCallback(() => {
    setHasError(false);
    setError(null);
    setErrorInfo(null);
  }, []);
  
  // 错误发生时返回错误 UI
  if (hasError) {
    return (
      <div className="error-boundary">
        <div className="error-message">
          <h2>发生错误</h2>
          <p>{error?.message || '未知错误'}</p>
          {errorInfo && (
            <div className="error-details">
              <h3>错误详情</h3>
              <pre>{errorInfo.componentStack}</pre>
            </div>
          )}
        </div>
        <button onClick={resetErrorBoundary}>重试</button>
      </div>
    );
  }
  
  // 没有错误时返回 null
  return null;
}

// 示例使用方法
function MyComponent() {
  const errorBoundary = useErrorBoundary();
  
  return (
    <div>
      {errorBoundary}
      <RiskyComponent /> {/* 可能抛出错误的组件 */}
    </div>
  );
}

vue的boundary

在 Vue 中,实现这个用errorCaptured捕捉
其中,errorCaptured 钩子可以捕捉到以下类型的错误:

  1. 子组件的生命周期钩子中的错误:如 createdmounted 等。
  2. 渲染函数中的错误:在模板或渲染函数中发生的错误。
  3. 事件处理器中的错误:在事件处理函数中抛出的错误。
    但是,errorCaptured 无法捕捉到以下类型的错误:
  4. 全局未处理的 Promise 拒绝:这些错误需要使用全局的 window.onunhandledrejection 来捕获。
  5. 异步操作中的错误:如 setTimeoutsetInterval 中的错误,除非在其中手动捕获并处理。
  6. Vue 实例的错误:如在根实例中发生的错误,需在全局范围内捕获。
    总之,errorCaptured 主要用于捕捉组件内部的错误,而不适用于全局或异步错误。

实现代码:

<!-- AdvancedErrorBoundary.vue -->
<template>
  <div>
    <slot v-if="!errorState" name="default"></slot>
    <slot v-else name="fallback" :error="errorState">
      <div class="error-view">
        <h3>⚠️ 组件异常</h3>
        <p>{{ errorState.message }}</p>
        <button @click="reset">重新加载</button>
      </div>
    </slot>
  </div>
</template>

<script>
export default {
  data: () => ({
    errorState: null
  }),
  errorCaptured(err, vm, info) {
    this.errorState = {
      error: err,
      component: vm,
      info,
      timestamp: Date.now()
    };
    this.reportError(err); // 错误上报
    return false;
  },
  methods: {
    reset() {
      this.errorState = null;
    },
    reportError(err) {
      // 发送错误到监控系统
    }
  }
};
</script>

实际应用(异步捕捉手动触发错误边界):


export default {
  components: { ErrorBoundary },
  methods: {
    async fetchData() { //handleClick 点击事件同理
      try {
        // 异步操作
        await apiCall();
      } catch (e) {
        // 1. 手动触发错误边界
        this.$emit('error', e); 
        
        // 2. 或调用父组件方法
        if (this.parentErrorHandler) {
          this.parentErrorHandler(e);
        }
      }
    },
};
</script>

全局异常捕捉:

// vue2
Vue.config.errorHandler = (err, vm, info) => {
  // 1. 处理全局错误
  console.error('Global error:', err, info);
  
  // 2. 显示全局错误提示
  showGlobalErrorOverlay();
};
// vue3
app.config.errorHandler = (err, vm, info) => {
  // 错误处理逻辑
};

全局异步异常捕捉:

通过 window.addEventListener('unhandledrejection') 捕获。

nuxt 的 Error boundary

nuxt的组件方式实现,是基于vue的errorCaptured,全局如下:

插件捕捉

// 创建插件 plugins/error-handler.js
export default function ({ error }) {
  // 可在此处添加错误上报逻辑
  console.error('Nuxt 错误捕获:', error);
}
// 并在 nuxt.config.js 中注册
export default {
  plugins: ['~/plugins/error-handler'],
};

微信小程序 Error boundary

实现方式

// components/ErrorBoundary/index.js
Component({
  options: {
    multipleSlots: true, // 启用多slot支持
  },
  properties: {
    fallback: {
      type: String,
      value: '页面加载失败,请稍后再试',
    },
    showError: {
      type: Boolean,
      value: false,
    },
  },
  data: {
    hasError: false,
    errorInfo: '',
  },
  methods: {
    // 重置错误状态
    resetErrorBoundary() {
      this.setData({
        hasError: false,
        errorInfo: '',
      });
      // 触发自定义事件通知父组件
      this.triggerEvent('reset');
    },
  },
  // 组件生命周期函数,在组件实例进入页面节点树时执行
  attached() {
    this.setData({
      hasError: this.properties.showError,
    });
  },
  // 错误捕获处理函数
  pageLifetimes: {
    show() {
      // 页面显示时重置错误状态(可选)
      if (this.data.hasError) {
        this.resetErrorBoundary();
      }
    },
  },
  // 捕获当前组件错误
  lifetimes: {
    error(err) {
      console.error('ErrorBoundary捕获到错误:', err);
      this.setData({
        hasError: true,
        errorInfo: err.toString(),
      });
      // 触发自定义事件通知父组件
      this.triggerEvent('error', { error: err });
    },
  },
});    

使用方式

// wxml
<!-- 在页面中使用ErrorBoundary -->
<ErrorBoundary fallback="组件加载失败" bind:error="handleError" bind:reset="handleReset">
  <!-- 可能出错的组件 -->
  <RiskyComponent />
</ErrorBoundary>
// js
// 页面JS
Page({
  methods: {
    handleError(e) {
      console.log('页面收到错误信息:', e.detail.error);
      // 可以在这里添加错误上报逻辑
    },
    handleReset() {
      console.log('用户点击了重试按钮');
      // 可以在这里添加重新加载数据的逻辑
    },
  },
});

h5的Error boundary

在纯HTML/JavaScript环境中实现类似React的Error Boundaries功能需要创建一个自定义的错误边界组件,能够捕获子元素的渲染错误并提供降级UI。

<script>
   // 错误边界实现
    const errorBoundaries = {};
    
    function createErrorBoundary(containerId) {
        const container = document.getElementById(containerId);
        let hasError = false;
        let error = null;
        let errorInfo = null;
        
        function render() {
            if (!hasError) {
                container.innerHTML = `
                    <div class="error-boundary-content">
                        <div class="safe-component">
                            <div class="component-title">安全组件</div>
                            <div class="component-content">这个组件运行正常</div>
                        </div>
                        
                        <div class="unstable-component">
                            <div class="component-title">不稳定组件</div>
                            <div class="component-content">点击按钮触发错误</div>
                        </div>
                    </div>`;
            } else {
                container.innerHTML = `
                    <div class="error-ui">
                        <div class="error-header">
                            <span class="error-icon">⚠️</span>
                            <div class="error-title">组件渲染错误</div>
                        </div>
                        
                        <div class="error-message">${error.message}</div>
                        
                        <div class="actions">
                            <button class="retry-btn" onclick="resetBoundary('${containerId}')">重试</button>
                            <button class="report-btn" onclick="reportError()">报告问题</button>
                        </div>
                    </div>`;
            }
        }
        
        function captureError(err, info) {
            hasError = true;
            error = err;
            errorInfo = info;
            render();
            console.error('ErrorBoundary caught:', err, info);
        }
        
        function reset() {
            hasError = false;
            error = null;
            errorInfo = null;
            render();
        }
        
        // 初始渲染
        render();
        
        return {
            captureError,
            reset
        };
    }
    
    // 创建错误边界实例
    errorBoundaries['boundary1'] = createErrorBoundary('boundary1');
    errorBoundaries['boundary2'] = createErrorBoundary('boundary2');
    
    // 触发错误函数
    function triggerError(boundaryId) {
        try {
            // 模拟一个可能出错的函数
            const invalidObj = null;
            
            // 故意访问null对象的属性来抛出错误
            invalidObj.someProperty = 'test';
        } catch (error) {
            // 捕获错误并传递给错误边界
            errorBoundaries[`boundary${boundaryId}`].captureError(
                new Error('组件渲染时发生错误: ' + error.message),
                { componentStack: '在UnstableComponent中' }
            );
        }
    }
    
    // 重置错误边界
    function resetBoundary(boundaryId) {
        errorBoundaries[boundaryId].reset();
    }
    
    // 报告错误
    function reportError() {
        alert('错误已报告给开发团队,感谢您的反馈!');
    }
    
    // 初始化时随机触发一个错误
    setTimeout(() => {
        if (Math.random() > 0.5) {
            triggerError(2);
        }
    }, 1500);
</script>

使用方式

// 创建错误边界
const boundary = createErrorBoundary('containerId');

// 在可能出错的地方捕获错误
try {
  // 可能出错的代码
} catch (error) {
  boundary.captureError(error, { componentStack: '...' });
}

// 重置错误边界
boundary.reset();

总结

Vue 与 React 的差异:

  1. Vue 的错误捕获是单向的(父 → 子),而 React 是双向的(子 → 父)。
    Vue 的 errorCaptured 钩子仅捕获渲染期间的错误,不捕获异步错误(如 Promise 错误)。
  2. 异步错误处理:
    全局异步错误需通过 window.addEventListener(‘unhandledrejection’) 捕获。
    在 Nuxt 中,异步错误可通过 try…catch 或全局错误页面处理。
  3. 生产环境错误上报:
    建议集成 Sentry 等错误监控工具,在错误捕获时自动上报。

网站公告

今日签到

点亮在社区的每一天
去签到