本文首发于公众号:符合预期的CoyPan

写在前面

在前端项目中,由于JavaScript本身是一个弱类型语言,加上浏览器环境的复杂性,网络问题等等,很容易发生错误。做好网页错误监控,不断优化代码,提高代码健壮性是一项很重要的工作。本文将从Error开始,讲到如何捕获页面中的异常。文章较长,细节较多,请耐心观看。

前端开发中的Error

JavaScript中的Error

JavaScript中,Error是一个构造函数,通过它创建一个错误对象。当运行时错误产生时,Error的实例对象会被抛出。构造一个Error的语法如下:

// message: 错误描述
// fileName: 可选。被创建的Error对象的fileName属性值。默认是调用Error构造器代码所在的文件的名字。
// lineNumber: 可选。被创建的Error对象的lineNumber属性值。默认是调用Error构造器代码所在的文件的行号。new Error([message[, fileName[, lineNumber]]])
复制代码
ECMAScript标准:

Error有两个标准属性:

  • Error.prototype.name :错误的名字
  • Error.prototype.message:错误的描述

例如,在chrome控制台中输入以下代码:

var a = new Error('错误测试');
console.log(a); // Error: 错误测试// at <anonymous>:1:9
console.log(a.name); // Error
console.log(a.message); // 错误测试
复制代码

Error只有一个标准方法:

  • Error.prototype.toString:返回表示一个表示错误的字符串。

接上面的代码:

a.toString();  // "Error: 错误测试"
复制代码
非标准的属性

各个浏览器厂商对于Error都有自己的实现。比如下面这些属性:

  1. Error.prototype.fileName:产生错误的文件名。
  2. Error.prototype.lineNumber:产生错误的行号。
  3. Error.prototype.columnNumber:产生错误的列号。
  4. Error.prototype.stack:堆栈信息。这个比较常用。

这些属性均不是标准属性,在生产环境中谨慎使用。不过现代浏览器差不多都支持了。

Error的种类

除了通用的Error构造函数外,JavaScript还有7个其他类型的错误构造函数。

  • InternalError: 创建一个代表Javascript引擎内部错误的异常抛出的实例。 如: "递归太多"。非ECMAScript标准。
  • RangeError: 数值变量或参数超出其有效范围。例子:var a = new Array(-1);
  • EvalError: 与eval()相关的错误。eval()本身没有正确执行。
  • ReferenceError: 引用错误。 例子:console.log(b);
  • SyntaxError: 语法错误。例子:var a = ;
  • TypeError: 变量或参数不属于有效范围。例子:[1,2].split('.')
  • URIError: 给 encodeURI或 decodeURl()传递的参数无效。例子:decodeURI('%2')

当JavaScript运行过程中出错时,会抛出上8种(上述7种加上通用错误类型)错误中的其中一种错误。错误类型可以通过error.name拿到。

你也可以基于Error构造自己的错误类型,这里就不展开了。

其他错误

上面介绍的都是JavaScript本身运行时会发生的错误。页面中还会有其他的异常,比如错误地操作了DOM。

DOMException

DOMException是W3C DOM核心对象,表示调用一个Web Api时发生的异常。什么是Web Api呢?最常见的就是DOM元素的一系列方法,其他还有XMLHttpRequest、Fetch等等等等,这里就不一一说明了。直接看下面一个操作DOM的例子:

var node = document.querySelector('#app');
var refnode = node.nextSibling;
var newnode = document.createElement('div');
node.insertBefore(newnode, refnode);// 报错:Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
复制代码

单从JS代码逻辑层面来看,没有问题。但是代码的操作不符合DOM的规则。

DOMException构造函数的语法如下:

// message: 可选,错误描述。
// name: 可选,错误名称。常量,具体值可以在这里找到:https://developer.mozilla.org/zh-CN/docs/Web/API/DOMExceptionnew DOMException([message[, name]]);
复制代码

DOMException有以下三个属性:

  1. DOMException.code:错误编号。
  2. DOMException.message:错误描述。
  3. DOMException.name:错误名称。

以上面那段错误代码为例,其抛出的DOMException各属性的值为:

code: 8
message: "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node."
name: "NotFoundError"
复制代码
Promise产生的异常

Promise中,如果Promisereject了,就会抛出异常:PromiseRejectionEvent。注意,下面两种情况都会导致Promisereject

  1. 业务代码本身调用了Promise.reject
  2. Promise中的代码出错。

