捕获和分析JavaScript Error的方法

如何捕获和分析 JavaScript Error

前端工程师都知道 JavaScript 有基本的异常处理能力。我们可以 throw new Error(),浏览器也会在我们调用 API 出错时抛出异常。但估计绝大多数前端工程师都没考虑过收集这些异常信息。反正只要 JavaScript 出错后刷新不复现,那用户就可以通过刷新解决问题,浏览器不会崩溃,当没有发生过好了。这种假设在 Single Page App 流行之前还是成立的。现在的 Single Page App 运行一段时间后状态复杂无比,用户可能进行了若干输入操作才来到这里的,说刷新就刷新啊?之前的操作岂不要完全重做?所以我们还是有必要捕获和分析这些异常信息的,然后我们就可以修改代码避免影响用户体验。
捕获异常的方式

我们自己写的 throw new Error() 想要捕获当然可以捕获,因为我们很清楚 throw 写在哪里了。但是调用浏览器 API 时发生的异常就不一定那么容易捕获了,有些 API 在标准里就写着会抛出异常,有些 API 只有个别浏览器因为实现差异或者有缺陷而抛出异常。对于前者我们还能通过 try-catch 捕获,对于后者我们必须监听全局的异常然后捕获。

try-catch

如果有些浏览器 API 是已知会抛出异常的,那我们就需要把调用放到 try-catch 里面,避免因为出错而导致整个程序进入非法状态。例如说 window.localStorage 就是这样的一个 API,在写入数据超过容量限制后就会抛出异常,在 Safari 的隐私浏览模式下也会如此。


代码如下:

try {
localStorage.setItem('date', Date.now());
} catch (error) {
reportError(error);
}

另一个常见的 try-catch 适用场景是回调。因为回调函数的代码是我们不可控的,代码质量如何,会不会调用其它会抛出异常的 API,我们一概不知道。为了不要因为回调出错而导致调用回调后的其它代码无法执行,所以把调用回到放到 try-catch 里面是必须的。


代码如下:

listeners.forEach(function(listener) {
try {
listener();
} catch (error) {
reportError(error);
}
});

window.onerror

对于 try-catch 覆盖不到的地方,如果出现异常就只能通过 window.onerror 来捕获了。


代码如下:

window.onerror =
function(errorMessage, scriptURI, lineNumber) {
reportError({
message: errorMessage,
script: scriptURI,
line: lineNumber
});
}

注意不要耍小聪明使用 window.addEventListener 或 window.attachEvent 的形式去监听 window.onerror。很多浏览器只实现了 window.onerror,或者是只有 window.onerror 的实现是标准的。考虑到标准草案定义的也是 window.onerror,我们使用 window.onerror 就好了。
属性丢失

假设我们有一个 reportError 函数用来收集捕获到的异常,然后批量发送到服务器端存储以便查询分析,那么我们会想要收集哪些信息呢?比较有用的信息包括:错误类型(name)、错误消息(message)、脚本文件地址(script)、行号(line)、列号(column)、堆栈跟踪(stack)。如果一个异常是通过 try-catch 捕获到的,这些信息都在 Error 对象上(主流浏览器都支持),所以 reportError 也能收集到这些信息。但如果是通过 window.onerror 捕获到的,我们都知道这个事件函数只有 3 个参数,所以这 3 个参数以外的信息就丢失了。
序列化消息

如果 Error 对象是我们自己创建的话,那么 error.message 就是由我们控制的。基本上我们把什么放进 error.message 里面,window.onerror 的第一个参数(message)就会是什么。(浏览器其实会略作修改,例如加上 'Uncaught Error: ' 前缀。)因此我们可以把我们关注的属性序列化(例如 JSON.Stringify)后存放到 error.message 里面,然后在 window.onerror 读取出来反序列化就可以了。当然,这仅限于我们自己创建的 Error 对象。
第五个参数

浏览器厂商也知道大家在使用 window.onerror 时受到的限制,所以开始往 window.onerror 上面添加新的参数。考虑到只有行号没有列号好像不是很对称的样子,IE 首先把列号加上了,放在第四个参数。然而大家更关心的是能否拿到完整的堆栈,于是 Firefox 说不如把堆栈放在第五个参数吧。但 Chrome 说那还不如把整个 Error 对象放在第五个参数,大家想读取什么属性都可以了,包括自定义属性。结果由于 Chrome 动作比较快,在 Chrome 30 实现了新的 window.onerror 签名,导致标准草案也就跟着这样写了。


