本文大体围绕下面几点展开讨论:
javascript
对于 Javascript 而言,咱们面对的仅仅只是异常,异常的出现不会直接致使 JS 引擎崩溃,最多只会使当前执行的任务终止。html
<script>
error
console.log('永远不会执行');
</script>
<script>
console.log('我继续执行')
</script>
复制代码
在对脚本错误进行上报以前,咱们须要对异常进行处理,程序须要先感知到脚本错误的发生,而后再谈异常上报。前端
脚本错误通常分为两种:语法错误,运行时错误。java
下面就谈谈几种异常监控的处理方式:webpack
try-catch 异常处理ios
try-catch 在咱们的代码中常常见到,经过给代码块进行 try-catch 进行包装后,当代码块发生出错时 catch 将能捕捉到错误的信息,页面也将能够继续执行。git
可是 try-catch 处理异常的能力有限,只能捕获捉到运行时非异步错误,对于语法错误和异步错误就显得无能为力,捕捉不到。github
示例:运行时错误web
try {
error // 未定义变量
} catch(e) {
console.log('我知道错误了');
console.log(e);
}
复制代码
然而对于语法错误和异步错误就捕捉不到了。ajax
示例:语法错误
try {
var error = 'error'; // 大写分号
} catch(e) {
console.log('我感知不到错误');
console.log(e);
}
复制代码
通常语法错误在编辑器就会体现出来,常表现的错误信息为:
这样。可是这种错误会直接抛出异常,常使程序崩溃,通常在编码时候容易观察获得。
示例:异步错误
try {
setTimeout(() => {
error // 异步错误
})
} catch(e) {
console.log('我感知不到错误');
console.log(e);
}
复制代码
除非你在 setTimeout 函数中再套上一层 try-catch,不然就没法感知到其错误,但这样代码写起来比较啰嗦。
window.onerror 异常处理
window.onerror 捕获异常能力比 try-catch 稍微强点,不管是异步仍是非异步错误,onerror 都能捕获到运行时错误。
示例:运行时同步错误
/**
* @param {String} msg 错误信息
* @param {String} url 出错文件
* @param {Number} row 行号
* @param {Number} col 列号
* @param {Object} error 错误详细信息
*/
window.onerror = function (msg, url, row, col, error) {
console.log('我知道错误了');
console.log({
msg, url, row, col, error
})
return true;
};
error;
复制代码
示例:异步错误
window.onerror = function (msg, url, row, col, error) {
console.log('我知道异步错误了');
console.log({
msg, url, row, col, error
})
return true;
};
setTimeout(() => {
error;
});
复制代码
然而 window.onerror 对于语法错误仍是无能为力,因此咱们在写代码的时候要尽量避免语法错误的,不过通常这样的错误会使得整个页面崩溃,仍是比较容易可以察觉到的。
在实际的使用过程当中,onerror 主要是来捕获预料以外的错误,而 try-catch 则是用来在可预见状况下监控特定的错误,二者结合使用更加高效。
须要注意的是,window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,不然即便是知道异常的发生控制台仍是会显示
。
关于 window.onerror 还有两点须要值得注意
当咱们遇到
报 404 网络请求异常的时候,onerror 是没法帮助咱们捕获到异常的。
<script>
window.onerror = function (msg, url, row, col, error) {
console.log('我知道异步错误了');
console.log({
msg, url, row, col, error
})
return true;
};
</script>
<img src="./404.png">
复制代码
因为网络请求异常不会事件冒泡,所以必须在捕获阶段将其捕捉到才行,可是这种方式虽然能够捕捉到网络请求的异常,可是没法判断 HTTP 的状态是 404 仍是其余好比 500 等等,因此还须要配合服务端日志才进行排查分析才能够。
<script>
window.addEventListener('error', (msg, url, row, col, error) => {
console.log('我知道 404 错误了');
console.log(
msg, url, row, col, error
);
return true;
}, true);
</script>
<img src="./404.png" alt="">
复制代码
这点知识仍是须要知道,要否则用户访问网站,图片 CDN 没法服务,图片加载不出来而开发人员没有察觉就尴尬了。
Promise 错误
经过 Promise 能够帮助咱们解决异步回调地狱的问题,可是一旦 Promise 实例抛出异常而你没有用 catch 去捕获的话,onerror 或 try-catch 也无能为力,没法捕捉到错误。
window.addEventListener('error', (msg, url, row, col, error) => {
console.log('我感知不到 promise 错误');
console.log(
msg, url, row, col, error
);
}, true);
Promise.reject('promise error');
new Promise((resolve, reject) => {
reject('promise error');
});
new Promise((resolve) => {
resolve();
}).then(() => {
throw 'promise error'
});
复制代码
虽然在写 Promise 实例的时候养成最后写上 catch 函数是个好习惯,可是代码写多了就容易糊涂,忘记写 catch。
因此若是你的应用用到不少的 Promise 实例的话,特别是你在一些基于 promise 的异步库好比 axios 等必定要当心,由于你不知道何时这些异步请求会抛出异常而你并无处理它,因此你最好添加一个 Promise 全局异常捕获事件 unhandledrejection。
window.addEventListener("unhandledrejection", function(e){
e.preventDefault()
console.log('我知道 promise 的错误了');
console.log(e.reason);
return true;
});
Promise.reject('promise error');
new Promise((resolve, reject) => {
reject('promise error');
});
new Promise((resolve) => {
resolve();
}).then(() => {
throw 'promise error'
});
复制代码
固然,若是你的应用没有作 Promise 全局异常处理的话,那极可能就像某乎首页这样:
监控拿到报错信息以后,接下来就须要将捕捉到的错误信息发送到信息收集平台上,经常使用的发送形式主要有两种:
实例 - 动态建立 img 标签进行上报
function report(error) {
var reportUrl = 'http://xxxx/report';
new Image().src = reportUrl + 'error=' + error;
}
复制代码
监控上报常见问题
Script error 脚本错误是什么
由于咱们在线上的版本,常常作静态资源 CDN 化,这就会致使咱们常访问的页面跟脚本文件来自不一样的域名,这时候若是没有进行额外的配置,就会容易产生 Script error。
可经过
查看效果。
Script error 是浏览器在同源策略限制下产生的,浏览器处于对安全性上的考虑,当页面引用非同域名外部脚本文件时中抛出异常的话,此时本页面是没有权利知道这个报错信息的,取而代之的是输出 Script error 这样的信息。
这样作的目的是避免数据泄露到不安全的域中,举个简单的例子,
<script src="xxxx.com/login.html"></script>
复制代码
上面咱们并无引入一个 js 文件,而是一个 html,这个 html 是银行的登陆页面,若是你已经登陆了,那 login 页面就会自动跳转到
,若是未登陆则跳转到
,那么报错也会是
,经过这些信息能够判断一个用户是否登陆他的账号,给入侵者提供了十分便利的判断渠道,这是至关不安全的。
介绍完背景后,那么咱们应该去解决这个问题?
首先能够想到的方案确定是同源化策略,将 JS 文件内联到 html 或者放到同域下,虽然能简单有效地解决 script error 问题,可是这样没法利用好文件缓存和 CDN 的优点,不推荐使用。正确的方法应该是从根本上解决 script error 的错误。
跨源资源共享机制( CORS )
首先为页面上的 script 标签添加 crossOrigin 属性
// http://localhost:8080/index.html
<script>
window.onerror = function (msg, url, row, col, error) {
console.log('我知道错误了,也知道错误信息');
console.log({
msg, url, row, col, error
})
return true;
};
</script>
<script src="http://localhost:8081/test.js" crossorigin></script>
// http://localhost:8081/test.js
setTimeout(() => {
console.log(error);
});
复制代码
当你修改完前端代码后,你还须要额外给后端在响应头里加上
,这里我以 Koa 为例。
const Koa = require('koa');
const path = require('path');
const cors = require('koa-cors');
const app = new Koa();
app.use(cors());
app.use(require('koa-static')(path.resolve(__dirname, './public')));
app.listen(8081, () => {
console.log('koa app listening at 8081')
});
复制代码
读者可经过
详细的跨域知识我就不展开了,有兴趣能够看看我以前写的文章:跨域,你须要知道的全在这里
你觉得这样就完了吗?并无,下面就说一些 Script error 你不常碰见的点:
咱们都知道 JSONP 是用来跨域获取数据的,而且兼容性良好,在一些应用中仍然会使用到,因此你的项目中可能会用这样的代码:
// http://localhost:8080/index.html
window.onerror = function (msg, url, row, col, error) {
console.log('我知道错误了,但不知道错误信息');
console.log({
msg, url, row, col, error
})
return true;
};
function jsonpCallback(data) {
console.log(data);
}
const url = 'http://localhost:8081/data?callback=jsonpCallback';
const script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
复制代码
由于返回的信息会当作脚本文件来执行,一旦返回的脚本内容出错了,也是没法捕捉到错误的信息。
解决办法也不难,跟以前同样,在添加动态添加脚本的时候加上 crossOrigin,而且在后端配上相应的 CORS 字段便可.
const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = url;
document.body.appendChild(script);
复制代码
读者能够经过
查看效果
知道原理以后你可能会以为没什么,不就是给每一个动态生成的脚本添加 crossOrigin 字段嘛,可是在实际工程中,你多是面向不少库来编程,好比使用 jQuery,Seajs 或者 webpack 来异步加载脚本,许多库封装了异步加载脚本的能力,以 jQeury 为例你多是这样来触发异步脚本。
$.ajax({
url: 'http://localhost:8081/data',
dataType: 'jsonp',
success: (data) => {
console.log(data);
}
})
复制代码
假如这些库中没有提供 crossOrigin 的能力的话(jQuery jsonp 可能有,伪装你不知道),那你只能去修改人家写的源代码了,因此我这里提供一个思路,就是去劫持 document.createElement,从根源上去为每一个动态生成的脚本添加 crossOrigin 字段。
document.createElement = (function() {
const fn = document.createElement.bind(document);
return function(type) {
const result = fn(type);
if(type === 'script') {
result.crossOrigin = 'anonymous';
}
return result;
}
})();
window.onerror = function (msg, url, row, col, error) {
console.log('我知道错误了,也知道错误信息');
console.log({
msg, url, row, col, error
})
return true;
};
$.ajax({
url: 'http://localhost:8081/data',
dataType: 'jsonp',
success: (data) => {
console.log(data);
}
})
复制代码
效果也是同样的,读者能够经过
来查看效果:
这样重写 createElement 理论上没什么问题,可是入侵了本来的代码,不保证必定不会出错,在工程上仍是须要多尝试下看看再使用,可能存在兼容性上问题,若是你以为会出现什么问题的话也欢迎留言讨论下。
关于 Script error 的问题就写到这里,若是你理解了上面的内容,基本上绝大部分的 Script error 都能迎刃而解。
window.onerror 可否捕获 iframe 的错误
当你的页面有使用 iframe 的时候,你须要对你引入的 iframe 作异常监控的处理,不然一旦你引入的 iframe 页面出现了问题,你的主站显示不出来,而你却浑然不知。
首先须要强调,父窗口直接使用 window.onerror 是没法直接捕获,若是你想要捕获 iframe 的异常的话,有分好几种状况。
若是你的 iframe 页面和你的主站是同域名的话,直接给 iframe 添加 onerror 事件便可。
<iframe src="./iframe.html" frameborder="0"></iframe>
<script>
window.frames[0].onerror = function (msg, url, row, col, error) {
console.log('我知道 iframe 的错误了,也知道错误信息');
console.log({
msg, url, row, col, error
})
return true;
};
</script>
复制代码
读者能够经过
查看效果:
若是你嵌入的 iframe 页面和你的主站不是同个域名的,可是 iframe 内容不属于第三方,是你能够控制的,那么能够经过与 iframe 通讯的方式将异常信息抛给主站接收。与 iframe 通讯的方式有不少,经常使用的如:postMessage,hash 或者 name 字段跨域等等,这里就不展开了,感兴趣的话能够看:跨域,你须要知道的全在这里
若是是非同域且网站不受本身控制的话,除了经过控制台看到详细的错误信息外,没办法捕获,这是出于安全性的考虑,你引入了一个百度首页,人家页面报出的错误凭啥让你去监控呢,这会引出不少安全性的问题。
压缩代码如何定位到脚本异常位置
线上的代码几乎都通过了压缩处理,几十个文件打包成了一个并丑化代码,当咱们收到
的时候,咱们根本不知道这个变量 a 到底是什么含义,此时报错的错误日志显然是无效的。
第一想到的办法是利用 sourcemap 定位到错误代码的具体位置,详细内容能够参考:Sourcemap 定位脚本错误
另外也能够经过在打包的时候,在每一个合并的文件之间添加几行空格,并相应加上一些注释,这样在定位问题的时候很容易能够知道是哪一个文件报的错误,而后再经过一些关键词的搜索,能够快速地定位到问题的所在位置。
收集异常信息量太多,怎么办
若是你的网站访问量很大,假如网页的 PV 有 1kw,那么一个必然的错误发送的信息就有 1kw 条,咱们能够给网站设置一个采集率:
Reporter.send = function(data) {
// 只采集 30%
if(Math.random() < 0.3) {
send(data) // 上报错误信息
}
}
复制代码
这个采集率能够经过具体实际的状况来设定,方法多样化,可使用一个随机数,也能够具体根据用户的某些特征来进行断定。
上面差很少是我对前端代码监控的一些理解,提及来容易,可是一旦在工程化运用,不免须要考虑到兼容性等种种问题,读者能够经过本身的具体状况进行调整,前端代码异常监控对于咱们的网站的稳定性起着相当重要的做用。如若文中全部不对的地方,还望指正。