目录

工程化与框架系列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;
    }
}

最佳实践与建议

  1. 错误分类

    • 运行时错误
    • 网络错误
    • 业务错误
    • 资源错误
  2. 错误处理策略

    • 全局统一处理
    • 优雅降级
    • 错误恢复
    • 用户反馈
  3. 错误上报

    • 错误去重
    • 采样上报
    • 批量上报
    • 错误分析
  4. 调试支持

    • 源码映射
    • 错误定位
    • 环境信息
    • 用户行为

总结

前端错误处理需要考虑以下方面:

  1. 错误捕获与分类
  2. 错误监控与上报
  3. 错误恢复与降级
  4. 调试支持与分析
  5. 用户体验优化

通过合理的错误处理机制,可以提高应用的稳定性和可用性。

学习资源

  1. 错误处理最佳实践
  2. 错误监控平台
  3. 调试工具指南
  4. 性能优化建议
  5. 用户体验设计

如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇

终身学习,共同成长。

咱们下一期见

💻