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

ErrorBoundary

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 捕捉错误。
javascript 复制代码
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; // 抛出错误以让错误边界捕获
javascript 复制代码
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. 包裹组件:在应用中使用错误边界包裹你的组件。
javascript 复制代码
<ErrorBoundary>
    <MyComponent />
</ErrorBoundary>

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

javascript 复制代码
   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 的集成(处理异步加载错误)
js 复制代码
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 主要用于捕捉组件内部的错误,而不适用于全局或异步错误。

实现代码:

vue 复制代码
<!-- 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>

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

ts 复制代码
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>

全局异常捕捉:

javascript 复制代码
// 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,全局如下:

插件捕捉

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

微信小程序 Error boundary

实现方式

javascript 复制代码
// 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>

使用方式

javascript 复制代码
// 创建错误边界
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 等错误监控工具,在错误捕获时自动上报。
相关推荐
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
liangshanbo12151 天前
React 性能瓶颈定位面试题——原理深挖版
react·performance·profiler
liangshanbo12152 天前
React 性能优化实战
性能优化·react
拖孩2 天前
代码我能全交给 AI,流量主这 500 个访客它一个都替不了我
前端·后端·微信小程序
FungLeo2 天前
成为全栈·React 管理后台篇·按钮级权限:能力映射、菜单过滤与自锁保护
react·rbac·路由守卫·权限控制·前端权限·成为全栈
志尊宝2 天前
Vue3 零基础每日笔记(055):组件里正确使用 store——storeToRefs 解构与 $patch 批量修改
笔记·vue·html·前端开发·软件开发
yzy852 天前
Vue中下载或打开文件的JavaScript方法
前端·javascript·vue
FungLeo2 天前
成为全栈·React 管理后台篇·评论审核工作流:把状态下拉改成可理解的动作
react·内容安全·状态机·交互设计·成为全栈·评论审核
甜到心里的蛋糕2 天前
如何用企业微信实现让微信收到通知实时通知
服务器·python·微信小程序·fastapi
2601_963870202 天前
基于springboot的综合网上购物商场系统
微信小程序·小程序·课程设计