PromiseRejectionEvent的构造函数目前在浏览器中大多都不兼容,这里就不说了。

PromiseRejectionEvent的属性有两个:

  1. PromiseRejectionEvent.promise:被rejectPromise
  2. PromiseRejectionEvent.reasonPromisereject的原因。会传递给rejectPromsiecatch中的参数。
加载资源出错

由于网络,安全等原因,网页加载资源失败,请求接口出错等,也是一种常见的错误。

关于错误的小结

一个网页在运行过程中,可能发生四种错误:

  1. JavaScript在运行过程,语言自身抛出的异常。
  2. JavaScript在运行过程中,调用Web Api时发生异常。
  3. Promise中的拒绝。
  4. 网页加载资源,调用接口时发生异常。

我认为,对于前两种错误,我们在平时的开发过程中,不用特别去区分,可以统一成:【代码出错】。

捕获错误

网页发生错误,开发者如何捕获这些错误呢 ? 常见的有以下方法。

try...catch...

try...catch…大家都不陌生了。一般用来在具体的代码逻辑中捕获错误。

try {throw new Error("oops");
}
catch (ex) {console.log("error", ex.message); // error oops
}
复制代码

try-block中的代码发生异常时,可以在catck-block中将异常接住,浏览器便不会抛出错误。但是,这种方式并不能捕获异步代码中的错误,如:

try {setTimeout(function(){throw new Error('lala');},0);
} catch(e) {console.log('error', e.message);
}
复制代码

这个时候,浏览器依然会抛出错误:Uncaught Error: lala

试想以下,如果我们将所有的代码合理的划分,然后都用try catch包起来,是不是就可以捕获到所有的错误了呢?可以通过编译工具来实现这个功能。不过,try catch是比较耗费性能的。

window.onerror

window.onerror = function(message, source, lineno, colno, error) { ... }
复制代码

函数参数:

  • message:错误信息(字符串)
  • source:发生错误的脚本URL(字符串)
  • lineno:发生错误的行号(数字)
  • colno:发生错误的列号(数字)
  • error:Error对象(对象)

注意,如果这个函数返回true,那么将会阻止执行浏览器默认的错误处理函数。

window.addEventListener('error')

window.addEventListener('error', function(event) { ... })
复制代码

我们调用Object.prototype.toString.call(event),返回的是[object ErrorEvent]。可以看到eventErrorEvent对象的实例。ErrorEvent是事件对象在脚本发生错误时产生,从Event继承而来。由于是事件,自然可以拿到target属性。ErrorEvent还包括了错误发生时的信息。

  • ErrorEvent.prototype.message: 字符串,包含了所发生错误的描述信息。
  • ErrorEvent.prototype.filename: 字符串,包含了发生错误的脚本文件的文件名。
  • ErrorEvent.prototype.lineno: 数字,包含了错误发生时所在的行号。
  • ErrorEvent.prototype.colno: 数字,包含了错误发生时所在的列号。
  • ErrorEvent.prototype.error: 发生错误时所抛出的 Error 对象。

注意,这里的ErrorEvent.prototype.error对应的Error对象,就是上文提到的Error, InternalErrorRangeErrorEvalErrorReferenceErrorSyntaxErrorTypeErrorURIErrorDOMException中的一种。

window.addEventListener('unhandledrejection')

window.addEventListener('unhandledrejection', function (event) { ... });
复制代码

在使用Promise的时候,如果没有声明catch代码块,Promise的异常会被抛出。只能通过这个方法或者window.onunhandledrejection才能捕获到该异常。

event就是上文提到的PromiseRejectionEvent。我们只需要关注其reason就行。

window.onerror 和 window.addEventListener('error')的区别

  1. 首先是事件监听器事件处理器的区别。监听器只能声明一次,后续的声明会覆盖之前的声明。而事件处理器则可以绑定多个回调函数。
  2. 资源( img 或 script )加载失败时,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror()处理函数。但这些error事件不会向上冒泡到window。不过,这些error事件能被window.addEventListener('error')捕获。也就是说,**面对资源加载失败的错误,只能用window.addEventListerner('error')window.onerror**无效。

关于错误捕获的小结

我认为,在开发的过程中,对于容易出错的地方,可以使用try{}catch(){}来进行错误的捕获,做好兜底处理,避免页面挂掉。而对于全局的错误捕获,在现代浏览器中,我倾向于只使用使用window.addEventListener('error')window.addEventListener('unhandledrejection')就行了。如果需要考虑兼容性,需要加上window.onerror,三者同时使用,window.addEventListener('error')专门用来捕获资源加载错误。

