前言
一个健壮的错误处理机制可以提升用户体验,帮助开发者快速定位问题。今天分享如何实现完善的错误处理和监控!
错误分类
错误类型
├── Vue 渲染错误
│ ├── 组件渲染错误
│ ├── 生命周期钩子错误
│ └── 模板语法错误
├── JavaScript 运行时错误
│ ├── TypeError
│ ├── ReferenceError
│ └── 自定义业务错误
├── 异步错误
│ ├── Promise rejection
│ ├── setTimeout/setInterval
│ └── Web API 错误
└── 资源加载错误
├── 图片加载失败
├── 脚本加载失败
└── API 请求失败
核心实现
1. 全局错误处理
// src/utils/errorHandler.ts
import { isDev } from './env'
// 错误日志服务
class ErrorLogger {
private logs: ErrorLog[] = []
private maxLogs = 100
log(error: Error, context?: string) {
const log: ErrorLog = {
id: `err_${Date.now()}`,
message: error.message,
stack: error.stack,
context,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
}
this.logs.unshift(log)
// 只保留最近 N 条
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(0, this.maxLogs)
}
// 开发环境输出到控制台
if (isDev) {
console.error('[Error]', context, error)
}
// 上报到服务器(生产环境)
if (!isDev) {
this.reportToServer(log)
}
}
private async reportToServer(log: ErrorLog) {
// 可以发送到你的日志服务
try {
await fetch('/api/error-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(log)
})
} catch (e) {
// 存储到 localStorage 作为后备
this.saveToLocalStorage(log)
}
}
private saveToLocalStorage(log: ErrorLog) {
const key = 'error_logs'
const existing = JSON.parse(localStorage.getItem(key) || '[]')
existing.push(log)
localStorage.setItem(key, JSON.stringify(existing.slice(-50)))
}
getLogs(): ErrorLog[] {
return this.logs
}
}
interface ErrorLog {
id: string
message: string
stack?: string
context?: string
timestamp: number
userAgent: string
url: string
}
export const errorLogger = new ErrorLogger()
2. Vue 错误处理器
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { errorLogger } from '@/utils/errorHandler'
const app = createApp(App)
// Vue 渲染错误处理
app.config.errorHandler = (err, instance, info) => {
// err: 错误对象
// instance: 发生错误的组件实例
// info: 额外的错误信息(如生命周期钩子名称)
console.error('Vue Error:', err)
console.error('Component:', instance)
console.error('Info:', info)
errorLogger.log(err, `Vue Error [${info}]`)
// 可以在这里显示用户友好的错误提示
// showErrorToast('发生了一些问题,请稍后重试')
}
// 组件警告处理(开发环境)
if (import.meta.env.DEV) {
app.config.warnHandler = (msg, instance, trace) => {
console.warn('Vue Warn:', msg)
console.warn('Trace:', trace)
}
}
// 异步错误处理
app.config.asyncErrorHandler = (err, instance, info) => {
errorLogger.log(err, `Async Error [${info}]`)
}
app.mount('#app')
3. Promise 错误处理
// src/utils/promiseHandler.ts
// 全局未处理的 Promise rejection
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled Promise Rejection:', event.reason)
errorLogger.log(
event.reason instanceof Error
? event.reason
: new Error(String(event.reason)),
'Unhandled Promise Rejection'
)
// 阻止默认行为(显示控制台错误)
event.preventDefault()
})
// 安全执行 Promise
export async function safeAsync<T>(
promise: Promise<T>,
fallback?: T
): Promise<T | undefined> {
try {
return await promise
} catch (error) {
errorLogger.log(
error instanceof Error ? error : new Error(String(error)),
'Safe Async'
)
return fallback
}
}
// Promise 包装函数
export function to<T>(promise: Promise<T>): Promise<[Error | null, T | null]> {
return promise
.then<[null, T]>((data) => [null, data])
.catch<[Error, null]>((err) => [err, null])
}
4. API 请求错误处理
// src/utils/request.ts
import { errorLogger } from './errorHandler'
import { ElMessage } from 'element-plus'
interface RequestOptions {
showError?: boolean
errorContext?: string
}
export async function request<T>(
url: string,
options: RequestOptions = {}
): Promise<T> {
const { showError = true, errorContext = 'API Request' } = options
try {
const response = await fetch(url)
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
errorLogger.log(error, errorContext)
if (showError) {
ElMessage.error(`请求失败: ${error.message}`)
}
throw error
}
return await response.json()
} catch (error) {
if (error instanceof Error) {
errorLogger.log(error, errorContext)
if (showError && !error.message.includes('HTTP')) {
ElMessage.error('网络请求失败,请检查网络连接')
}
}
throw error
}
}
// 使用示例
async function fetchArticle(id: string) {
return request<Article>(`/api/articles/${id}`, {
showError: true,
errorContext: 'fetchArticle'
})
}
5. 资源加载错误处理
// src/composables/useResourceError.ts
import { onMounted, onErrorCaptured } from 'vue'
export function useResourceError() {
// 图片加载失败
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement
img.src = '/default-image.png' // 默认图片
img.classList.add('error-loaded')
}
// 脚本加载失败
function loadScript(src: string): Promise<void> {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.onload = () => resolve()
script.onerror = () => {
reject(new Error(`Failed to load script: ${src}`))
}
document.head.appendChild(script)
})
}
// Vue 组件内错误捕获
onErrorCaptured((err, instance, info) => {
console.error('Captured in component:', err)
console.error('Component:', instance)
console.error('Info:', info)
// 返回 false 阻止错误继续传播
return false
})
return {
handleImageError,
loadScript
}
}
6. 错误边界组件
<!-- src/components/ErrorBoundary.vue -->
<template>
<slot v-if="!hasError" />
<div v-else class="error-boundary">
<div class="error-content">
<div class="error-icon">😢</div>
<h2>页面出现了一些问题</h2>
<p>别担心,这只是一个小插曲</p>
<div class="error-actions">
<el-button type="primary" @click="handleRetry">
重试一下
</el-button>
<el-button @click="handleGoHome">
返回首页
</el-button>
</div>
<details v-if="showDetails" class="error-details">
<summary>查看错误详情</summary>
<pre>{{ errorMessage }}</pre>
</details>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const hasError = ref(false)
const errorMessage = ref('')
const showDetails = ref(false)
onErrorCaptured((err, instance, info) => {
hasError.value = true
errorMessage.value = `${err.message}\n\nComponent: ${instance?.$options?.name || 'Unknown'}\nInfo: ${info}`
console.error('ErrorBoundary caught:', err)
// 返回 false 阻止错误传播
return false
})
function handleRetry() {
hasError.value = false
errorMessage.value = ''
}
function handleGoHome() {
hasError.value = false
router.push('/')
}
</script>
<style scoped>
.error-boundary {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
padding: 40px;
}
.error-content {
text-align: center;
max-width: 400px;
}
.error-icon {
font-size: 64px;
margin-bottom: 20px;
}
.error-content h2 {
margin: 0 0 8px;
font-size: 24px;
}
.error-content p {
color: #666;
margin-bottom: 24px;
}
.error-actions {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 24px;
}
.error-details {
text-align: left;
margin-top: 20px;
}
.error-details pre {
background: #f5f5f5;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
font-size: 12px;
}
</style>
7. 使用 ErrorBoundary
<!-- src/App.vue -->
<template>
<ErrorBoundary>
<router-view />
</ErrorBoundary>
</template>
<script setup lang="ts">
import ErrorBoundary from '@/components/ErrorBoundary.vue'
</script>
错误监控服务
推荐工具
- Sentry - 功能强大的错误追踪服务
- FunDebug - 国产前端监控工具
- Badjs - 腾讯开源的前端监控
Sentry 集成
npm install @sentry/vue @sentry/tracing
// src/utils/sentry.ts
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'
export function initSentry(app: any) {
Sentry.init({
app,
dsn: 'YOUR_SENTRY_DSN',
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
}),
],
environment: import.meta.env.MODE,
beforeSend(event) {
// 过滤非关键错误
if (event.exception?.values?.[0]?.type === 'AbortError') {
return null
}
return event
}
})
}
💡 最佳实践
- 始终在 Promise 后添加 .catch()
- 使用 try-catch 包装异步代码
- 提供用户友好的错误提示
- 记录详细错误日志便于调试
🔗 相关资源
- Sentry:sentry.io
- FunDebug:fundebug.com