代码如下:

window.onerror = function(
errorMessage,
scriptURI,
lineNumber,
columnNumber,
error
) {
if (error) {
reportError(error);
} else {
reportError({
message: errorMessage,
script: scriptURI,
line: lineNumber,
column: columnNumber
});
}
}

属性正规化

我们之前讨论到的 Error 对象属性,其名称都是基于 Chrome 命名方式的,然而不同浏览器对 Error 对象属性的命名方式各不相同,例如脚本文件地址在 Chrome 叫做 script 但在 Firefox 叫做 filename。因此,我们还需要一个专门的函数来对 Error 对象进行正规化处理,也就是把不同的属性名称都映射到统一的属性名称上。具体做法可以参考这篇文章。尽管浏览器实现会更新,但人手维护一份这样的映射表并不会太难。

类似的是堆栈跟踪(stack)的格式。这个属性以纯文本的形式保存一份异常在发生时的堆栈信息,由于各个浏览器使用的文本格式不一样,所以也需要人手维护一份正则表达,用于从纯文本中提取每一帧的函数名(identifier)、文件(script)、行号(line)和列号(column)。
安全限制

如果你也遇到过消息为 'Script error.' 的错误,你会明白我在说什么的,这其实是浏览器针对不同源(origin)脚本文件的限制。这个安全限制的理由是这样的:假设一家网银在用户登录后返回的 HTML 跟匿名用户看到的 HTML 不一样,一个第三方网站就能把这家网银的 URI 放到 script.src 属性里面。HTML 当然不可能被当做 JS 解析啦,所以浏览器会抛出异常,而这个第三方网站就能通过解析异常的位置来判断用户是否有登录。为此浏览器对于不同源脚本文件抛出的异常一律进行过滤,过滤得只剩下 'Script error.' 这样一条不变的消息,其它属性统统消失。

对于有一定规模的网站来说,脚本文件放在 CDN 上,不同源是很正常的。现在就算是自己做个小网站,常见框架如 jQuery 和 Backbone 都能直接引用公共 CDN 上的版本,加速用户下载。所以这个安全限制确实造成了一些麻烦,导致我们从 Chrome 和 Firefox 收集到的异常信息都是无用的 'Script error.'。
CORS

想要绕过这个限制,只要保证脚本文件和页面本身同源即可。但把脚本文件放在不经 CDN 加速的服务器上,岂不降低用户下载速度?一个解决方案是,脚本文件继续放在 CDN 上,利用 XMLHttpRequest 通过 CORS 把内容下载回来,再创建 <script> 标签注入到页面当中。在页面当中内嵌的代码当然是同源的啦。

这说起来很简单,但实现起来却有很多细节问题。用一个简单的例子来说:


代码如下:

<script src="http://cdn.com/step1.js"></script>
<script>
(function step2() {})();
</script>
<script src="http://cdn.com/step3.js"></script>

我们都知道这个 step1、step2、step3 如果存在依赖关系的话,则必须严格按照这个顺序执行,否则就可能出错。浏览器可以并行请求 step1 和 step3 的文件,但在执行时顺序是保证的。如果我们自己通过 XMLHttpRequest 获取 step1 和 step3 的文件内容,我们就需要自行保证其顺序正确性。此外不要忘记了 step2,在 step1 以非阻塞形式下载的时候 step2 就可以被执行了,所以我们还必须人为干预 step2 让它等待 step1 完成后再执行。

如果我们已经有一整套工具来生成网站上不同页面的 <script> 标签的话,我们就需要调整一下这套工具让它对 <script> 标签做出改动:


代码如下:

<script>
scheduleRemoteScript('http://cdn.com/step1.js');
</script>
<script>
scheduleInlineScript(function code() {
(function step2() {})();
});
</script>
<script>
scheduleRemoteScript('http://cdn.com/step3.js');
</script>

我们需要实现 scheduleRemoteScript 和 scheduleInlineScript 这两个函数,并且保证它们在第一个引用外部脚本文件的 <script> 标签之前就被定义好,然后余下的 <script> 标签都会被改写成上面这种形式。注意原本立即执行的 step2 函数被放到了一个更大的 code 函数里面了。code 函数并不会被执行,它只是一个容器而已,这样使得原本 step2 的代码不需要转义就能保留下来,但又不会被立即执行。

