一、背景
最近加入了一個刻意練習(xí)小組谒主,自選了一個課題完域。
題目:《實現(xiàn)一個前端異常收集器》
目標:收集前端的各類錯誤,包括收集時間瘩将、容錯等吟税。
先介紹一下思路:
二、github源碼
安裝:
yarn add web-error-tracker
https://github.com/evilrescuer/web-error-tracker
測試項目:showcase
三姿现、常見前端異常類型
此處為示例片段代碼肠仪,具體請查看github源碼
1.JavaScript語法異常
如Uncaught ReferenceError: t is not defined
function testJavaScriptSyntaxError() {
t();
}
testJavaScriptSyntaxError();
2.加載圖片資源異常
加載圖片錯誤
function testImgError() {
const img = document.createElement('IMG');
img.src = './test.png';
document.body.append(img);
}
testImgError();
3.未捕獲的Promise錯誤
沒有捕獲 unhandledrejection錯誤
function testPromiseError() {
new Promise((resolve, reject) => {
t();
});
}
testPromiseError();
4.api返回錯誤
// Node.js啟動一個server,模擬接口返回500
if (ctx.req.url === '/test500') {
ctx.body = 'Internal Server Error';
ctx.status = 500;
}
// 測試
// 事先引入axios庫
// ...
function testCallApiError() {
axios.get('http://localhost:3000/test500');
}
testCallApiError();
5.跨域異常
// html
<button id="btn-cors-error">跨域異常</button>
<script src="http://localhost:3000/file.js" crossorigin></script>
// Node.js服務(wù)模擬file.js返回
if (ctx.req.url === '/file.js') {
ctx.set('Content-Type', 'text/javascript');
ctx.body = `
const btn = document.querySelector('#btn-cors-error');
btn.addEventListener('click', () => {
// b is not defined
var a = b;
});
`;
}
注:必須加上crossorigin
备典,否則捕獲的錯誤不夠詳細异旧,而是Script Error
6.動態(tài)創(chuàng)建的有錯誤的腳本
function testCreateAWrongScriptError() {
const script = document.createElement('script');
// testCreateAWrongScriptErrorValiable is not defined
script.innerHTML = `
var a = testCreateAWrongScriptErrorValiable;
`;
document.body.append(script);
}
testCreateAWrongScriptError()
7.iframe內(nèi)部異常
// html
<iframe src="./iframe.html" frameborder="0"></iframe>
// iframe
<script>
setTimeout(() => {
// b is not defined
var a = b;
}, 1000)
</script>
四、源碼解析
// 修改原生EventTarget對象
function modifyEventTarget (destWindow) {
// 跨域異常-crossOrigin
const originAddEventListener = destWindow.EventTarget.prototype.addEventListener;
destWindow.EventTarget.prototype.addEventListener = function (type, listener, options) {
const wrappedListener = function (...args) {
try {
return listener.apply(this, args);
}
catch (err) {
throw err;
}
};
return originAddEventListener.call(destWindow, type, wrappedListener, options);
};
}
// 修改原生XMLHttpRequest
function hookAjax (proxy, destWindow = window) {
const realXhr = "RealXMLHttpRequest";
destWindow[realXhr] = destWindow[realXhr] || destWindow.XMLHttpRequest;
destWindow.XMLHttpRequest = function () {
const xhr = new destWindow[realXhr];
for (const attr in xhr) {
let type = "";
try {
type = typeof xhr[attr];
} catch (e) {
}
if (type === "function") {
this[attr] = hookFunction(attr);
} else {
Object.defineProperty(this, attr, {
get: getterFactory(attr),
set: setterFactory(attr),
enumerable: true
});
}
}
this.xhr = xhr;
};
function getterFactory(attr) {
return function () {
const v = this.hasOwnProperty(attr + "_") ? this[attr + "_"] : this.xhr[attr];
const attrGetterHook = (proxy[attr] || {})["getter"];
return attrGetterHook && attrGetterHook(v, this) || v
}
}
function setterFactory(attr) {
return function (v) {
const xhr = this.xhr;
const that = this;
const hook = proxy[attr];
if (typeof hook === "function") {
xhr[attr] = function () {
proxy[attr](that) || v.apply(xhr, arguments);
}
} else {
const attrSetterHook = (hook || {})["setter"];
v = attrSetterHook && attrSetterHook(v, that) || v
try {
xhr[attr] = v;
} catch (e) {
this[attr + "_"] = v;
}
}
}
}
function hookFunction(fun) {
return function () {
const args = [].slice.call(arguments);
if (proxy[fun] && proxy[fun].call(this, args, this.xhr)) {
return;
}
return this.xhr[fun].apply(this.xhr, args);
}
}
return destWindow[realXhr];
}
class ErrorTracker {
constructor() {
this.errorBox = [];
}
init() {
this.handleWindow(window);
}
getErrors() {
return this.errorBox;
}
handleWindow(destWindow) {
const _instance = this;
modifyEventTarget(destWindow);
// XHR錯誤(利用http status code判斷)
hookAjax({
onreadystatechange: xhr => {
if (xhr.readyState === 4) {
if (xhr.status >= 400 || xhr.status <= 599) {
console.log('xhr錯誤:', xhr);
const error = xhr.xhr;
_instance.errorBox.push(new FEError(`api response ${error.status}`, error.responseURL, null, null, error.responseText));
}
}
}
}, destWindow);
// 全局JS異常-window.onerror / 全局靜態(tài)資源異常-window.addEventListener
destWindow.addEventListener('error', event => {
event.preventDefault();
console.log('errorEvent錯誤:', event);
if (event instanceof destWindow.ErrorEvent) {
_instance.errorBox.push(new FEError(event.message, event.filename, event.lineno, event.colno, event.error));
}
else if (event instanceof destWindow.Event) {
if (event.target instanceof HTMLImageElement) {
_instance.errorBox.push(new FEError('load img error', event.target.src, null, null, null));
}
}
return true;
}, true);
// 沒有catch等promise異常-unhandledrejection
destWindow.addEventListener('unhandledrejection', event => {
event.preventDefault();
console.log('unhandledrejection錯誤:', event);
_instance.errorBox.push(new FEError('unhandled rejection', null, null, null, event.reason));
return true;
});
// 頁面嵌套錯誤(iframe錯誤等等提佣、單點登錄)(注意:不能捕獲iframe加載時的錯誤)
destWindow.addEventListener('load', () => {
const iframes = destWindow.document.querySelectorAll('iframe');
iframes.forEach(iframe => {
_instance.handleWindow(iframe.contentWindow);
});
});
}
}
class FEError {
constructor(message, source, lineno, colno, stack) {
this.message = message;
this.source = source;
this.lineno = lineno;
this.colno = colno;
this.stack = stack;
this.time = new Date();
}
}
window.errorTracker = new ErrorTracker();
window.errorTracker.init();
五吮蛹、總結(jié)
總體思路:通過監(jiān)聽error事件、修改原生xhr拌屏、修改EventTarget潮针。
如有iframe,需要在iframe的window環(huán)境下倚喂,遞歸以上處理每篷。
問題 | 方案 | 備注 |
---|---|---|
1.JavaScript語法異常 | window.addEventListener監(jiān)聽error事件 | 回調(diào)中event對象為ErrorEvent的實例 |
2.加載圖片資源異常 | window.addEventListener監(jiān)聽error事件 | 回調(diào)中event對象為Event的實例 |
3.未捕獲的Promise錯誤 | window.addEventListener監(jiān)聽unhandledrejection事件 | 無 |
4.api返回錯誤 | 修改原生XMLHttpRequest | 無 |
5.跨域異常 | 修改原生EventTarget對象、并在資源link上加上crossorigin屬性 | 無 |
6.動態(tài)創(chuàng)建的有錯誤的腳本 | 無需特殊處理 | 無 |
7.iframe內(nèi)部異常 | 先獲取所有iframe的Dom節(jié)點端圈,再遞歸處理 | 無 |
(完)