工程化与框架系列29-前端错误处理实践
目录
工程化与框架系列(29)–前端错误处理实践
前端错误处理实践 🔧
引言
前端错误处理是保证应用稳定性和用户体验的关键环节。本文将深入探讨前端错误处理的最佳实践,包括错误捕获、监控、上报和恢复等方面,帮助开发者构建更加健壮的前端应用。
错误处理概述
前端错误处理主要包括以下方面:
- 错误捕获 :运行时错误、Promise错误、网络错误等
- 错误监控 :错误收集、分析和统计
- 错误上报 :错误信息发送到服务器
- 错误恢复 :优雅降级和容错处理
- 调试支持 :错误定位和调试辅助
错误处理实现
错误监控管理器
// 错误监控管理器
class ErrorMonitor {
private static instance: ErrorMonitor;
private isInitialized: boolean;
private config: ErrorMonitorConfig;
private errorQueue: ErrorInfo[];
private timer: number | null;
private constructor() {
this.isInitialized = false;
this.config = {
appId: '',
appVersion: '',
maxQueueSize: 100,
flushInterval: 5000,
reportUrl: '',
ignoreErrors: []
};
this.errorQueue = [];
this.timer = null;
}
// 获取单例实例
static getInstance(): ErrorMonitor {
if (!ErrorMonitor.instance) {
ErrorMonitor.instance = new ErrorMonitor();
}
return ErrorMonitor.instance;
}
// 初始化监控器
initialize(config: Partial<ErrorMonitorConfig>): void {
if (this.isInitialized) {
return;
}
this.config = { ...this.config, ...config };
// 注册全局错误处理器
this.registerErrorHandlers();
// 启动定时上报
this.startAutoReport();
this.isInitialized = true;
}
// 手动上报错误
report(error: Error | string, extra?: Record<string, any>): void {
const errorInfo = this.createErrorInfo(error, extra);
this.addToQueue(errorInfo);
}
// 立即上报所有错误
flush(): Promise<void> {
if (this.errorQueue.length === 0) {
return Promise.resolve();
}
const errors = [...this.errorQueue];
this.errorQueue = [];
return this.sendToServer(errors);
}
// 注册错误处理器
private registerErrorHandlers(): void {
// 处理未捕获的错误
window.addEventListener('error', (event) => {
if (this.shouldIgnoreError(event.error)) {
return;
}
const errorInfo = this.createErrorInfo(event.error, {
type: 'uncaught',
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
this.addToQueue(errorInfo);
});
// 处理未处理的Promise拒绝
window.addEventListener('unhandledrejection', (event) => {
if (this.shouldIgnoreError(event.reason)) {
return;
}
const errorInfo = this.createErrorInfo(event.reason, {
type: 'unhandledrejection'
});
this.addToQueue(errorInfo);
});
// 处理资源加载错误
window.addEventListener('error', (event) => {
if (event.target && (event.target as HTMLElement).nodeName) {
const target = event.target as HTMLElement;
const errorInfo = this.createErrorInfo(new Error('Resource load failed'), {
type: 'resource',
tagName: target.nodeName.toLowerCase(),
src: (target as HTMLImageElement | HTMLScriptElement).src ||
(target as HTMLLinkElement).href
});
this.addToQueue(errorInfo);
}
}, true);
}
// 创建错误信息
private createErrorInfo(
error: Error | string,
extra?: Record<string, any>
): ErrorInfo {
const errorObj = error instanceof Error ? error : new Error(error);
const timestamp = Date.now();
return {
appId: this.config.appId,
appVersion: this.config.appVersion,
timestamp,
url: window.location.href,
userAgent: navigator.userAgent,
message: errorObj.message,
stack: errorObj.stack,
...extra
};
}
// 添加到错误队列
private addToQueue(errorInfo: ErrorInfo): void {
this.errorQueue.push(errorInfo);
// 队列超出限制时立即上报
if (this.errorQueue.length >= this.config.maxQueueSize) {
this.flush();
}
}
// 启动自动上报
private startAutoReport(): void {
if (this.timer !== null) {
return;
}
this.timer = window.setInterval(() => {
this.flush();
}, this.config.flushInterval);
}
// 停止自动上报
private stopAutoReport(): void {
if (this.timer === null) {
return;
}
window.clearInterval(this.timer);
this.timer = null;
}
// 发送错误到服务器
private async sendToServer(errors: ErrorInfo[]): Promise<void> {
try {
const response = await fetch(this.config.reportUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(errors)
});
if (!response.ok) {
throw new Error('Failed to report errors');
}
} catch (error) {
console.error('Error reporting failed:', error);
// 重新加入队列
this.errorQueue.push(...errors);
}
}
// 判断是否忽略错误
private shouldIgnoreError(error: any): boolean {
return this.config.ignoreErrors.some(pattern => {
if (pattern instanceof RegExp) {
return pattern.test(error.message || error);
}
return pattern === error.message || pattern === error;
});
}
}
// 错误监控配置接口
interface ErrorMonitorConfig {
appId: string;
appVersion: string;
maxQueueSize: number;
flushInterval: number;
reportUrl: string;
ignoreErrors: (string | RegExp)[];
}
// 错误信息接口
interface ErrorInfo {
appId: string;
appVersion: string;
timestamp: number;
url: string;
userAgent: string;
message: string;
stack?: string;
[key: string]: any;
}
// 使用示例
const errorMonitor = ErrorMonitor.getInstance();
// 初始化错误监控
errorMonitor.initialize({
appId: 'my-app',
appVersion: '1.0.0',
reportUrl: '/api/errors',
ignoreErrors: [
/Script error/,
'Network error'
]
});
// 手动上报错误
try {
throw new Error('Something went wrong');
} catch (error) {
errorMonitor.report(error, {
type: 'business',
module: 'payment'
});
}
错误边界组件
// 错误边界基类
abstract class ErrorBoundary extends HTMLElement {
private root: ShadowRoot;
private hasError: boolean;
private error: Error | null;
constructor() {
super();
this.root = this.attachShadow({ mode: 'open' });
this.hasError = false;
this.error = null;
this.initialize();
}
// 初始化组件
private initialize(): void {
// 监听子元素错误
this.addEventListener('error', this.handleError.bind(this));
// 渲染初始内容
this.render();
}
// 处理错误
private handleError(event: ErrorEvent): void {
event.preventDefault();
this.hasError = true;
this.error = event.error;
// 上报错误
ErrorMonitor.getInstance().report(event.error, {
type: 'component',
tagName: this.tagName.toLowerCase()
});
// 重新渲染
this.render();
}
// 重置错误状态
protected reset(): void {
this.hasError = false;
this.error = null;
this.render();
}
// 渲染组件
private render(): void {
this.root.innerHTML = this.hasError
? this.renderError(this.error!)
: this.renderContent();
}
// 渲染正常内容(子类实现)
protected abstract renderContent(): string;
// 渲染错误内容(子类实现)
protected abstract renderError(error: Error): string;
}
// 错误边界示例
class UserProfileErrorBoundary extends ErrorBoundary {
private userId: string;
constructor() {
super();
this.userId = '';
}
// 观察的属性
static get observedAttributes() {
return ['user-id'];
}
// 属性变化处理
attributeChangedCallback(
name: string,
oldValue: string,
newValue: string
) {
if (name === 'user-id') {
this.userId = newValue;
this.reset();
}
}
// 渲染用户信息
protected renderContent(): string {
return `
<div class="user-profile">
<h2>User Profile</h2>
<div class="loading">Loading user ${this.userId}...</div>
</div>
<style>
.user-profile {
padding: 16px;
border: 1px solid #eee;
border-radius: 4px;
}
.loading {
color: #666;
font-style: italic;
}
</style>
`;
}
// 渲染错误信息
protected renderError(error: Error): string {
return `
<div class="error">
<h3>Something went wrong</h3>
<p>${error.message}</p>
<button onclick="this.getRootNode().host.reset()">
Retry
</button>
</div>
<style>
.error {
padding: 16px;
border: 1px solid #f66;
border-radius: 4px;
background: #fee;
color: #c00;
}
button {
margin-top: 8px;
padding: 4px 12px;
border: 1px solid #c00;
border-radius: 4px;
background: #fff;
color: #c00;
cursor: pointer;
}
button:hover {
background: #c00;
color: #fff;
}
</style>
`;
}
}
// 注册组件
customElements.define('error-boundary', UserProfileErrorBoundary);
// 使用示例
const template = `
<error-boundary user-id="123">
<user-profile></user-profile>
</error-boundary>
`;
网络错误处理
// 网络请求客户端
class HttpClient {
private baseUrl: string;
private timeout: number;
private retryCount: number;
private retryDelay: number;
constructor(config: HttpClientConfig) {
this.baseUrl = config.baseUrl || '';
this.timeout = config.timeout || 10000;
this.retryCount = config.retryCount || 3;
this.retryDelay = config.retryDelay || 1000;
}
// 发送请求
async request<T>(config: RequestConfig): Promise<T> {
let lastError: Error | null = null;
// 重试机制
for (let i = 0; i <= this.retryCount; i++) {
try {
return await this.sendRequest<T>(config);
} catch (error) {
lastError = error as Error;
// 判断是否需要重试
if (!this.shouldRetry(error) || i === this.retryCount) {
break;
}
// 等待延迟时间
await this.delay(this.retryDelay * Math.pow(2, i));
}
}
// 处理错误
this.handleError(lastError!);
throw lastError;
}
// 发送单次请求
private async sendRequest<T>(config: RequestConfig): Promise<T> {
const url = this.baseUrl + config.url;
const controller = new AbortController();
// 设置超时
const timeoutId = setTimeout(() => {
controller.abort();
}, this.timeout);
try {
const response = await fetch(url, {
method: config.method || 'GET',
headers: {
'Content-Type': 'application/json',
...config.headers
},
body: config.data ? JSON.stringify(config.data) : undefined,
signal: controller.signal
});
clearTimeout(timeoutId);
// 处理响应
if (!response.ok) {
throw new HttpError(
response.statusText,
response.status,
await response.json()
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
if (error.name === 'AbortError') {
throw new HttpError('Request timeout', 408);
}
throw new HttpError(error.message, 0);
}
throw error;
}
}
// 判断是否需要重试
private shouldRetry(error: any): boolean {
if (!(error instanceof HttpError)) {
return false;
}
// 根据状态码判断
const retryableStatus = [408, 500, 502, 503, 504];
return retryableStatus.includes(error.status);
}
// 处理错误
private handleError(error: Error): void {
// 上报错误
ErrorMonitor.getInstance().report(error, {
type: 'http',
url: error instanceof HttpError ? error.url : undefined,
status: error instanceof HttpError ? error.status : undefined
});
}
// 延迟函数
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// HTTP错误类
class HttpError extends Error {
constructor(
message: string,
public status: number,
public data?: any
) {
super(message);
this.name = 'HttpError';
}
}
// 配置接口
interface HttpClientConfig {
baseUrl?: string;
timeout?: number;
retryCount?: number;
retryDelay?: number;
}
interface RequestConfig {
url: string;
method?: string;
headers?: Record<string, string>;
data?: any;
}
// 使用示例
const http = new HttpClient({
baseUrl: 'https://api.example.com',
timeout: 5000,
retryCount: 3
});
// 发送请求
async function fetchUserProfile(userId: string) {
try {
const user = await http.request({
url: `/users/${userId}`,
method: 'GET'
});
return user;
} catch (error) {
if (error instanceof HttpError) {
switch (error.status) {
case 404:
throw new Error('User not found');
case 401:
throw new Error('Unauthorized');
default:
throw new Error('Failed to fetch user profile');
}
}
throw error;
}
}
最佳实践与建议
错误分类
- 运行时错误
- 网络错误
- 业务错误
- 资源错误
错误处理策略
- 全局统一处理
- 优雅降级
- 错误恢复
- 用户反馈
错误上报
- 错误去重
- 采样上报
- 批量上报
- 错误分析
调试支持
- 源码映射
- 错误定位
- 环境信息
- 用户行为
总结
前端错误处理需要考虑以下方面:
- 错误捕获与分类
- 错误监控与上报
- 错误恢复与降级
- 调试支持与分析
- 用户体验优化
通过合理的错误处理机制,可以提高应用的稳定性和可用性。
学习资源
- 错误处理最佳实践
- 错误监控平台
- 调试工具指南
- 性能优化建议
- 用户体验设计
如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇
终身学习,共同成长。
咱们下一期见
💻