一、背景
最近加入了一个刻意练习小组,自选了一个课题。
题目:《实现一个前端异常收集器》
目标:收集前端的各类错误,包括收集时间、容错等。
先介绍一下思路:
二、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服务模拟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.动态创建的有错误的脚本
function testCreateAWrongScriptError() {
const script = document.createElement('script');
// testCreateAWrongScriptErrorValiable is not defined
script.innerHTML = `
var a = testCreateAWrongScriptErrorValiable;
`;
document.body.append(script);
}
testCreateAWrongScriptError()
7.iframe内部异常
// 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 / 全局静态资源异常-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();
五、总结
总体思路:通过监听error事件、修改原生xhr、修改EventTarget。
如有iframe,需要在iframe的window环境下,递归以上处理。
问题 | 方案 | 备注 |
---|---|---|
1.JavaScript语法异常 | window.addEventListener监听error事件 | 回调中event对象为ErrorEvent的实例 |
2.加载图片资源异常 | window.addEventListener监听error事件 | 回调中event对象为Event的实例 |
3.未捕获的Promise错误 | window.addEventListener监听unhandledrejection事件 | 无 |
4.api返回错误 | 修改原生XMLHttpRequest | 无 |
5.跨域异常 | 修改原生EventTarget对象、并在资源link上加上crossorigin属性 | 无 |
6.动态创建的有错误的脚本 | 无需特殊处理 | 无 |
7.iframe内部异常 | 先获取所有iframe的Dom节点,再递归处理 | 无 |
(完)