/**
* 前端全局错误监控 & 崩溃监控 封装
* 功能:捕获JS报错、Vue报错、异步报错、资源加载失败、白屏、页面崩溃
* 存储:使用原生 IndexedDB 本地存储(不丢数据、支持大容量)
* 上报:有网时自动上报,断网时本地保存,下次启动自动续报
* 适用:Vue3 项目(任何封装框架都能用)
*/
class ErrorMonitor {
constructor() {
// 数据库配置
this.dbName = "ErrorMonitorDB"; // 数据库名
this.storeName = "errorLogs"; // 表名
this.db = null;
// 初始化
this.initDB();
this.bindErrorEvents();
// 页面启动时,自动上报之前未发送的错误
this.uploadOnStart();
}
// ==============================================
// 1. 初始化 IndexedDB(原生,无依赖)
// ==============================================
initDB() {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = (e) => {
this.db = e.target.result;
if (!this.db.objectStoreNames.contains(this.storeName)) {
this.db.createObjectStore(this.storeName, { keyPath: "id", autoIncrement: true });
}
};
request.onsuccess = (e) => {
this.db = e.target.result;
};
}
// ==============================================
// 2. 保存错误到本地 IndexedDB
// ==============================================
saveToLocal(data) {
return new Promise((resolve) => {
if (!this.db) return resolve();
const tx = this.db.transaction(this.storeName, "readwrite");
const store = tx.objectStore(this.storeName);
const log = {
...data,
url: location.href,
ua: navigator.userAgent,
time: new Date().toLocaleString(),
};
store.add(log);
tx.oncomplete = () => resolve();
});
}
// ==============================================
// 3. 获取所有本地错误
// ==============================================
getLocalLogs() {
return new Promise((resolve) => {
if (!this.db) return resolve([]);
const tx = this.db.transaction(this.storeName, "readonly");
const store = tx.objectStore(this.storeName);
const logs = [];
store.openCursor().onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) {
logs.push(cursor.value);
cursor.continue();
} else {
resolve(logs);
}
};
});
}
// ==============================================
// 4. 清空本地错误
// ==============================================
clearLocalLogs() {
return new Promise((resolve) => {
if (!this.db) return resolve();
const tx = this.db.transaction(this.storeName, "readwrite");
tx.objectStore(this.storeName).clear();
tx.oncomplete = () => resolve();
});
}
// ==============================================
// 5. 上报错误(后台接口)
// ==============================================
async upload(data) {
try {
// 可换成你们后台真实接口
await fetch("/api/error/report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
} catch (err) {
// 上报失败 → 已经存在本地,不处理
}
}
// ==============================================
// 6. 统一入口:先存本地 → 再上报
// ==============================================
async capture(data) {
await this.saveToLocal(data);
this.upload(data);
}
// ==============================================
// 7. 页面启动时自动上报本地积压的错误
// ==============================================
async uploadOnStart() {
const logs = await this.getLocalLogs();
if (logs.length === 0) return;
await this.upload({ type: "batch", list: logs });
await this.clearLocalLogs();
}
// ==============================================
// 8. 绑定所有浏览器错误监听
// ==============================================
bindErrorEvents() {
const monitor = this;
// ------------------------------
// JS 同步错误
// ------------------------------
window.onerror = function (msg, url, line, col, err) {
monitor.capture({
type: "js_error",
message: msg,
stack: err?.stack,
});
return true;
};
// ------------------------------
// Promise / async 错误
// ------------------------------
window.addEventListener("unhandledrejection", (e) => {
monitor.capture({
type: "promise_error",
message: e.reason?.message,
stack: e.reason?.stack,
});
});
// ------------------------------
// 资源加载失败(图片/CSS/JS)
// ------------------------------
window.addEventListener(
"error",
(e) => {
if (e.target?.src || e.target?.href) {
monitor.capture({
type: "resource_error",
tag: e.target.tagName,
url: e.target.src || e.target.href,
});
}
},
true
);
// ------------------------------
// 页面关闭/崩溃
// ------------------------------
window.addEventListener("beforeunload", () => {
monitor.capture({ type: "page_crash" });
});
// ------------------------------
// 白屏检测
// ------------------------------
setTimeout(() => {
const app = document.getElementById("app");
if (!app || app.innerHTML.length < 50) {
monitor.capture({ type: "blank_screen" });
}
}, 3000);
}
// ==============================================
// 给 Vue 绑定全局错误捕获
// ==============================================
bindVueErrorHandler(app) {
app.config.errorHandler = (err, instance, info) => {
this.capture({
type: "vue_error",
message: err.message,
stack: err.stack,
info,
});
};
}
}
// 单例模式,全局只一个实例
export const errorMonitor = new ErrorMonitor();
=========================================================本地查看日志错误===================================================
<template>
<div class="error-log-wrapper" style="padding:20px;max-width:1200px;margin:0 auto">
<h2>前端本地错误日志(IndexedDB)</h2>
<div style="margin:10px 0">
<button @click="getLogs" style="padding:6px 12px;margin-right:8px">刷新日志</button>
<button @click="clearLogs" style="padding:6px 12px;margin-right:8px">清空本地日志</button>
<button @click="testThrowError" style="padding:6px 12px">测试抛错</button>
</div>
<div v-if="logList.length === 0" style="color:#999;margin:20px 0">暂无本地错误日志</div>
<!-- 日志列表 -->
<div v-for="(item,idx) in logList" :key="idx"
style="border:1px solid #eee;padding:12px;margin:8px 0;border-radius:6px;background:#fafafa">
<div><b>类型:</b>{{ item.type }}</div>
<div><b>时间:</b>{{ item.time }}</div>
<div><b>页面:</b>{{ item.url }}</div>
<div><b>信息:</b>{{ item.message || '无' }}</div>
<div style="margin-top:6px">
<b>堆栈:</b>
<pre style="white-space:pre-wrap;font-size:12px;color:#666">{{ item.stack || '无' }}</pre>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 引入你之前封装的全局监控实例
import { errorMonitor } from '@/utils/errorMonitor'
const logList = ref([])
// 读取本地IndexedDB错误日志
const getLogs = async () => {
logList.value = await errorMonitor.getLocalLogs()
}
// 清空本地日志
const clearLogs = async () => {
await errorMonitor.clearLocalLogs()
getLogs()
}
// 测试:主动抛一个JS错误,看是否能捕获+入库
const testThrowError = () => {
throw new Error('测试主动抛出:前端监控demo错误')
}
onMounted(() => {
getLogs()
})
</script>