接下来我们还需要实现一套完整的机制,保证这些由 scheduleRemoteScript 根据地址下载回来的文件内容和由 scheduleInlineScript 直接获取到的代码能够按照正确的顺序一个接一个地执行。详细的代码我就不在这里给出了,大家有兴趣可以自己去实现。
行号反查

通过 CORS 获取内容再把代码注入页面能够突破安全限制,但会引入一个新的问题,那就是行号冲突。原本通过 error.script 可以定位到唯一的脚本文件,再通过 error.line 可以定位到唯一的行号。现在由于都是页面内嵌的代码,多个 <script> 标签并不能通过 error.script 来区分,然而每一个 <script> 标签内部的行号都是从 1 算起的,结果就导致我们无法利用异常信息定位错误所在的源代码位置。

为了避免行号冲突,我们可以浪费一些行号,使得每一个 <script> 标签中有实际代码所使用的行号区间互相不重叠。举个例子来说,假设每个 <script> 标签中的实际代码都不超过 1000 行,那么我可以让第一个 <script> 标签中的代码占用第 1–1000 行,让第二个 <script> 标签中的代码占用第 1001–2000 行(前面插入 1000 行空行),第三个 <script> 标签种的代码占用第 2001–3000 行(前面插入 2000 行空行),以此类推。然后我们使用 data-* 属性记录这些信息,便于反查。


代码如下:

<script
data-src="http://cdn.com/step1.js"
data-line-start="1"
>
// code for step 1
</script>
<script data-line-start="1001">
// '\n' * 1000
// code for step 2
</script>
<script
data-src="http://cdn.com/step3.js"
data-line-start="2001"
>
// '\n' * 2000
// code for step 3
</script>

经过这样处理后,如果一个错误的 error.line 是 3005 的话,那意味着实际的 error.script 应该是 'http://cdn.com/step3.js',而实际的 error.line 则应该是 5。我们可以在之前提到的 reportError 函数里面完成这项行号反查工作。

当然,由于我们没办法保证每一个脚本文件只有 1000 行,也有可能有些脚本文件明显小于 1000 行,所以其实不需要固定分配 1000 行的区间给每一个 <script> 标签。我们可以根据实际脚本行数来分配区间,只要保证每一个 <script> 标签所使用的区间互不重叠就可以了。
crossorigin 属性

浏览器对于不同源的内容进行的安全限制当然不仅限于 <script> 标签。既然 XMLHttpRequest 可以通过 CORS 来突破这个限制,为什么直接通过标签引用的资源就不可以呢?这当然是可以的。