跨域脚本错误,Script Error

在进行错误捕获的过程中,很多时候并不能拿到完整的错误信息,得到的仅仅是一个"Script Error"

产生原因

由于12年前这篇文章里提到的安全问题:blog.jeremiahgrossman.com/2006/12/i-k…

当加载自不同域的脚本中发生语法错误时,为避免信息泄露,语法错误的细节将不会报告,而是使用简单的"Script error."代替

一般而言,页面的JS文件都是放在CDN的,和页面自身的URL产生了跨域问题,所以引起了"Script Error"

解决办法

服务端添加Access-Control-Allow-Origin,页面在script标签中配置 crossorigin="anonymous"。这样,便解决了因为跨域而带来的"Script Error"问题。

能绕过Script Error

上面介绍了"Script Error"的标准解决方案。但是,并不是所有的浏览器都支持crossorigin="anonymous",也不是所有的服务端都能及时配置Access-Control-Allow-Origin,这种情况下,还有什么方法能在全局捕获到所有的错误,并拿到详细信息呢?

劫持原生方法

看一个例子:

const nativeAddEventListener = EventTarget.prototype.addEventListener; // 先将原生方法保存起来。
EventTarget.prototype.addEventListener = function (type, func, options) { // 重写原生方法。const wrappedFunc = function (...args) { // 将回调函数包裹一层try catchtry { return func.apply(this, args);} catch (e) {const errorObj = {...error_name: e.name || '',error_msg: e.message || '',error_stack: e.stack || (e.error && e.error.stack),error_native: e,...};// 接下来可以将errorObj统一进行处理。}}return nativeAddEventListener.call(this, type, wrappedFunc, options); // 调用原生的方法,保证addEventListener正确执行
}
复制代码

我们劫持了原生的addEventListener代码,对addEventListener代码中的回调函数加了一层try{}catch(){},这样,回调函数中抛出的错误会被catch住,浏览器不会对try-catch 起来的异常进行跨域拦截,所以我们可以拿到详细的错误信息。通过上面的操作,我们可以拿到所有监听事件的回调函数中的错误啦。其他的场景怎么办呢?继续劫持原生方法。

一个前端项目中,除了事件监听,接口请求也是一个频繁出现的场景。接着上面的代码,下面我们来劫持一下Ajax

if (!XMLHttpRequest) {return;}const nativeAjaxSend = XMLHttpRequest.prototype.send; // 首先将原生的方法保存。const nativeAjaxOpen = XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open = function (mothod, url, ...args) { // 劫持open方法,是为了拿到请求的urlconst xhrInstance = this; xhrInstance._url = url;return nativeAjaxOpen.apply(this, [mothod, url].concat(args));}XMLHttpRequest.prototype.send = function (...args) { // 对于ajax请求的监控,主要是在send方法里处理。const oldCb = this.onreadystatechange;const oldErrorCb = this.onerror;const xhrInstance = this;xhrInstance.addEventListener('error', function (e) { // 这里捕获到的error是一个ProgressEvent。e.target 的值为 XMLHttpRequest的实例。当网络错误(ajax并没有发出去)或者发生跨域的时候,会触发XMLHttpRequest的error, 此时,e.target.status 的值为:0,e.target.statusText 的值为:''const errorObj = {...error_msg: 'ajax filed',error_stack: JSON.stringify({status: e.target.status,statusText: e.target.statusText}),error_native: e,...}/*接下来可以对errorObj进行统一处理*/});xhrInstance.addEventListener('abort', function (e) { // 主动取消ajax的情况需要标注,否则可能会产生误报if (e.type === 'abort') { xhrInstance._isAbort = true;}});this.onreadystatechange = function (...innerArgs) {if (xhrInstance.readyState === 4) {if (!xhrInstance._isAbort && xhrInstance.status !== 200) { // 请求不成功时,拿到错误信息const errorObj = {error_msg: JSON.stringify({code: xhrInstance.status,msg: xhrInstance.statusText,url: xhrInstance._url}),error_stack: '',error_native: xhrInstance};/*接下来可以对errorObj进行统一处理*/}}oldCb && oldCb.apply(this, innerArgs);}return nativeAjaxSend.apply(this, args);}
}
复制代码

