前言:一套完整的异常监控体系不是简单的几个 try...catch,而是由全方位捕获、链路追踪、资源还原、以及自动化告警构成的工程化矩阵。
为了让这篇文章更具实操价值,我将针对你要求的三个核心结构进行深度扩充,加入硬核的代码细节和底层原理解析。
作为开发者,我们最怕听到用户说:"你的页面白屏了",而你打开电脑却发现"在我这儿是好的"。一套成熟的异常捕获机制,就是你在用户手机里的"黑匣子"。
1. 捕获维度:别漏掉任何一个角落
前端的异常主要分为三大类,每一类都有对应的"捕捉网"。一个合格的监控 SDK 必须像雷达一样覆盖所有角落。
① 同步与异步运行错误
这是最常见的逻辑错误(如 undefined.map())。
-
常规武器:
try...catch它能捕获同步代码错误。但在异步逻辑中,如果不用
async/await,它会失效。JavaScript
javascript// ❌ 错误示范:无法捕获异步错误 try { setTimeout(() => { throw new Error('异步崩了') }, 0); } catch (e) { console.log('抓不到我'); } -
全局补丁:
window.onerror它是最后的防线。它可以收集到大部分未被捕获的运行时错误,包括堆栈、行列号。
JavaScript
phpwindow.onerror = function(message, source, lineno, colno, error) { reportError({ type: 'javascript', msg: message, url: source, line: lineno, col: colno, stack: error?.stack }); return true; // 阻止错误继续抛出到控制台 };
② Promise 叛逆:未处理的 Rejection
异步编程中,如果 Promise 被 reject 了但没有 .catch(),普通的监听函数是抓不到它的。这在 async/await 普及的今天尤为致命。
-
必杀技:
unhandledrejection事件JavaScript
phpwindow.addEventListener('unhandledrejection', event => { const { reason } = event; reportError({ type: 'promise', message: reason?.message || reason, stack: reason?.stack || '', metadata: { href: window.location.href } }); event.preventDefault(); });
③ 资源加载失败
当 script 或 img 标签加载 404 时,并不会冒泡到 window.onerror。因为这类错误是静态资源自身的网络错误,不是 JS 引擎的执行错误。
-
对策:捕获阶段监听
JavaScript
iniwindow.addEventListener('error', event => { const target = event.target || event.srcElement; const isResource = target instanceof HTMLElement && (target.src || target.href); if (isResource) { reportError({ type: 'resource', tagName: target.localName, url: target.src || target.href }); } }, true); // 注意:必须在捕获阶段(true)监听
2. 核心挑战:如何跨越"混淆代码"的迷雾?
为了性能,我们线上的代码都是经过 Webpack 或 Vite 压缩混淆的。如果你收到的日志是 app.js:1:5432 报错,这种信息毫无价值。
实战方案:Source Map 离线映射
在开发环境构建时生成 .map 文件。千万不要把 .map 文件发到线上服务器 ,否则你的源码就泄露了。正确的做法是:将 .map 上传到内网的监控后台。
还原算法(Node.js 实现细节):
当监控系统收到报错的行列号时,利用 source-map 库还原真相:
JavaScript
javascript
const sourceMap = require('source-map');
const fs = require('fs');
async function locateSource(errorInfo) {
// 1. 读取打包时生成的 map 文件
const rawSourceMap = JSON.parse(fs.readFileSync('./dist/app.js.map', 'utf8'));
// 2. 创建消费者实例
const consumer = await new sourceMap.SourceMapConsumer(rawSourceMap);
// 3. 传入混淆后的行列,还原原始代码位置
const originalPos = consumer.originalPositionFor({
line: errorInfo.line,
column: errorInfo.column
});
// originalPos 包含: source(哪个文件), line(第几行), column, name(函数名)
console.log(`[还原成功]: 真凶在 ${originalPos.source} 第 ${originalPos.line} 行`);
consumer.destroy();
}
3. 进阶实战:异常捕获的"三层漏斗"模型
我们将异常处理比作**"三层漏斗"**,层层过滤,各司其职。
第一层:局部精细捕获 (Try-Catch) ------ "防弹衣"
场景: 核心业务逻辑,如下单按钮、支付接口、复杂的数据解析。
目的: 提供降级方案。
JavaScript
scss
async function handlePay() {
try {
await payOrder();
} catch (err) {
// 提示用户,并提供"重试"按钮,而不是白屏
showToast('支付暂不可用,请稍后重试');
reportError(err, { severity: 'critical' });
}
}
第二层:全局被动收集 (Global Listeners) ------ "黑匣子"
场景: 意料之外的 Bug。
目的: 收集环境信息与用户操作路径(Breadcrumbs) 。光看堆栈不够,我们需要知道用户报错前点过什么。
JavaScript
javascript
// 维护一个行为追踪栈
const breadcrumbs = [];
function pushBreadcrumb(info) {
breadcrumbs.push({ ...info, time: Date.now() });
if (breadcrumbs.length > 20) breadcrumbs.shift(); // 只保留最近20条
}
// 拦截全局点击
window.addEventListener('click', e => pushBreadcrumb({ type: 'click', target: e.target.tagName }));
第三层:框架级守护 (ErrorBoundary) ------ "防爆板"
场景: React 或 Vue 组件崩溃。
目的: 局部降级。如果侧边栏广告组件崩了,不能让整个网页都挂掉。
JavaScript
scala
// React 示例
class GlobalErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error, info) {
reportError(error, { componentStack: info.componentStack });
}
render() {
if (this.state.hasError) return <div className="fallback">组件加载失败</div>;
return this.props.children;
}
}
🛠️ "避坑"锦囊
-
别"吞掉"异常 :绝对不要
catch(e) {}却什么都不做。这会让 Bug 成为隐形杀手。 -
处理
Script Error:跨域 CDN 脚本报错必须配合crossorigin属性,否则你只能看到一行无用的Script Error。 -
性能优化:
sendBeacon:上报时,使用
navigator.sendBeacon()。它能确保在页面卸载时异步发出请求,且不阻塞主线程,不影响下一页面的加载速度。
💡 生产级 SDK 精简实战代码
这段代码可以作为你文章的最后压轴,展示一个工业级 SDK 的雏形:
JavaScript
typescript
/**
* 极简生产级监控 SDK
*/
const Monitor = {
breadcrumbs: [],
init(url) {
this.url = url;
this.bindEvents();
},
bindEvents() {
// 1. JS 运行错误
window.addEventListener('error', e => {
if (e.message) this.report('javascript', e.message, { stack: e.error?.stack });
else this.report('resource', e.target.src || e.target.href);
}, true);
// 2. Promise 错误
window.addEventListener('unhandledrejection', e => {
this.report('promise', e.reason?.message || e.reason);
});
// 3. 记录面包屑:点击
window.addEventListener('click', e => {
this.breadcrumbs.push({ type: 'click', t: Date.now() });
});
},
report(type, msg, extra = {}) {
const data = JSON.stringify({
type, msg,
breadcrumbs: this.breadcrumbs.slice(-5),
ua: navigator.userAgent,
...extra
});
// 使用 Beacon 保证可靠上报
if (navigator.sendBeacon) {
navigator.sendBeacon(this.url, data);
} else {
new Image().src = `${this.url}?data=${encodeURIComponent(data)}`;
}
}
};
Monitor.init('https://your-log-server.com/report');