在复杂的网络环境和浏览器环境下,自测、QA测试以及 Code Review 都是不够的,如果对页面稳定性和准确性要求较高,就必须有一套完善的代码异常监控体系,本文从前端代码异常监控的方法和问题着手,尽量全面地阐述错误日志收集各个阶段中可能遇到的阻碍和处理方案。
☞ 收集日志的方法
平时收集日志的手段,可以归类为两个方面,一个是逻辑中的错误判断,为主动判断;一个是利用语言给我们提供的捷径,暴力式获取错误信息,如 try..catch 和 window.onerror。
1. 主动判断
我们在一些运算之后,得到一个期望的结果,然而结果不是我们想要的- // test.js
- function calc(){
- // code...
- return val;
- }
- if(calc() !== "someVal"){
- Reporter.send({
- position: "test.js::<Function>calc"
- msg: "calc error"
- });
- }
复制代码 这种属于逻辑错误/状态错误的反馈,在接口 status 判断中用的比较多。
2. try..catch 捕获
判断一个代码段中存在的错误:- try {
- init();
- // code...
- } catch(e){
- Reporter.send(format(e));
- }
复制代码 以 init 为程序的入口,代码中所有同步执行出现的错误都会被捕获,这种方式也可以很好的避免程序刚跑起来就挂。
3. window.onerror
捕获全局错误:- window.onerror = function() {
- var errInfo = format(arguments);
- Reporter.send(errInfo);
- return true;
- };
复制代码 在上面的函数中返回 return true,错误便不会暴露到控制台中。下面是它的参数信息:- /**
- * @param {String} errorMessage 错误信息
- * @param {String} scriptURI 出错的文件
- * @param {Long} lineNumber 出错代码的行号
- * @param {Long} columnNumber 出错代码的列号
- * @param {Object} errorObj 错误的详细信息,Anything
- */
- window.onerror = function(errorMessage, scriptURI, lineNumber,columnNumber,errorObj) {
- // code..
- }
复制代码 window.onerror 算是一种特别暴力的容错手段,try..catch 也是如此,他们底层的实现就是利用 C/C++ 中的 goto 语句实现,一旦发现错误,不管目前的堆栈有多深,不管代码运行到了何处,直接跑到顶层或者 try..catch 捕获的那一层,这种一脚踢开错误的处理方式并不是很好。
☞ 收集日志存在的问题
收集日志的目的是为了及时发现问题,最好日志能够告诉我们,错误在哪里,更优秀的做法是,不仅告诉错误在哪里,还告诉我们,如何处理这个错误。终极目标是,发现错误,自动容错,这一步是最难的。
1. 无具体报错信息,Script error.
先看下面的例子,test.html- [/code]test.js
- [code]// http://barret/test.js
- function test(){
- ver a = 1;
- return a+1;
- }
- test();
复制代码 我们期望收集到的日志是下面这样具体的信息:
为了对资源进行更好的配置和管理,我们通常将静态资源放到异域上- [/code]而拿到的结果却是:
- [align=center]
[/align] - 翻开 Chromium 的 WebCore 源码,可以看到:
- [align=center]
[/align] - 跨域情况下,返回的结果是 Script error.。
- [code]// http://trac.webkit.org/browser/branches/chromium/1453/Source/WebCore/dom/ScriptExecutionContext.cpp#L333
- String message = errorMessage;
- int line = lineNumber;
- String sourceName = sourceURL;
- // 已经拿到了所有的错误信息,但如果发现是非同源情况,`sanitizeScriptError` 中复写错误信息
- sanitizeScriptError(message, line, sourceName, cachedScript);
复制代码 旧版 的 WebCore 中只判断了 securityOrigin()->canRequest(targetURL),新版中还多了一个 cachedScript 的判断,可以看出浏览器对这方面的限制越来越严格。
在本地测试了下:
可见在 file:// 协议下,securityOrigin()->canRequest(targetURL) 也是 false。
☞ 为何Script error.?
简单报错: Script error,目的是避免数据泄露到不安全的域中,一个简单的例子:- [/code]上面我们并没有引入一个 js 文件,而是一个 html,这个 html 是银行的登录页面,如果你已经登录了 bank.com,那 login 页面就会自动跳转到 Welcome xxx...,如果未登录则跳转到 Please Login...,那么 JS 报错也会是 Welcome xxx... is not defined,Please Login... is not defined,通过这些信息可以判断一个用户是否登录他的银行帐号,给 hacker 提供了十分便利的判断渠道,这是相当不安全的。
- [b]☞ crossOrigin参数跳过跨域限制[/b]
- image 和 script 标签都有 crossorigin 参数,它的作用就是告诉浏览器,我要加载一个外域的资源,并且我信任这个资源。
- [code]
复制代码 然而,却报错了:
这是意料之中的错误,跨域资源共享策略要求,服务器也设置 Access-Control-Allow-Origin 的响应头:- header('Access-Control-Allow-Origin: *');
复制代码 回头看看我们 CDN 的资源,
Javascript/CSS/Image/Font/SWF 等这些静态资源其实都已经早早地加上了 CORS 响应头。
2. 压缩代码无法定位到错误的具体位置
线上的代码几乎都是经过打包压缩的,几十上百的文件压缩后打包成一个,而且只有一行。当我们收到 a is not defined 的时候,如果只在特定场景下才报错,我们根本无法定位到这个被压缩的 a 是个什么东西,那么此时的错误日志就是无效的。
第一个想到的办法是利用 sourceMap,利用它可以定位到压缩代码某一点在未压缩代码的具体位置。下面是 sourceMap 引入的格式,在代码的最后一行加入:- //# sourceMappingURL=index.js.map
复制代码 以前使用的是 ‘//@’ 作为开头,现在使用 ‘//#’,然而对于错误上报,这玩意儿没啥用。JS 不能拿到他真实的行数,只能通过 Chrome DevTools 这样的工具辅助定位,而且并不是每个线上资源都会添加 sourceMap 文件。sourceMap 的用途目前还只能体现在开发阶段。
当然,如果理解了 sourceMap 的 VLQ编码和位置对应关系,也可以将拿到的日志进行二次解析,映射到真实路径位置,这个成本比较高,貌似暂时也没人尝试过。
那么,有什么办法,可以定位错误的具体位置,或者说有什么办法可以缩小我们定位问题的难度呢?
可以这样考虑:打包的时候,在每两个合并的文件之间加上 1000 个空行,最后上线的文件就会变成- (function(){var longCode.....})(); // file 1
- // 1000 个空行
- (function(){var longCode.....})(); // file 2
- // 1000 个空行
- (function(){var longCode.....})(); // file 3
- // 1000 个空行
- (function(){var longCode.....})(); // file 4
- var _fileConfig = ['file 1', 'file 2', 'file 3', 'file 4']
复制代码 如果报错在第 3001 行,- window.onerror = function(msg, url, line, col, error){
- // line = 3001
- var lineNum = line;
- console.log("错误位置:" + _fileConfig[parseInt(lineNum / 1000) - 1]);
- // -> "错误位置:file 3"
- };
复制代码 可以计算出,错误出现在第三个文件中,范围就缩小了很多。
3. error 事件的注册
多次注册 error 事件,不会重复执行多个回调:- var fn = window.onerror = function() {
- console.log(arguments);
- };
- window.addEventListener("error", fn);
- window.addEventListener("error", fn);
复制代码 触发错误之后,上面代码的结果为:
window.onerror 和 addEventListener 都执行了,并只执行了一次。
4. 收集日志的量
没有必要将所有的错误信息全部送到 Log 中,这个量太大了。如果网页 PV 有 1kw,那么一个必现错误发送的 log 信息将有 1kw 条,大约一个 G 的日志。我们可以给 Reporter 函数添加一个采样率:
[code]function needReport (sampling){ // sampling: 0 - 1 return Math.random() |