我们引用框架时,某些框架会用**console.error**的方法抛出错误。我们可以劫持console.error,来捕获错误。

        const nativeConsoleError = window.console.error;window.console.error = function (...args) {args.forEach(item => {if (typeDetect.isError(item)) {...} else {...}});nativeConsoleError.apply(this, args);}
复制代码

原生的方法有很多,还比如fetchsetTimeout等。这里不一一列举了。但是使用劫持原生方法以覆盖所有的场景是十分困难的。

前端框架是怎么捕获错误的

我们主要来看一下ReactVue是怎么解决错误捕获问题的。

React中的错误捕获

Reactv16以前,可以使用unstable_handleError来处理捕获的错误。Reactv16以后,使用componentDidCatch来处理捕获的错误。若需全局捕获错误,可以在最外层包裹一层组件,在componentDidCatch中捕获错误信息。具体用法参考官方文档:reactjs.org/blog/2017/0…

React中,错误会被throw出来。在写作本文的时候,我遇到一个问题,如果在加载react相关的代码前,按照上文的方法劫持addEventListener,那么React将不会正常工作了,但是没有任何报错。React有一套自己的事件系统,会不会和这个有关呢?之前没有研究过React源码,粗略调试了以下,没有发现问题所在。后续会仔细研究。

Vue中的错误捕获

Vue的源码中,在关键函数(比如钩子函数等)执行的时候,都加上try{}catch(){},在cacth中处理捕获到的错误。看下面的源码。

...
// vue源码片段
function callHook (vm, hook) {// #7573 disable dep collection when invoking lifecycle hookspushTarget();var handlers = vm.$options[hook];if (handlers) {for (var i = 0, j = handlers.length; i < j; i++) {try {handlers[i].call(vm);} catch (e) {handleError(e, vm, (hook + " hook"));}}}if (vm._hasHookEvent) {vm.$emit('hook:' + hook);}popTarget();
}
...
function globalHandleError (err, vm, info) {if (config.errorHandler) {try {return config.errorHandler.call(null, err, vm, info)} catch (e) {logError(e, null, 'config.errorHandler');}}logError(err, vm, info);
}function logError (err, vm, info) {{warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm);}/* istanbul ignore else */if ((inBrowser || inWeex) && typeof console !== 'undefined') {console.error(err);} else {throw err}
}
复制代码

