从原生到框架之前端的错误监听详解(Vue和React两大框架)
作者:懒癌重度患者(゚ー゚)
前言
在前端开发中,错误就像隐藏在代码里的 “暗礁”—— 可能是用户操作时的偶发异常,也可能是特定环境下的兼容性问题。如果没有完善的错误监听机制,这些 “暗礁” 会悄悄摧毁用户体验,甚至导致应用崩溃。
今天,我们就从原生 JavaScript 出发,逐步深入 Vue 和 React 两大框架的错误处理方案。
一、原生 JavaScript:错误监听的 “基石”
无论使用哪种框架,原生 JS 的错误处理都是基础。它能覆盖框架未处理的底层错误,比如第三方库异常、DOM 操作失误等
1. try…catch
try...catch
常用于捕获已知可能发生的代码块,可精确控制捕获范围。
try { // 可能抛出错误的代码 const result = JSON.parse(invalidJson); } catch (error) { // 错误处理逻辑 console.error('解析JSON失败:', error.message); // 可记录错误到服务端 reportErrorToServer(error); }
try...catch 在同/异步任务的表现:
try
代码块会同步执行,执行过程中如果遇到错误,会立即被catch
捕获。- 如果
try
里包含异步任务(比如setTimeout
、Promise
、async/await
中的异步操作)- 这类异步任务的回调会脱离
try
的执行栈,错误无法被外层catch
捕获。 - 如果想要让异步错误被
catch
捕获,关键是让“异步操作的错误重新回到try
的执行栈”,这需要借助async/await
或Promise.catch
配合
- 这类异步任务的回调会脱离
// 错误示例:setTimeout 内的错误无法被捕获 try { setTimeout(() => { // 这里的错误会在异步队列执行,此时 try...catch 已结束 JSON.parse('invalid-json'); // 抛出错误 }, 1000); } catch (error) { // 永远不会进入这里! console.log('捕获到错误:', error); } // 错误示例:未 await 的 Promise 错误无法被捕获 try { // Promise 回调是异步的,throw 时 try...catch 已执行完 Promise.resolve().then(() => { throw new Error('Promise 内的错误'); }); } catch (error) { // 也不会进入这里! console.log('捕获到错误:', error); }
// 正确示例:await 配合 try...catch 捕获异步错误 async function fetchData() { try { // await 会等待 Promise 执行,若失败则抛出错误 const response = await fetch('https://api.example.com/invalid-url'); } catch (error) { // 能捕获到 fetch 失败、HTTP 错误、JSON 解析错误等所有异步相关错误 console.log('捕获到异步错误:', error.message); // 可做降级处理(如返回默认数据) return { defaultData: [] }; } } fetchData();
// 示例:Promise.catch 配合 try...catch function fetchData() { return new Promise((resolve, reject) => { fetch('https://api.example.com/invalid-url') .then(response => { if (!response.ok) throw new Error(`请求失败: ${response.status}`); return response.json(); }) .then(data => resolve(data)) .catch(error => { // 手动把异步错误抛到外层 reject(error); }); }); } // 外层用 try...catch 捕获 reject 的错误 async function wrapper() { try { await fetchData(); } catch (error) { console.log('捕获到异步错误:', error.message); } } wrapper();
2. window.onerror
如果错误没有被 try...catch
捕获,window.onerror
会成为最后一道防线,监听全局错误。
这里要注意, window.onerror
不捕获 Promise
未处理的拒绝错误,而像 setTimeout/setInterval
这类异步执行的普通错误,它是会捕获的。
// 全局错误监听 window.onerror = function(message, source, lineno, colno, error) { // 关键参数说明: // message:错误描述(如"Uncaught ReferenceError: xxx is not defined") // source:错误发生的脚本URL(方便定位哪个文件出错) // lineno/colno:错误行号/列号 // error:完整的Error对象(包含stack堆栈信息) // 过滤掉无关错误(比如某些第三方库的非致命警告) if (message.includes('script error') && !source) return true; console.error('全局同步错误:', { message, source: source?.split('/').pop(), // 简化文件名 line: lineno, column: colno, stack: error?.stack }); // 上报错误(生产环境必备) reportGlobalError(error); // 返回true:阻止浏览器默认错误提示(避免用户看到难看的控制台报错) return true; };
注意⚠️
- 跨域脚本(如 CDN 资源)默认只显示
Script error
,需两步解决:- 给
<script>
标签加crossorigin
属性:<script src="https://cdn.example.com/xxx.js" crossorigin="anonymous"></script>
- CDN 服务器配置
Access-Control-Allow-Origin: *
(或指定你的域名)
- 给
- 压缩后的代码(如 webpack 打包产物)需配合 sourceMap,才能还原真实的错误行号。
- 打包时生成 sourceMap 文件
- 压缩 JS 文件的末尾添加一行注释,指定 sourceMap 的路径
- 把 sourceMap 文件也部署到 CDN
3. Promise 错误:unhandledrejection
当 Promise 被 reject
但没有 catch
时,会触发 unhandledrejection
事件
window.addEventListener('unhandledrejection', (event) => { // 阻止浏览器默认提示(部分浏览器会在控制台警告) event.preventDefault(); const error = event.reason; // Promise拒绝的原因(Error对象) console.error('未捕获的Promise错误:', { message: error.message, stack: error.stack, // 额外信息:比如是哪个接口请求失败 requestUrl: error.config?.url || '未知' }); // 上报异步错误 reportAsyncError(error); }); Promise.reject(new Error('Unhandled Promise rejection'))
二、Vue:框架级错误处理的“分层策略”
Vue 内部封装了一套 “组件内捕获 + 全局汇总” 的错误处理机制,既能精确监控组件错误,又能统一管理全局异常。
Vue 会对 组件渲染、指令执行、生命周期钩子 等核心流程等错误进行拦截,但对非核心流程的错误不拦截。
为什么拦截?
Vue 的设计理念是 “局部错误隔离”—— 某个组件出错,只销毁该组件,不影响其他组件渲染。如果将错误抛给 window.onerror
,可能导致开发者误判为 “全局错误”,且不符合框架的容错逻辑。
为什么不拦截?
非核心流程这类错误属于 “开发者主动编写的业务逻辑错误”(而非框架渲染链路错误),Vue 认为开发者应自行处理(如事件中用 try/catch),因此不主动拦截。
1. 组件级:errorCaptured 生命周期
如果某个组件是 “高危区域”(如复杂表单、第三方图表),用 errorCaptured
在组件内拦截错误,避免影响全局。
特点:
- 监听所有下级组件的错误
- 返回 false 会阻止向上传播(阻止传播到 window.onerror)
- 不能监听异步错误
<!-- ErrorSafeChart.vue:包裹易出错的图表组件 --> <template> <div class="chart-container"> <!-- 错误时显示降级UI --> <div v-if="hasError" class="error-tip"> <icon name="warning" /> 图表加载失败,点击重试 <button @click="resetError">重试</button> </div> <ComplexChart v-else :data="chartData" /> </div> </template> <script setup> import { ref } from 'vue'; import ComplexChart from './ComplexChart.vue'; const hasError = ref(false); const chartData = ref([]); // 组件内错误捕获生命周期 const errorCaptured = (error, instance, info) => { // 参数说明: // error:错误对象 // instance:发生错误的子组件实例(这里是ComplexChart) // info:错误发生的场景(如"render"渲染时、"watch"监听时) hasError.value = true; console.error('图表组件错误:', { component: instance.$options.name || 'ComplexChart', scene: info, error: error.message }); // 上报组件错误 reportVueComponentError(error, instance, info); // 关键:返回false阻止错误向上传播(不会触发全局errorHandler和window.onerror) // 适合处理已知的非致命错误,避免全局告警 return false; }; // 重置错误状态(重试逻辑) const resetError = () => { hasError.value = false; // 重新加载图表数据 fetchChartData(); }; // 加载图表数据 const fetchChartData = async () => { // ... 数据请求逻辑 }; </script>
适用场景:
- 监控特定组件(如支付表单、地图组件)的错误
- 对错误进行差异化降级(如图表错了显示静态图片,表单错了保留用户输入)
2. 全局级:app.config.errorHandler
所有未被errorCaptured拦截的组件错误,都会汇总到errorHandler,适合做全局统一处理。
特点:
- Vue 全局错误监听,所有组件错误都汇总到这里
- 如果
errorCaptured
返回false
,不会传播到这里 - 和
window.onerror
互斥 - 不能监听异步错误
// main.js:Vue入口文件 import { createApp } from 'vue'; import App from './App.vue'; import { reportVueGlobalError } from './utils/errorReport'; const app = createApp(App); // 配置Vue全局错误处理器 app.config.errorHandler = (error, instance, info) => { // 过滤掉已处理过的错误(比如某些组件手动标记过) if (error.__vue_handled__) return; console.error('Vue全局错误:', { component: instance?.$options.name || '根组件', scene: info, // 如"render"、"watch"、"v-on handler" message: error.message, stack: error.stack }); // 标记错误已处理,避免重复上报 error.__vue_handled__ = true; // 上报全局错误(生产环境必加) reportVueGlobalError(error, instance, info); // 可选:全局提示用户(如顶部Toast) showGlobalToast('系统出现小错误,请刷新页面重试'); }; // 配置Vue警告处理器(开发环境辅助调试) if (import.meta.env.DEV) { app.config.warnHandler = (msg, instance, trace) => { console.warn('Vue警告:', { message: msg, component: instance?.$options.name, trace: trace // 警告的调用栈 }); }; } app.mount('#app');
三、React:ErrorBoundary 组件的“优雅降级”
React 的错误处理核心是 ErrorBoundary
—— 一个特殊的组件,能捕获子组件树的渲染错误,并显示降级 UI,类似 Vue 的 errorCaptured
,但功能更聚焦。
1. 实现一个通用 ErrorBoundary
// components/ErrorBoundary.jsx import React from 'react'; import { reportReactError } from '../utils/errorReport'; import ErrorFallback from './ErrorFallback'; // 自定义降级UI组件 class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, // 是否发生错误 error: null, // 错误对象 errorInfo: null // 错误详情(含组件栈) }; } // 静态方法:更新错误状态(渲染前触发) static getDerivedStateFromError(error) { // 返回新状态,让下一次渲染显示降级UI return { hasError: true, error }; } // 错误发生后触发(可执行副作用,如日志上报) componentDidCatch(error, errorInfo) { // 记录错误详情(errorInfo含componentStack,能定位哪个组件出错) this.setState({ errorInfo }); console.error('React组件错误:', { error: error.message, componentStack: errorInfo.componentStack, // 从props获取额外上下文(如当前页面路由) page: this.props.currentPage }); // 上报React错误 reportReactError({ error, componentStack: errorInfo.componentStack, page: this.props.currentPage }); } // 重置错误状态(支持重试) resetError = () => { this.setState({ hasError: false, error: null, errorInfo: null }); }; render() { const { hasError, error, errorInfo } = this.state; const { children, fallback } = this.props; // 有错误:显示降级UI(优先用props传入的fallback,否则用默认) if (hasError) { return fallback ? ( React.cloneElement(fallback, { error, resetError: this.resetError }) ) : ( <ErrorFallback error={error} errorInfo={errorInfo} onReset={this.resetError} /> ); } // 无错误:渲染子组件 return children; } } export default ErrorBoundary;
2. 如何使用 ErrorBoundary
ErrorBoundary 是 “容器组件”,只需包裹需要监控的子组件树即可,推荐在路由级别或核心功能模块使用:
(1)全局路由级包裹
// App.jsx import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import ErrorBoundary from './components/ErrorBoundary'; import Home from './pages/Home'; import Order from './pages/Order'; import NotFound from './pages/NotFound'; import GlobalErrorFallback from './components/GlobalErrorFallback'; function App() { return ( <Router> {/* 全局ErrorBoundary:捕获所有路由页面的错误 */} <ErrorBoundary currentPage="全局" fallback={<GlobalErrorFallback />} > <Routes> {/* 页面级ErrorBoundary:针对特定页面单独处理 */} <Route path="/order" element={ <ErrorBoundary currentPage="订单页" fallback={<div>订单页加载失败,<button onClick={(e) => e.target.onReset()}>重试</button></div>} > <Order /> </ErrorBoundary> } /> <Route path="/" element={<Home />} /> <Route path="*" element={<NotFound />} /> </Routes> </ErrorBoundary> </Router> ); } export default App;
(2)组件级包裹(高危组件)
// pages/Home.jsx import ErrorBoundary from '../components/ErrorBoundary'; import PaymentForm from '../components/PaymentForm'; // 高危组件:支付表单 import ProductList from '../components/ProductList'; function Home() { return ( <div className="home-page"> <h1>首页</h1> {/* 只包裹高危组件,不影响其他部分 */} <ErrorBoundary currentPage="首页-支付表单" fallback={<div>支付表单加载失败,请稍后再试</div>} > <PaymentForm /> </ErrorBoundary> <ProductList /> {/* 普通组件,不包裹 */} </div> ); }
3. ErrorBoundary 的“盲区”
ErrorBoundary 只监听组件渲染时报错,不监听dom事件、异步错误。
原因:
- 事件处理器中的代码并不在 React 的渲染阶段执行。在
onClick, onChange
等回调中抛出错误时,React 的渲染流程已经结束,错误发生在浏览器正常的事件调用栈中,ErrorBoundary
无从干预。这个时候可以通过try...catch
手动捕获错误。 setTimeout
setInterval
Promise
的.then()
或async/await
中的异步操作,其回调执行时已经脱离了最初的 React 渲染上下文和调用栈。ErrorBoundary
监听的是渲染期间的同步错误。
注意:开发环境下,React 会通过 iframe
的方式把错误的 stack
直接覆盖在页面上(如要调试 UI ,可以通过控制台删掉这个 iframe
),便于调试,生产环境才会直接显示 UI。
4. 避免创建类式组件
上述可以看出,ErrorBoundary 是通过 getDerivedStateFromError
和 componentDidCatch
去更新错误状态及执行副作用的,而这两个生命周期都是类才具有的方法。
函数式组件中目前还没有与 static getDerivedStateFromError
直接等同的东西。如果你想避免创建类式组件,请像上面那样编写一个 ErrorBoundary
组件,并在整个应用程序中使用它。或者使用 react-error-boundary
包来执行此操作。
总结
到此这篇关于从原生到框架之前端的错误监听的文章就介绍到这了,更多相关前端的错误监听内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!