你是否一直對前端錯誤監控系統的底層原理充滿好奇?
想知道那些「黑科技」是如何攔截報錯、上報數據的嗎?
與其只做工具的使用者,不如深入底層,探尋其背後的實現機制。
本文將從原理角度切入,手把手帶你設計並實現一個輕量級、功能完備的前端錯誤監控 SDK。
通過手寫這個 SDK,你不僅能獲得一個可用的監控工具,更能深入掌握以下核心知識點:
onerror、unhandledrejection 等 API 的工作細節。XMLHttpRequest、fetch)來實現無感監控。Navigator.sendBeacon 的使用場景,確保在頁面卸載時也能穩定上報數據。別被「監控系統」這四個字嚇到了。拆解下來,核心邏輯就三步:監聽 -> 收集 -> 上報。
在開始編碼之前,我們先梳理一下 SDK 的整體架構。我們需要監控 JS 運行時錯誤、網路請求錯誤 以及 資源加載錯誤,並將這些數據統一格式化後上報到服務端。

為了保持代碼的模組化和可維護性,我們採用以下目錄結構:
error-monitor/
├── dist/ # 打包產物
├── src/ # 原碼目錄
│ ├── index.ts # 入口文件
│ ├── errorHandler.ts # JS 錯誤捕獲
│ ├── networkMonitor.ts # 網路請求監控
│ ├── resourceMonitor.ts # 資源加載監控
│ ├── sender.ts # 上報邏輯
│ └── utils.ts # 工具函數
├── test/ # 測試靶場
│ ├── server.js # 本地測試服務
│ └── index.html # 錯誤觸發頁面
├── package.json # 專案配置
├── rollup.config.js # Rollup 打包配置
├── tsconfig.json # TypeScript 配置
└── README.md
錯誤監控原碼在 src 目錄下,最終使用 rollup 對代碼進行打包,dist 是打包產物;test 目錄下是對打包產物的測試:能否攔截 JS/請求/資源錯誤,能否穩妥上報。現在就從 0 到 1 開幹,做個 mini 版的錯誤監控 SDK。
🚀 瀏覽專案的完整代碼及示例可以點擊這裡 error-monitor,如果對您有幫助歡迎 Star。
index.ts)SDK 的入口主要負責接收配置(如上報地址、專案名稱)並啟動各個監控模組。
// src/index.ts
import { monitorJavaScriptErrors } from './errorHandler';
import { monitorNetworkErrors } from './networkMonitor';
import { monitorResourceErrors } from './resourceMonitor';
interface ErrorMonitorConfig {
reportUrl: string; // 上報接口地址
projectName: string; // 專案識別
environment: string; // 環境 (dev/prod)
}
export const initErrorMonitor = (config: ErrorMonitorConfig) => {
const { reportUrl, projectName, environment } = config;
// 啟動三大監控模組
monitorJavaScriptErrors(reportUrl, projectName, environment);
monitorNetworkErrors(reportUrl, projectName, environment);
monitorResourceErrors(reportUrl, projectName, environment);
};
errorHandler.ts)這是錯誤監控的「基本盤」。瀏覽器中的 JavaScript 錯誤主要分為兩類,必須「兵分兩路」進行攔截:
同步運行時錯誤,這是最經典的錯誤類型(比如 undefined is not a function)。
我們使用老牌的 window.onerror 進行捕獲。它雖然古老,但依然是獲取錯誤行號、列號和堆棧信息最直接、相容性最好的方式。
隨著 async/await 的普及,未被 catch 的 Promise 錯誤越來越常見。這部分錯誤 不會 觸發 onerror,需要通過監聽 unhandledrejection 事件來捕獲。
一句話總結
關鍵原則:不破壞原有邏輯
監控 SDK 的定位永遠是「旁聽者」,絕不能「反客為主」。它不能改變頁面原本的錯誤處理結果、不該屏蔽控制台的報錯輸出、更不該影響其他第三方庫的行為。
所以在實現時,要遵守以下三點:
// src/errorHandler.ts
import { sendErrorData } from './sender';
export const monitorJavaScriptErrors = (
reportUrl: string,
projectName: string,
environment: string
) => {
// 1. 捕獲 JS 運行時錯誤
const originalOnError = window.onerror;
window.onerror = (message, source, lineno, colno, error) => {
const errorInfo = {
type: 'JavaScript Error',
message,
source,
lineno,
colno,
stack: error ? error.stack : null,
projectName,
environment,
timestamp: new Date().toISOString(),
};
sendErrorData(errorInfo, reportUrl);
// 關鍵點:如果原來有 onerror 處理函數,繼續執行它,避免覆蓋用戶邏輯
// 這樣做是為了不破壞宿主環境(例如用戶自己寫的或其他 SDK)已有的錯誤處理邏輯
if (originalOnError) {
return originalOnError(message, source, lineno, colno, error);
}
};
// 2. 捕獲未處理的 Promise Rejection
const originalOnUnhandledRejection = window.onunhandledrejection;
window.onunhandledrejection = (event) => {
const errorInfo = {
type: 'Unhandled Promise Rejection',
message: event.reason?.message || event.reason,
stack: event.reason?.stack,
projectName,
environment,
timestamp: new Date().toISOString(),
};
sendErrorData(errorInfo, reportUrl);
// 關鍵點:執行原有的 Promise 錯誤處理邏輯
// 這樣做是為了不破壞宿主環境(例如用戶自己寫的或其他 SDK)已有的錯誤處理邏輯
if (originalOnUnhandledRejection) {
return originalOnUnhandledRejection.call(window, event);
}
};
};
networkMonitor.ts)接口監控是監控的難點,因為瀏覽器並沒有提供一個全局的 onNetworkError 事件。
解決方案:AOP(面向切面編程)重寫
簡單來說,就是把原生的方法「包」一層:在請求發出前/響應返回後,插入我們的監控代碼,然後再執行原有的邏輯。這樣業務代碼完全無感知,而我們卻能拿到所有的請求細節。
難點與細節:
// src/networkMonitor.ts
export const monitorNetworkErrors = (
reportUrl: string,
projectName: string,
environment: string
) => {
// 1. 劫持 XMLHttpRequest
const originalXhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (
method: string,
url: string | URL,
...args: any[]
) {
// 關鍵點:排除上報接口自身的請求,防止死循環
const urlStr = typeof url === 'string' ? url : String(url);
if (urlStr.includes(reportUrl)) {
return originalXhrOpen.apply(this, [method, url, ...args] as any);
}
// 監聽 error 事件
this.addEventListener('error', () => {
sendErrorData(
{
type: 'Network Error',
message: `Request Failed: ${method} ${url}`,
projectName,
environment,
},
reportUrl
);
});
return originalXhrOpen.apply(this, [method, url, ...args] as any);
};
// 2. 劫持 Fetch
const originalFetch = window.fetch;
window.fetch = async (input, init) => {
// 關鍵點:排除上報接口自身的請求,防止死循環
const urlStr = (input instanceof Request) ? input.url : String(input);
if (urlStr.includes(reportUrl)) {
return originalFetch(input, init);
}
try {
const response = await originalFetch(input, init);
if (!response.ok) {
sendErrorData(
{
type: 'Fetch Error',
message: `HTTP ${response.status}: ${response.statusText}`,
url: input instanceof Request ? input.url : input,
projectName,
environment,
},
reportUrl
);
}
return response;
} catch (error) {
// 網路故障等無法發出請求的情況
sendErrorData(
{
type: 'Fetch Error',
message: `Fetch Failed: ${input}`,
projectName,
environment,
},
reportUrl
);
throw error;
}
};
};
resourceMonitor.ts)這裡有一個常見的誤區:很多人認為 window.onerror 可以捕獲所有錯誤,但實際上它無法捕獲 資源加載錯誤 (如 img 、 script 、 link 的 404)。
error 事件是 不冒泡 的。 window.onerror 機制依賴於事件冒泡到頂層窗口,因此它對資源加載錯誤無能為力。addEventListener 的 捕獲階段 (將第三個參數設為 true)。我們需要專門編寫一個模組,通過 window.addEventListener('error', handler, true) 並在回調中通過 event.target 過濾出 IMG 、 SCRIPT 等標籤的錯誤。
// src/resourceMonitor.ts
export const monitorResourceErrors = (
reportUrl: string,
projectName: string,
environment: string
) => {
// 注意:useCapture 設置為 true,在捕獲階段處理
window.addEventListener(
'error',
(event) => {
const target = event.target as HTMLElement;
// 過濾掉 window 自身的 error,只處理資源元素的 error
if (target && (target.tagName === 'IMG' || target.tagName === 'SCRIPT')) {
sendErrorData(
{
type: 'Resource Load Error',
message: `Failed to load ${target.tagName}: ${
target.getAttribute('src') || target.getAttribute('href')
}`,
projectName,
environment,
},
reportUrl
);
}
},
true // 捕獲階段
);
};
sender.ts)收集到錯誤數據後,如何發給後端?這看似簡單,實則暗藏玄机。
痛點:頁面卸載時的「遺言」發不出去
用戶遇到 Bug 的第一反應往往是關閉頁面。如果我們使用普通的 fetch 或 XHR 上報:
救星:Navigator.sendBeacon
sendBeacon 是專門為此場景設計的 API。它有三大優勢:
因此,我們的上報策略是:優先 sendBeacon,不支持則降級為 fetch。
// src/sender.ts
export const sendErrorData = (errorData: Record<string, any>, url: string) => {
// 補充瀏覽器信息(UserAgent 等)
const dataToSend = {
...errorData,
userAgent: navigator.userAgent,
// 還可以添加更多環境信息,如螢幕解析度、當前 URL 等
};
// 優先使用 sendBeacon (異步,不阻塞,頁面卸載時仍有效)
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(dataToSend)], {
type: 'application/json',
});
navigator.sendBeacon(url, blob);
} else {
// 降級使用 fetch
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dataToSend),
}).catch(console.error);
}
};
💡 知識擴展:經典的 1x1 GIF 打點
你可能聽說過用 new Image().src = 'http://api.com/report?data=...' 這種方式上報。這在統計 PV/UV 時非常流行,因為它相容性極好且天然跨域。
但在錯誤監控場景下,通常不推薦作為主力方案。
核心原因正是數據量:
所以,對於體積較大的錯誤數據,走 POST 通道的 sendBeacon 或 fetch 是更穩妥的選擇。
如果線上出現大規模故障,成千上萬的用戶同時上報錯誤,可能會瞬間把監控伺服器打掛(DDoS 既視感)。
這時候我們需要引入兩個機制:
採樣 (Sampling):
if (Math.random() > 0.2) return;緩衝 (Buffering):
既然是 SDK,最好的分發方式當然是發布到 NPM。這樣其他專案只需要一行命令就能接入你的前端錯誤監控系統。
這裡我們選擇 Rollup 對代碼進行打包,因為它比 Webpack 更適合打包庫(Library),生成的代碼更簡潔。
package.json)package.json 不僅僅是依賴管理,它還定義了你的包如何被外部使用。配置不當會導致用戶引入報錯或無法獲得代碼提示。
{
"name": "error-monitor-sdk",
"version": "1.0.0",
"description": "A lightweight front-end error monitoring SDK",
"main": "dist/index.cjs.js", // CommonJS 入口
"module": "dist/index.esm.js", // ESM 入口
"browser": "dist/index.umd.js", // UMD 入口
"type": "module",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w"
},
"keywords": ["error-monitor", "frontend", "sdk"],
"license": "MIT",
"files": ["dist"], // 發布時僅包含 dist 目錄
"devDependencies": {
"rollup": "^4.9.0",
"@rollup/plugin-typescript": "^11.1.0",
"@rollup/plugin-terser": "^0.4.0", // 用於壓縮代碼
"typescript": "^5.3.0",
"tslib": "^2.6.0"
}
}
💡 關鍵字段解讀:
name: 包的「身份證號」。在 NPM 全球範圍內必須唯一,發布前記得先去搜一下有沒有重名。main: CommonJS 入口。給 Node.js 環境或老舊構建工具(如 Webpack 4)使用的。module: ESM 入口。給現代構建工具(Vite, Webpack 5)使用的。支持 Tree Shaking(搖樹優化),能減小體積。browser: UMD 入口。給瀏覽器直接通過 <script> 標籤引入使用的(如 CDN)。files: 發布白名單。指定 npm publish 時只上傳哪些文件(這裡我們只傳編譯後的 dist 目錄)。原碼、測試代碼等不需要發上去,以減小包體積。tsconfig.json)我們需要配置 TypeScript 如何編譯代碼,並生成類型聲明文件(.d.ts),這對使用 TS 的用戶非常友好。
{
"compilerOptions": {
"target": "es5", // 編譯成 ES5,兼容舊瀏覽器
"module": "esnext", // 保留 ES 模塊語法,交給 Rollup 處理
"declaration": true, // 生成 .d.ts 類型文件 (關鍵!)
"declarationDir": "./dist", // 類型文件輸出目錄
"strict": true, // 開啟嚴格模式,代碼更健壯
"moduleResolution": "node" // 按 Node 方式解析模塊
},
"include": ["src/**/*"] // 編譯 src 下的所有文件
}
rollup.config.js)為了兼容各種使用場景,我們配置 Rollup 輸出三種格式:
.esm.js): 給現代構建工具(Vite, Webpack)使用,支持 Tree Shaking。.cjs.js): 給 Node.js 或舊版工具使用。.umd.js): 可以直接在瀏覽器通過 <script> 標籤引入,會掛載全局變量。import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
export default {
input: 'src/index.ts', // 入口文件
output: [
{
file: 'dist/index.cjs.js',
format: 'cjs',
sourcemap: true,
},
{
file: 'dist/index.esm.js',
format: 'es',
sourcemap: true,
},
{
file: 'dist/index.umd.js',
format: 'umd',
name: 'ErrorMonitor', // <script> 引入時的全局變量名
sourcemap: true,
plugins: [terser()], // UMD 格式進行壓縮體積
},
],
plugins: [
typescript({
tsconfig: './tsconfig.json',
declaration: true,
declarationDir: 'dist',
}),
],
};
package.json 裡的 name,確保沒有被佔用。如果不幸重名,改個獨特的名字,比如 error-monitor-sdk-vip。打開終端(Terminal),在專案根目錄下操作:
第一步:登錄 NPM
npm login
Logged in as <your-username>.npm config set registry https://registry.npmjs.org/第二步:打包代碼
確保 dist 目錄是最新的,不要發布空代碼。
npm run build
第三步:正式發布
npm publish --access public
--access public 參數用於確保發布的包是公開的(特別是當包名帶 @ 前綴時)。+ [email protected] 字樣,恭喜你,發布成功!
現在,全世界的開發者都可以通過 npm install error-monitor-sdk 來使用你的作品了!
SDK 發布後,支持多種引入方式,適配各種開發場景。
適用於現代前端專案(Vite, Webpack, Rollup 等)。
# 請將 error-monitor-sdk 替換為你實際發布的包名
npm install error-monitor-sdk
在你的業務代碼入口(如 main.ts 或 app.js)引入並初始化:
// 請將 error-monitor-sdk 替換為你實際發布的包名
import { initErrorMonitor } from 'error-monitor-sdk';
initErrorMonitor({
reportUrl: 'http://localhost:3000/error-report',
projectName: 'MyAwesomeProject',
environment: 'production',
});
適用於 Node.js 環境或舊版打包工具。
# 請將 error-monitor-sdk 替換為你實際發布的包名
npm install error-monitor-sdk
// 請將 error-monitor-sdk 替換為你實際發布的包名
const { initErrorMonitor } = require('error-monitor-sdk');
initErrorMonitor({
reportUrl: 'http://localhost:3000/error-report',
projectName: 'MyAwesomeProject',
environment: 'production',
});
適用於不使用構建工具的傳統專案或簡單的 HTML 頁面。
<!-- 請將 error-monitor-sdk 替換為你實際發布的包名,x.x.x 替換為具體版本號 -->
<script src="https://unpkg.com/error-monitor-sdk/dist/index.umd.js"></script>
<script>
// UMD 版本會將 SDK 掛載到 window.ErrorMonitor
ErrorMonitor.initErrorMonitor({
reportUrl: 'http://localhost:3000/error-report',
projectName: 'MyAwesomeProject',
environment: 'production',
});
</script>
到這裡,我們這個「麻雀雖小,五臟俱全」的錯誤監控 SDK 就算是跑起來了。
回頭看看,幾百行代碼沒白寫,實打實搞定了三件事:
Navigator.sendBeacon,哪怕用戶秒關頁面,最後那條「遺言」也能顽強地發給伺服器。不過說實話,這離真正的「企業級」監控還有點距離。
想在生產環境(特別是高流量業務)扛大旗,還得把下面這些坑填了:
a.b is not a function 只有哭的份。貪多嚼不爛,這次我們先聚焦在最核心的「錯誤監控」閉環。
至於上面那些進階玩法,我們下篇文章接著聊,帶你一步步把這個系統打磨得更完美。
造輪子不是為了重複造,而是為了親手拆開看看裡面的齒輪是怎麼轉的,這才是學習的本質。
希望這篇文章能是你打造專屬監控系統的起點。Happy Coding!