Vue中提供了Vue.config.errorHandler`来处理捕获到的错误。

// err: 捕获到的错误对象。
// vm: 出错的VueComponent.
// info: Vue 特定的错误信息,比如错误所在的生命周期钩子
Vue.config.errorHandler = function (err, vm, info) {}
复制代码

如果开发者没有配置Vue.config.errorHandler,那么捕获到的错误会以console.error的方式输出。

上报错误

捕获到错误后,如何上报呢?最常见、最简单的方式就是通过<img>了。代码简单,且没有跨域烦恼。

function logError(error){var img = new Image();img.onload = img.onerror = function(){img = null;}img.src = `${上报地址}?${processErrorParam(error)}`;
}
复制代码

当上报数据比较多时,可以使用post的方式进行上报。

错误的上报其实是一项复杂的工程,涉及到上报策略、上报分类等等。特别是在项目的业务比较复杂的时候,更应该关注上报的质量,避免影响到业务功能的正常运行。使用了打包工具处理的代码,往往还需要结合sourceMap进行代码定位。本文就不做介绍了。

写在后面

要建立一套完整、可用的前端错误监控体系是一项复杂、浩大的工程。但是,这项工程往往是必备的。本文主要介绍了你可能没关注过的Error的一些细节,以及如何捕获页面中的错误。关于劫持原生方法部分的代码,你可以在github.com/CoyPan/Fec找到。

符合预期。

前端开发中的Error以及异常捕获相关推荐

  1. 前端开发中的性能那点事

     前端开发中的性能那点事(一)巧用xdebug 前言: 在我们平时的php开发中,一个大的项目经过长时间的积累以后你会发现性能越来越慢,而性能到底消耗在了什么地方,常常是一个令人头疼的问题,funct ...

  2. java backbone_[Java教程]移动前端开发中的Backbone之一:Backbone中的模型和集合

    [Java教程]移动前端开发中的Backbone之一:Backbone中的模型和集合 0 2015-09-24 17:00:04 当我们开发含有大量Javascript的web应用程序时,首先你需要做 ...

  3. 【repost】一探前端开发中的JS调试技巧

    有请提示:文中涉及较多Gif演示动画,移动端请尽量在Wifi环境中阅读 前言:调试技巧,在任何一项技术研发中都可谓是必不可少的技能.掌握各种调试技巧,必定能在工作中起到事半功倍的效果.譬如,快速定位问 ...

  4. 前端开发中的调试技巧

    前端开发中的调试技巧 骨灰级调试大师Alert 那还是互联网刚刚起步的时代,网页前端还主要以内容展示为主,浏览器脚本还只能为页面提供非常简单的辅助功能的时候.那个时候,网页主要运行在以IE6为主的浏览 ...

  5. 前端开发中JS调试技巧,你知道几种?用过几种?

    调试技巧,在任何一项技术研发中都可谓是必不可少的技能.掌握各种调试技巧,必定能在工作中起到事半功倍的效果.譬如,快速定位问题.降低故障概率.帮助分析逻辑错误等等.而在互联网前端开发越来越重要的今天,如 ...

  6. 前端开发中的地理定位问题小总结

    项目中遇到地理定位的问题,没遇过不知道,遇过就发现这是个比较坑的问题.这个问题其实困扰了我挺久的,还不如一次性了解清楚.所以本文进行了一些小总结,先简单总结我看过的关于定位技术的资料,然后介绍前端开发 ...

  7. 前端开发中常用设计模式-总结篇

    本文是向大家介绍前端开发中常用的设计模式,它使我们编写的代码更容易被复用,也更容易被人理解,并且保证代码的稳定可靠性. 1.什么是设计模式 通俗来讲,就是日常使用设计的一种惯性思维. 因为对应的这种思 ...

  8. 一探前端开发中的JS调试技巧

    转自:http://seejs.me/2016/03/27/%E3%80%90%E5%8E%9F%E5%88%9B%E3%80%91%E4%B8%80%E6%8E%A2%E5%89%8D%E7%AB% ...

  9. 前端开发中常用图片格式

    前端开发中常用图片格式 在我们的日常开发中.必不可少会使用很多种图片. 我们需要根据业务场景来选择所使用的图片类型. 这里我整理了一些常用图片类型.他们的优缺点以及建议的使用场景. 如何在计算机中显示 ...

最新文章

  1. Android Google 服务框架相关问题
  2. kali如何制作php字典_Kali Linux安装搜狗输入法
  3. 【WebRTC---入门篇】(十五)WebRTC信令服务器实现
  4. python内建时间模块 time和datetime
  5. cad计算总长度插件_超实用的CAD插件大合集,视频教程手把手教学,工作效率翻倍...
  6. Kinect开发学习笔记之(七)骨骼数据的提取
  7. spark使用checkpoint恢复的两个小坑
  8. iOS 开发----Xcode 因为证书问题经常报的那些错
  9. 【Sentry】Sentry安装
  10. 【译】组织好你的Asp.Net MVC解决方案
  11. 阿里服务器降温系统,双十一服务器靠“泡澡”降温?阿里看上了3M的这项“冷”科技...
  12. java程序的运行方式
  13. 上位机软件需求说明书100元
  14. excel如何从字符串中截取指定字符(LEFT、RIGHR、MID三大函数)
  15. 计算机安装重装出现错误,一键重装失败怎么办?电脑重装系统失败的原因和解决方法...
  16. CAD用直线绘制矩形
  17. win7安装打印机 计算机,win7添加网络打印机的详细步骤【图解】
  18. [9i] 我们常说的好朋友:“哥们”、“兄弟”、“死党”、“闺蜜”,以及坏朋友:“小人”、“背叛者”在英文中怎么说。
  19. 一花一世界,在微服务中定义边界
  20. android 4k手机屏幕分辨率,【11-09讨论】手机上4K屏幕真的有必要吗?

热门文章

  1. java 动态解析_Java 如何解析key为动态的json操作
  2. java web 监听器 例子_Java web技术应用---监听器
  3. r语言ggplot2一夜多图_ggplot2简介
  4. php tp 查数据库数据一条_php读取数据库乱码
  5. ubuntu中命令打开图片、文档、音乐、视频等
  6. oracle归档日志是否启用,oracle归档日志满了,导致无法启动 ORA-03113
  7. 查询liunx上磁盘占用情况
  8. html创建等边三角形,CSS3 等边三角形组成星形图案
  9. 简洁实用的Redis分布式锁用法
  10. What is OPcache