错误边界与全局错误捕获
00:00
JavaScript错误边界与全局错误捕获:ErrorBoundary、window.onerror、unhandledrejection。
1. JavaScript 错误类型
1.1 错误分类
| 错误类型 | 触发时机 | 可捕获 |
|---|---|---|
| 语法错误 | 解析阶段 | 否(无法运行) |
| 运行时错误 | 执行阶段 | 是 |
| 异步错误 | setTimeout/Promise 等 | 需特殊处理 |
| 资源加载错误 | img/script 加载失败 | 需特殊处理 |
1.2 错误对象
try {
undefinedFunction();
} catch (error) {
console.log(error instanceof Error); // true
console.log(error instanceof TypeError); // true
console.log(error.message); // "undefinedFunction is not defined"
console.log(error.stack); // 完整调用栈
}
2. 全局错误捕获
2.1 window.onerror
window.onerror = (message, source, lineno, colno, error) => {
console.log('全局错误:', { message, source, lineno, colno, error });
// 上报错误
reportError({
type: 'runtime',
message,
source,
line: lineno,
column: colno,
stack: error?.stack,
});
return true; // 阻止默认错误处理(控制台输出)
};
2.2 window.addEventListener(‘error’)
window.addEventListener(
'error',
(event) => {
// 捕获运行时错误和资源加载错误
if (event.target instanceof HTMLElement) {
// 资源加载错误
console.log('资源加载失败:', event.target.src || event.target.href);
} else {
// 运行时错误
console.log('运行时错误:', event.message);
}
},
true
); // 使用捕获阶段
2.3 unhandledrejection
window.addEventListener('unhandledrejection', (event) => {
console.log('未处理的 Promise 拒绝:', event.reason);
reportError({
type: 'unhandledrejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
});
event.preventDefault(); // 阻止控制台警告
});
2.4 完整的全局错误监控
class ErrorMonitor {
constructor() {
this.errors = [];
this.init();
}
init() {
// 运行时错误
window.addEventListener(
'error',
(event) => {
if (event.target && event.target !== window) {
this.track({
type: 'resource',
message: `Failed to load: ${event.target.src || event.target.href}`,
tagName: event.target.tagName,
});
} else {
this.track({
type: 'runtime',
message: event.message,
filename: event.filename,
line: event.lineno,
column: event.colno,
stack: event.error?.stack,
});
}
},
true
);
// Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
this.track({
type: 'unhandledrejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
});
event.preventDefault();
});
}
track(error) {
this.errors.push({
...error,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent,
});
this.report(error);
}
report(error) {
// 发送到错误收集服务
navigator.sendBeacon('/api/errors', JSON.stringify(error));
}
}
3. React Error Boundary
3.1 类组件实现
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
reportError({
type: 'react',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div role="alert">
<h2>出错了</h2>
<p>{this.state.error.message}</p>
<button onClick={() => this.setState({ hasError: false })}>重试</button>
</div>
)
);
}
return this.props.children;
}
}
// 使用
<ErrorBoundary fallback={<ErrorPage />}>
<App />
</ErrorBoundary>;
3.2 函数式 Error Boundary(react-error-boundary)
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>出错了: {error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
/* 重置状态 */
}}
onError={(error, info) => reportError(error, info)}
>
<Dashboard />
</ErrorBoundary>
);
}
3.3 细粒度错误边界
function Dashboard() {
return (
<div>
<ErrorBoundary FallbackComponent={WidgetError}>
<ChartWidget />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetError}>
<TableWidget />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetError}>
<StatsWidget />
</ErrorBoundary>
</div>
);
}
// 一个 Widget 崩溃不影响其他 Widget
4. Vue 错误处理
4.1 全局错误处理器
app.config.errorHandler = (err, instance, info) => {
console.error('Vue Error:', err);
reportError({
type: 'vue',
message: err.message,
stack: err.stack,
component: instance?.$options?.name,
info,
});
};
4.2 errorCaptured 钩子
export default {
name: 'ErrorBoundary',
data: () => ({ error: null }),
errorCaptured(err, vm, info) {
this.error = err;
reportError({ type: 'vue-captured', message: err.message, info });
return false; // 阻止错误继续向上传播
},
render() {
return this.error ? this.$slots.fallback?.() || h('div', '出错了') : this.$slots.default?.();
},
};
5. 错误上报最佳实践
5.1 错误聚合
function aggregateErrors(errors) {
const groups = new Map();
errors.forEach((error) => {
const key = `${error.type}:${error.message}:${error.source}:${error.line}`;
const group = groups.get(key) || { ...error, count: 0, firstSeen: error.timestamp };
group.count++;
group.lastSeen = error.timestamp;
groups.set(key, group);
});
return Array.from(groups.values());
}
5.2 Source Map 还原
// 使用 source-map 库还原压缩后的错误位置
import { SourceMapConsumer } from 'source-map';
async function getOriginalPosition(sourceMap, line, column) {
const consumer = await new SourceMapConsumer(sourceMap);
const pos = consumer.originalPositionFor({ line, column });
consumer.destroy();
return pos;
}
5.3 采样策略
function shouldReport(error) {
// 1% 采样率
if (Math.random() > 0.01) return false;
// 忽略已知无害错误
const ignoredMessages = ['ResizeObserver loop', 'Script error'];
if (ignoredMessages.some((msg) => error.message?.includes(msg))) return false;
return true;
}