针对 <script> 标签引用不同源脚本文件的限制同样作用于 <img> 标签引用不同源图片文件。如果一个 <img> 标签是不同源的话,一旦在 <canvas> 绘图时用到了,该 <canvas> 将变为只写状态,保证网站不能通过 JavaScript 窃取未授权的不同源图片数据。后来 <img> 标签通过引入 crossorigin 属性解决了这个问题。如果使用 crossorigin="anonymous",则相当于匿名 CORS;如果使用 `crossorigin=“use-credentials”,则相当于带认证的 CORS。

既然 <img> 标签能这样做,为什么 <script> 标签就不能这样做?于是浏览器厂商就为 <script> 标签加入了同样的 crossorigin 属性用于解决上述安全限制问题。现在 Chrome 和 Firefox 对这个属性的支持是完全没有问题的。Safari 则会把 crossorigin="anonymous" 当做 crossorigin="use-credentials" 处理,结果是如果服务器只支持匿名 CORS 则 Safari 会当做认证失败。由于 CDN 服务器出于性能的考虑被设计为只能返回静态内容,不可能动态的根据请求返回认证 CORS 所需的 HTTP Header,Safari 相当于不能利用此特性来解决上述问题。
总结

JavaScript 异常处理看起来很简单,跟其它语言没什么区别,但真的要把异常都捕获了然后对属性做分析,其实还不是那么容易的事情。现在尽管有一些第三方服务提供捕获 JavaScript 异常的类 Google Analytics 服务,但如果要弄明白其中的细节和原理还是必须自己亲手做一次。

(0)

相关推荐

  • javascript Error 对象 错误处理

    Error对象 Property:   name: 错误名   number: 错误号   description: 描述   message: 错误信息,多同description  FF Only 属性   fileName: 错误发生的文件   stack: 错误发生时的调用堆栈 Constructor:   Error(){     this(0,"")} Error(description){     this(0,description)} Error(number,des

  • java进行error捕获和处理示例(java异常捕获)

    下面给个小例子,来验证一下error的捕获. 复制代码 代码如下: public class TestCatchError extends Error{ private static final long serialVersionUID = -351488225420878020L; public TestCatchError(){        super();    } public TestCatchError(String msg){        super(msg);    } p

  • c++ mk文件出错Jni调用产生java.lang.UnsatisfiedLinkError错误解决方法

    错误为: Android.mk文件 c++的调用方法为: 复制代码 代码如下: LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := TestNdk LOCAL_CPP_EXTENSION :=com_ndk_test_JniClient.cpp include $(BUILD_SHARED_LIBRARY) c中的调用方法: 复制代码 代码如下: LOCAL_SRC_FILES := com_ndk_test_Jn

  • javascript window.onerror事件学习新收获

    在捕获js错误时,我们通常使用try{}catch(e){}的方式,然后通过e.errorMessage等方式获取错误信息然后报告错误.但对于onerror事件可能很少问津,我们是否思考过如何报告错误所在的行号?如果想过这个是否也被这个问题所困扰过,是否认为在js里不可能捕获错误的行号呢?其实本人就遇到上述的几个问题,今日读某人写的一段js代码顿然发现了onerror事件,要说onerror这个时间也是n久以前就知道了,但对于其所带有的三个参数和其特殊性质却一直没有去了解过.经过自己的研究测试,

  • 解决Java中OutOfMemoryError的问题

    目前为止,我遇到使用Tomcat有三种情况:第一,使用Eclipse,在Eclipse中配置Tomcat.第二,直接在Tomcat中部署项目.第三将Tomcat安装为windows服务. 在这三种情况下,出现OutOfMemoryError.该怎么解决呢?这里我不得不提我被网上那些不负责任的文章害得很惨.各种设置内存的方法都试了,可就是不起作用.下面我说的这几种方法都是我亲自试验过的,没有问题. 第一种情况:  如图:我用红色框框出来的.其中Xms和Xmx是增加java虚拟机初始堆大小和最大堆大

  • JS高级调试技巧:捕获和分析 JavaScript Error详解

    反正只要 JavaScript 出错后刷新不复现,那用户就可以通过刷新解决问题,浏览器不会崩溃,当没有发生过好了.这种假设在 Single Page App 流行之前还是成立的.现在的 Single Page App 运行一段时间后状态复杂无比,用户可能进行了若干输入操作才来到这里的,说刷新就刷新啊?之前的操作岂不要完全重做?所以我们还是有必要捕获和分析这些异常信息的,然后我们就可以修改代码避免影响用户体验. 捕获异常的方式 我们自己写的 throw new Error() 想要捕获当然可以捕获

  • 解决Eclipse add external jars运行出现java.lang.NoClassDefFoundError的方法

    最近发现一个问题,有时候对一个Android项目反复的Add jar和remove jar,发现编译可以通过,但是运行起来当应用到外部jar的对象时,会抛出java.lang.NoClassDefFoundError异常.导致程序奔溃. 查看项目属性,发现java build path里比正常的项目少了Android Dependencies这一项: 通过和丢失之前的项目进行比对,发现差别在于工程根目录下的.classpath文件,<classpathentry exported="tru

  • JavaScript中的onerror事件概述及使用

    1.Window.onerror事件 onerror事件用来协助处理页面中的JavaScript错误.当页面上出现异常时,error事件便在window对象上触发.例如: 在这种情况下,浏览器会显示错误信息: . 如果不希望浏览器在默认对话框中显示出错消息,让onerror方法返回true即可. 此外,onerror方法还提供了三个参数用来确定错误确切的信息,分别是: 1)message:一个字符串,声明了出现的错误的信息(不同的浏览器中返回的消息内容略有差异). 2)url:一个字符串,声明了

  • 解析java中的error该不该捕获

    写java程序时,通常会被提示捕获异常,而又有一些异常是不需要强制捕获的,这是一个被说烂了的话题.像我一样从其他语言转过来的人确实有点迷惑,那我以我的理解重新解释一遍吧. 异常的基类是Exception,Exception子类有RuntimeException和其它Exception.这些其它的Exception叫做Checked异常,RuntimeException叫做Unchecked异常. 只看名字不太好理解,说的通俗点,java为了程序能够稳定的运行,就提示开发者捕获已知异常.编译器知道

  • 捕获和分析JavaScript Error的方法

    如何捕获和分析 JavaScript Error 前端工程师都知道 JavaScript 有基本的异常处理能力.我们可以 throw new Error(),浏览器也会在我们调用 API 出错时抛出异常.但估计绝大多数前端工程师都没考虑过收集这些异常信息.反正只要 JavaScript 出错后刷新不复现,那用户就可以通过刷新解决问题,浏览器不会崩溃,当没有发生过好了.这种假设在 Single Page App 流行之前还是成立的.现在的 Single Page App 运行一段时间后状态复杂无比

  • node.js express捕获全局异常的三种方法实例分析

    本文实例讲述了node.js express捕获全局异常的三种方法.分享给大家供大家参考,具体如下: 场景 express的路由里抛出异常后,全局中间件没办法捕获,需要在所有的路由函数里写try catch,这坑爹的逻辑让人每次都要多写n行代码 官方错误捕获中件间代码如下 app.use(function(err, req, res, next) { console.error(err.stack); res.status(500).send('Something broke!'); }); 测

  • JavaScript接口实现方法实例分析

    本文实例讲述了JavaScript接口实现方法.分享给大家供大家参考,具体如下: JavaScript中实现接口的方法有三种: 第一种,使用注释的方法实现接口 特点: (1)最简单,但是功能最弱 (2)利用 interface和 implement"文字" (3)把他们用注释的方式表现出来 具体实现如下: 1,用注释定义一个接口 /* * interface PersonDao(){ * function add(obj); * function remove(obj); * func

  • javascript数组去重方法分析

    本文实例讲述了javascript数组去重方法.分享给大家供大家参考,具体如下: 方法一. 思路:创建一个新的空数组,循环遍历旧数组,用indexOf()方法,可以取得元素在数组中的位置,如果值为-1表示不存在.那么新数组用indexOf去获取老数组的每一个元素,如果值为-1表示不存在,就把他push到新数组里,最后输出新数组即去重后的数组 var arr=[24,56,74,89,24,56,78,09,24]; var new_arr=[]; for(var i=0;i<arr.length

  • javascript匀速运动实现方法分析

    本文实例讲述了javascript匀速运动实现方法.分享给大家供大家参考,具体如下: 匀速运动步骤: 1. 清除定时器 2. 开启定时器 3. 运动是否完成:a.运动完成,清除定时器:b.运动未完成继续 匀速运动停止条件:距离足够近  Math.abs(当然距离-目标距离) < 最小运动距离 运行效果截图如下: div的匀速运动(简单运动)示例: <!doctype html> <html> <head> <meta charset="utf-8&

  • JavaScript操作Cookie方法实例分析

    本文实例讲述了JavaScript操作Cookie方法.分享给大家供大家参考.具体如下: // My methods for setting, reading and deleting cookies. // I have methods to check for the existence of cookie names or values, // to retrieve by name or value, and to create a formatted string of // all

  • JavaScript使用shift方法移除素组第一个元素实例分析

    本文实例讲述了JavaScript使用shift方法移除素组第一个元素的用法.分享给大家供大家参考.具体如下: JS中我们可以通过pop方法移除数组的最后一个元素,可以通过shift方法移除数组的第一个元素 <!DOCTYPE html> <html> <body> <p id="demo"> Click the button to remove the first element of the array.</p> <

  • javascript日期比较方法实例分析

    本文实例讲述了javascript日期比较方法.分享给大家供大家参考,具体如下: 今天为了实现这么一个功能--用户可以手动输入日期,但是日期只能输入在今天之前的,今天之后的时间就不能提交,即今天是15号,只能输入15号之前的日期,15号之后的日期不能输入 /** 将字符串为yyyyMMdd格式的字符串转换为日期类型 date就是日期字符串 */ function getDate(date){ var year = date.substr(0,4);//substr()是从0开始计算的 var m

  • jQuery实现动态加载(按需加载)javascript文件的方法分析

    本文实例讲述了jQuery实现动态加载(按需加载)javascript文件的方法.分享给大家供大家参考,具体如下: 为了提高网页程序的性能,在很多情况下javascript 是按需加载,而不是全部写在 <head>里面.利用jQuery可以很方便的实现按需加载js. $("#load").click(function(){ $.getScript('helloworld.js', function() { $("#content").html('js 加

随机推荐