JavaScript之错误异常探讨

同步发布于 https://github.com/xianshanna…我的建议是不要隐藏错误,勇敢地抛出来。没有人会因为代码出现 bug 导致程序崩溃而羞耻,我们可以让程序中断,让用户重来。错误是无法避免的,如何去处理它才是最重要的。JavaScript 提供一套错误处理机制,错误是干扰程序正常流程的非正常的事故。而没人可以保持程序没有 bug,那么上线后遇到特殊的 bug,如何更快的定位问题所在呢?这就是我们这个专题需要讨论的问题。下面会从 JavaScript Error 基础知识、如何拦截和捕获异常、如何方便的在线上报错误等方面来叙述,本人也是根据网上的知识点进行了一些总结和分析(我只是互联网的搬运工,不是创造者),如果有什么错漏的情况,请在 issue 上狠狠的批评我。这个专题目前是针对浏览器的,还没考虑到 node.js,不过都是 JavaScript Es6 语法,大同小异。什么时候 JavaScript 会抛出错误呢?一般分为两种情况:JavaScript 自带错误开发者主动抛出的错误JavaScript 引擎自动抛出的错误大多数场景下我们遇到的错误都是这类错误。如果发生Javscript 语法错误、代码引用错误、类型错误等,JavaScript 引擎就会自动触发此类错误。如下一些场景:场景一console.log(a.notExited)// 浏览器会抛出 Uncaught ReferenceError: a is not defined场景二const a;// 浏览器抛出 Uncaught SyntaxError: Missing initializer in const declaration语法错误,浏览器一般第一时间就抛出错误,不会等到执行的时候才会报错。场景三let data;data.forEach(v=>{})// Uncaught TypeError: Cannot read property ‘forEach’ of undefined手动抛出的错误一般都是类库开发的自定义错误异常(如参数等不合法的错误异常抛出)。或者重新修改错误 message 进行上报,以方便理解。场景一function sum(a,b){ if(typeof a !== ’number’) { throw TypeError(‘Expected a to be a number.’); } if(typeof b !== ’number’) { throw TypeError(‘Expected b to be a number.’); } return a + b;}sum(3,’d’);// 浏览器抛出 uncaught TypeError: Expected b to be a number.场景二当然我们不一定需要这样做。let data;try { data.forEach(v => {});} catch (error) { error.message = ‘data 没有定义,data 必须是数组。’; error.name = ‘DataTypeError’; throw error;}如何创建 Error 对象?创建语法如下:new Error([message[,fileName,lineNumber]])省略 new 语法也一样。其中fileName 和 lineNumber 不是所有浏览器都兼容的,谷歌也不支持,所以可以忽略。Error 构造函数是通用错误类型,除了 Error 类型,还有 TypeError、RangeError 等类型。Error 实例这里列举的都是 Error 层的原型链属性和方法,更深层的原型链的继承属性和方便不做说明。一些有兼容性的而且不常用的属性和方法不做说明。console.log(Error.prototype)// 浏览器输出 {constructor: ƒ, name: “Error”, message: “”, toString: ƒ}其他错误类型构造函数是继承 Error,实例是一致的。属性Error.prototype.message错误信息, Error(“msg”).message === “msg”。Error.prototype.name错误类型(名字), Error(“msg”).name === “Error”。如果是 TypeError,那么 name 为 TypeError。Error.prototype.stackError 对象作为一个非标准的栈属性提供了一种函数追踪方式。无论这个函数被被调用,处于什么模式,来自于哪一行或者哪个文件,有着什么样的参数。这个栈产生于最近一次调用最早的那次调用,返回原始的全局作用域调用。这个不是规范,存在兼容性。经测试,谷歌、火狐、Edge、safar 都支持此特性(都是在最新的版本下测试 2019-04-02),IE 不支持。方法Error.prototype.constructorError.prototype.toString返回值格式为 ${name}: ${message}。常用 Error 类型除了通用的 Error 构造函数外,JavaScript还有常见的 5 个其他类型的错误构造函数。TypeError创建一个 Error 实例,表示错误的原因:变量或参数不属于有效类型。throw TypeError(“类型错误”);// Uncaught TypeError: 类型错误RangeError创建一个 Error 实例,表示错误的原因:数值变量或参数超出其有效范围。throw RangeError(“数值超出有效范围”);// Uncaught RangeError: 数值超出有效范围ReferenceError创建一个 Error 实例,表示错误的原因:无效引用。throw ReferenceError(“无效引用”);// Uncaught ReferenceError: 无效引用SyntaxError创建一个 Error 实例,表示错误的原因:语法错误。这种场景很少用,除非类库定义了新语法(如模板语法)。throw SyntaxError(“语法错误”);// Uncaught SyntaxError: 语法错误URIError创建一个 Error 实例,表示错误的原因:涉及到 uri 相关的错误。throw URIError(“url 不合法”);// Uncaught RangeError: url 不合法自定义 Error 类型自定义新的 Error 类型需要继承 Error ,如下自定义 CustomError:function CustomError(…args){ class InnerCustomError extends Error { name = “CustomError”; } return new InnerCustomError(…args);}继承 Error 后,我们只需要对 name 做重写,然后封装成可直接调用的函数即可。如何拦截 JavaScript 错误?既然没人能保证 web 应用不会出现 bug,那么出现异常报错时,如何拦截并进行一些操作呢?try…catch… 拦截这是拦截 JavaScript 错误,拦截后,如果不手动抛出错误,这个错误将静默处理。平常写代码如果我们知道某段代码可能会出现报错问题,就可以使用这种方式。如下:const { data } = this.props;try { data.forEach(d=>{}); // 如果 data 不是数组就会报错} catch(err){ console.error(err); // 这里可以做上报处理等操作}一些使用方式十分不友好的处理方式try…catch… 使用需要注意,try…catch… 后,错误会被拦截,如果不主动抛出错误,那么无法知道报错位置。如下面的处理方式就是不好的。function badHandler(fn) { try { return fn(); } catch (err) { /noop,不做任何处理/ } return null;}badHandler();这样 fn 回调发送错误后,我们无法知道错误是在哪里发生的,因为已经被 try…catch 了,那么如何解决这个问题呢?相对友好但糟糕的处理方式function CustomError(…args){ class InnerCustomError extends Error { name = “CustomError”; } return new InnerCustomError(…args);}function uglyHandlerImproved(fn) { try { return fn(); } catch (err) { throw new CustomError(err.message); } return null;}badHandler();现在,这个自定义的错误对象包含了原本错误的信息,因此变得更加有用。但是因为再度抛出来,依然是未处理的错误。try…catch… 可以拦截异步错误吗?这个也要分场景,也看个人的理解方向,首先理解下面这句话:try…catch 只会拦截当前执行环境的错误,try 块中的异步已经脱离了当前的执行环境,所以 try…catch… 无效。setTimeout 和 Promise 都无法通过 try…catch 捕获到错误,指的是 try 包含异步(非当前执行环境),不是异步包含 try(当前执行环境)。异步无效和有效 try…catch 如下:setTimeout这个无效:try { setTimeout(() => { data.forEach(d => {}); });} catch (err) { console.log(‘这里不会运行’);}下面的 try…catch 才会有效:setTimeout(() => { try { data.forEach(d => {}); } catch (err) { console.log(‘这里会运行’); }});Promise这个无效:try { new Promise(resolve => { data.forEach(d => {}); resolve(); });} catch (err) { console.log(‘这里不会运行’);}下面的 try…catch 才会有效:new Promise(resolve => { try { data.forEach(d => {}); } catch (err) { console.log(‘这里会运行’); }});小结不是所有场景都需要 try…catch… 的,如果所有需要的地方都 try…catch,那么代码将变得臃肿,可读性变差,开发效率变低。那么我需要统一获取错误信息呢?有没有更好的处理方式?当然有,后续会提到。Promise 错误拦截Promise.prototype.catch 可以达到 try…catch 一样的效果,只要是在 Promise 相关的处理中报错,都会被 catch 到。当然如果你在相关回调函数中 try…catch,然后做了静默提示,那么也是 catch 不到的。如下会被 catch 到:new Promise(resolve => { data.forEach(v => {});}).catch(err=>{/这里会运行/})下面的不会被 catch 到:new Promise(resolve => { try { data.forEach(v => {}); }catch(err){}}).catch(err=>{/这里不会运行/})Promise 错误拦截,这里就不详细说了,如果你看懂了 try…catch,这个也很好理解。setTimeout 等其他异步错误拦截呢?目前没有相关的方式直接拦截 setTimeout 等其他异步操作。如果要拦截 setTimeout 等异步错误,我们需要在异步回调代码中处理,如:setTimeout(() => { try { data.forEach(d => {}); } catch (err) { console.log(‘这里会运行’); }});这样可以拦截到 setTimeout 回调发生的错误,但是如果是下面这样 try…catch 是无效的:try { setTimeout(() => { data.forEach(d => {}); });} catch (err) { console.log(‘这里不会运行’);}如何获取 JavaScript 错误信息?你可以使用上面拦截错误信息的方式获取到错误信息。但是呢,你要每个场景都要去拦截一遍吗?首先我们不确定什么地方会发生错误,然后我们也不可能每个地方都去拦截错误。不用担心,JavaScript 也考虑到了这一点,提供了一些便捷的获取方式(不是拦截,错误还是会终止程序的运行,除非主动拦截了)。window.onerror 事件获取错误信息onerror 事件无论是异步还是非异步错误(除了 Promise 错误),onerror 都能捕获到运行时错误。需要注意一下几点:window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx。如果使用 addEventListener,event.preventDefault() 可以达到同样的效果。window.onerror 是无法捕获到网络异常的错误、<img/>或<script>资源加载失败等错误。不过如果你使用了 fetch 等支持 promise 的方式,错误可以通过 unhandledrejection 方式拿到错误信息。语法:window.onerrorwindow.onerror = function(message, source, lineno, colno, error) { … }window.addEventListener(’error’)window.addEventListener(’error’, function(event) { … })详细看这里。unhandledrejection 事件获取 Promise 错误unhandledrejection 存在兼容性问题,IE、Edge、火狐等目前都不支持。用法如下:window.addEventListener(“unhandledrejection”, event => { console.warn(UNHANDLED PROMISE REJECTION: ${event.reason});});或者window.onunhandledrejection = event => { console.warn(UNHANDLED PROMISE REJECTION: ${event.reason});};详细看这里。如何处理错误信息?错误对象 Error 包含如下的字段返回:name错误的类型,一般有 Error、TypeError、ReferenceError、RangeError、URIError 等,当然也可以是自定义的错误类型。message错误的详细信息,可以是 JavaScript 自动抛出的错误信息,也可以是手动抛出的自定义信息。stack这个不是规范,存在兼容性。经测试,谷歌、火狐、Edge、safar 都支持此特性(都是在最新的版本下测试 2019-04-02),IE 不支持。name 和 message 我们都不用做什么处理,主要是要针对 stack 做处理,一般我们需要把,这三个字段的信息提交到错误处理系统,针对性处理。同时我们生成环境的代码是被压缩后的代码,需要使用 sourceMap 进行映射还原代码。后续会补充这个讨论。参考文章Error(MDN)A Guide to Proper Error Handling in JavaScriptAnatomy of a JavaScript Error ...

April 3, 2019 · 3 min · jiezi

前端开发中的Error以及异常捕获

本文首发于公众号:符合预期的CoyPan写在前面在前端项目中,由于JavaScript本身是一个弱类型语言,加上浏览器环境的复杂性,网络问题等等,很容易发生错误。做好网页错误监控,不断优化代码,提高代码健壮性是一项很重要的工作。本文将从Error开始,讲到如何捕获页面中的异常。文章较长,细节较多,请耐心观看。前端开发中的ErrorJavaScript中的ErrorJavaScript中,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:9console.log(a.name); // Errorconsole.log(a.message); // 错误测试Error只有一个标准方法:Error.prototype.toString:返回表示一个表示错误的字符串。接上面的代码:a.toString(); // “Error: 错误测试"非标准的属性各个浏览器厂商对于Error都有自己的实现。比如下面这些属性:Error.prototype.fileName:产生错误的文件名。Error.prototype.lineNumber:产生错误的行号。Error.prototype.columnNumber:产生错误的列号。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。DOMExceptionDOMException是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有以下三个属性:DOMException.code:错误编号。DOMException.message:错误描述。DOMException.name:错误名称。以上面那段错误代码为例,其抛出的DOMException各属性的值为:code: 8message: “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中,如果Promise被reject了,就会抛出异常:PromiseRejectionEvent。注意,下面两种情况都会导致Promise被reject:业务代码本身调用了Promise.reject。Promise中的代码出错。PromiseRejectionEvent的构造函数目前在浏览器中大多都不兼容,这里就不说了。PromiseRejectionEvent的属性有两个:PromiseRejectionEvent.promise:被reject的Promise。PromiseRejectionEvent.reason:Promise被reject的原因。会传递给reject。Promsie的catch中的参数。加载资源出错由于网络,安全等原因,网页加载资源失败,请求接口出错等,也是一种常见的错误。关于错误的小结一个网页在运行过程中,可能发生四种错误:JavaScript在运行过程,语言自身抛出的异常。JavaScript在运行过程中,调用Web Api时发生异常。Promise中的拒绝。网页加载资源,调用接口时发生异常。我认为,对于前两种错误,我们在平时的开发过程中,不用特别去区分,可以统一成:【代码出错】。捕获错误网页发生错误,开发者如何捕获这些错误呢 ? 常见的有以下方法。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.onerrorwindow.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]。可以看到event是ErrorEvent对象的实例。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, InternalError,RangeError,EvalError,ReferenceError,SyntaxError,TypeError,URIError,DOMException中的一种。window.addEventListener(‘unhandledrejection’)window.addEventListener(‘unhandledrejection’, function (event) { … });在使用Promise的时候,如果没有声明catch代码块,Promise的异常会被抛出。只能通过这个方法或者window.onunhandledrejection才能捕获到该异常。event就是上文提到的PromiseRejectionEvent。我们只需要关注其reason就行。window.onerror 和 window.addEventListener(’error’)的区别首先是事件监听器和事件处理器的区别。监听器只能声明一次,后续的声明会覆盖之前的声明。而事件处理器则可以绑定多个回调函数。资源( <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年前这篇文章里提到的安全问题:https://blog.jeremiahgrossman…,浏览器们都对内核进行了升级:当加载自不同域的脚本中发生语法错误时,为避免信息泄露,语法错误的细节将不会报告,而是使用简单的"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 catch try { 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方法,是为了拿到请求的url const 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); }原生的方法有很多,还比如fetch、setTimeout等。这里不一一列举了。但是使用劫持原生方法以覆盖所有的场景是十分困难的。前端框架是怎么捕获错误的我们主要来看一下React和Vue是怎么解决错误捕获问题的。React中的错误捕获在Reactv16以前,可以使用unstable_handleError来处理捕获的错误。Reactv16以后,使用componentDidCatch来处理捕获的错误。若需全局捕获错误,可以在最外层包裹一层组件,在componentDidCatch中捕获错误信息。具体用法参考官方文档:https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html在React中,错误会被throw出来。在写作本文的时候,我遇到一个问题,如果在加载react 相关的代码前,按照上文的方法劫持addEventListener,那么React将不会正常工作了,但是没有任何报错。React有一套自己的事件系统,会不会和这个有关呢?之前没有研究过React源码,粗略调试了以下,没有发现问题所在。后续会仔细研究。Vue中的错误捕获Vue的源码中,在关键函数(比如钩子函数等)执行的时候,都加上try{}catch(){},在cacth中处理捕获到的错误。看下面的源码。…// vue源码片段function callHook (vm, hook) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget(); 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的方式输出。上报错误捕获到错误后,如何上报呢?最常见、最简单的方式就是通过&lt;img&gt;了。代码简单,且没有跨域烦恼。function logError(error){ var img = new Image(); img.onload = img.onerror = function(){ img = null; } img.src = ${上报地址}?${processErrorParam(error)}`;}当上报数据比较多时,可以使用post的方式进行上报。错误的上报其实是一项复杂的工程,涉及到上报策略、上报分类等等。特别是在项目的业务比较复杂的时候,更应该关注上报的质量,避免影响到业务功能的正常运行。使用了打包工具处理的代码,往往还需要结合sourceMap进行代码定位。本文就不做介绍了。写在后面要建立一套完整、可用的前端错误监控体系是一项复杂、浩大的工程。但是,这项工程往往是必备的。本文主要介绍了你可能没关注过的Error的一些细节,以及如何捕获页面中的错误。关于劫持原生方法部分的代码,你可以在https://github.com/CoyPan/Fec找到。符合预期。欢迎关注我的公众号: 符合预期的CoyPan,这里只有干货,符合你的预期。 ...

January 2, 2019 · 3 min · jiezi

前端异常监控-看这篇就够了

前端异常监控如果debug是移除bug的流程,那么编程就一定是将bug放进去的流程。如果没有用户反馈问题,那就代表我们的产品棒棒哒,对不对?主要内容Web规范中相关前端异常异常按照捕获方式分类异常的捕获方式日志上报的方式前端异常类型(Execption)WebIDL和ecma-262中的错误类型ECMAScript exceptions <==> IDL 的简单异常当脚本代码运行时发生的错误,会创建Error对象,并将其抛出,除了通用的Error构造函数外,以下是另外几个ECMAScript 2015中定义的错误构造函数。EvalError eval错误RangeError 范围错误ReferenceError 引用错误TypeError 类型错误URIError URI错误SyntaxError 语法错误 (这个错误WebIDL中故意省略,保留给ES解析器使用)Error 通用错误 (这个错误WebIDL中故意省略,保留给开发者使用使用)DOMException 最新的DOM规范定义的错误类型集,兼容旧浏览的DOMError接口, 完善和规范化DOM错误类型。IndexSizeError 索引不在允许的范围内HierarchyRequestError 节点树层次结构是不正确的。WrongDocumentError 对象是错误的InvalidCharacterError 字符串包含无效字符。NoModificationAllowedError 对象不能被修改。NotFoundError 对象不能在这里被找到。NotSupportedError 不支持的操作InvalidStateError 对象是一个无效的状态。SyntaxError 字符串不匹配预期的模式InvalidModificationError 对象不能以这种方式被修改NamespaceError 操作在XML命名空间内是不被允许的InvalidAccessError 对象不支持这种操作或参数。TypeMismatchError 对象的类型不匹配预期的类型。SecurityError 此操作是不安全的。NetworkError 发生网络错误AbortError 操作被中止URLMismatchError 给定的URL不匹配另一个URL。QuotaExceededError 已经超过给定配额。TimeoutError 操作超时。InvalidNodeTypeError 这个操作的 节点或节点祖先 是不正确的DataCloneError 对象不能克隆。前端错误异常按照捕获方式分类[x] 运行时异常语法错误[x] 资源加载异常imgscriptlinkaudiovideoiframe…外链资源的DOM元素[x] 异步请求异常XMLHttpRequestfetch[x] Promise异常[ ] CSS中资源异常@font-facebackground-image…暂时无法捕获前端错误异常的捕获方式try-catch (ES提供基本的错误捕获语法)只能捕获同步代码的异常回调setTimeoutpromisewindow.onerror = cb (DOM0)imgscriptlinkwindow.addEventListener(’error’, cb, true) (DOM2)window.addEventListener(“unhandledrejection”, cb) (DOM4)Promise.then().catch(cb)封装XMLHttpRequest&fetch | 覆写请求接口对象try-catch-finally将能引发异常的代码块放到try中,并对应一个响应,然后有异常会被捕获 try { // 模拟一段可能有错误的代码 throw new Error(“会有错误的代码块”) } catch(e){ // 捕获到try中代码块的错误得到一个错误对象e,进行处理分析 report(e) } finally { console.log(“finally”) }onerror事件window.onerror当JavaScript运行时错误(包括语法错误)发生时,window会触发一个ErrorEvent接口的事件,并执行window.onerror()/** * @description 运行时错误处理器 * @param {string} message 错误信息 * @param {string} source 发生错误的脚本URL * @param {number} lineno 发生错误的行号 * @param {number} colno 发生错误的列号 * @param {object} error Error对象 /function err(message,source,lineno,colno,error) {…}window.onerror = errelement.onerror当一项资源(如<img>或<script>)加载失败,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror()处理函数。element.onerror = function(event) { … } //注意和window.onerror的参数不同注意:这些error事件不会向上冒泡到window,不过能被单一的window.addEventListener捕获。window.addEventListeneraddEventListener相关的一些内容W3C DOM2 Events规范中提供的注册事件监听器的方法, 在这之前均使用el.onclick的形式(DOM0 规范的基本内容,几乎所有浏览器都支持)。注意: 接口的几种语法error事件捕获资源加载错误资源加载失败,不会冒泡,但是会被addEventListener捕获,所以我们可以指定在加载失败事件的捕获阶段捕获该错误。注意: 接口同时也能捕获运行时错误。window.addEventListener(“error”, function(e) { var eventType = [].toString.call(e, e); if (eventType === “[object Event]”) { // 过滤掉运行时错误 // 上报加载错误 report(e) } }, true);unhandledrejection事件捕获Promise异常最新的规范中定义了 unhandledrejection事件用于全局捕获promise对象没有rejection处理器时异常情况。window.addEventListener(“unhandledrejection”, function (event) { // …your code here to handle the unhandled rejection… // Prevent the default handling (error in console) event.preventDefault();});Promise.then().catch(cb).finally()Promise中的错误会被Promise.prototype.catch捕获,所以我们通过这种方式捕获错误,这包括一些不支持unhandledrejection事件的环境中promisede polyfill实现。new Promise(function(resolve, reject) { throw ‘Uncaught Exception!’;}).catch(function(e) { console.log(e); // Uncaught Exception!});封装XMLHttpRequest&fetch | 覆写请求接口对象// 覆写XMLHttpRequest APIif(!window.XMLHttpRequest) return; var xmlhttp = window.XMLHttpRequest; var _oldSend = xmlhttp.prototype.send; var _handleEvent = function (event) { if (event && event.currentTarget && event.currentTarget.status !== 200) { report(event) } } xmlhttp.prototype.send = function () { if (this[‘addEventListener’]) { this[‘addEventListener’](’error’, _handleEvent); this[‘addEventListener’](’load’, _handleEvent); this[‘addEventListener’](‘abort’, _handleEvent); this[‘addEventListener’](‘close’, _handleEvent); } else { var _oldStateChange = this[‘onreadystatechange’]; this[‘onreadystatechange’] = function (event) { if (this.readyState === 4) { _handleEvent(event); } _oldStateChange && _oldStateChange.apply(this, arguments); }; } return _oldSend.apply(this, arguments); }// 覆写fetch APIif (!window.fetch) return;var _oldFetch = window.fetch;window.fetch = function() { return _oldFetch .apply(this, arguments) .then(function(res){ if (!res.ok) { // True if status is HTTP 2xx report(res) } return res; }) .catch(function(error){ report(res) });}日志上报的方式异步请求上报, 后端提供接口,或者直接发到日志服务器img请求上报, url参数带上错误信息eg:(new Image()).src = ‘http://baidu.com/tesjk?r=tksjk'注意跨源脚本异常当加载自不同域的脚本中发生语法错误时,为避免信息泄露,语法错误的细节将不会报告,而代之简单的 “Script error.“由于同源策略影响,浏览器限制跨源脚本的错误访问,这样跨源脚本错误报错信息如下图:在H5的规定中,只要满足下面俩个条件,是允许获取跨源脚本的错误信息的。客户端在script标签上增加crossorigin属性;服务端设置js资源响应头Access-Control-Origin:(或者是域名)。扩展阅读业界已经有的监控平台Sentry开源阿里的ARMSfundebugFrontJS几个异常监控的问题如何保证大家提交的代码是符合预期的? 如何了解前端项目的运行是否正常,是否存在错误?代码质量体系控制和错误监控以及性能分析如果用户使用网页,发现白屏,现在联系上了你们,你们会向他询问什么信息呢?先想一下为什么会白屏?我们以用户访问页面的过程为顺序,大致排查一下用户没打开网络DNS域名劫持http劫持cdn或是其他资源文件访问出错服务器错误前端代码错误前端兼容性问题用户操作出错通过以上可能发生错误的环节,我们需要向用户手机一下以下的用户信息当前的网络状态运营商地理位置访问时间客户端的版本(如果是通过客户端访问)系统版本浏览器信息设备分辨率页面的来源用户的账号信息通过performance API收集用户各个页面访问流程所消耗的时间收集用户js代码报错的信息如果我们使用了脚本代码压缩,然而我们又不想将sourcemap文件发布到线上,我们怎么捕获到错误的具体信息?CSS文件中也存在引用资源,@font-face, background-image …等这些请求错误该如何进行错误捕获?总结Web规范中相关前端异常DOM处理异常ECMAScript处理异常异常按照捕获方式分类运行时异常资源加载异常异步请求异常Promise异常异常的捕获方式try-catch (ES提供基本的错误捕获语法)只能捕获同步代码的异常回调setTimeoutpromisewindow.onerror = cb (DOM0)imgscriptlinkwindow.addEventListener(’error’, cb, true) (DOM2)window.addEventListener(“unhandledrejection”, cb) (DOM4)Promise.then().catch(cb)封装XMLHttpRequest&fetch | 覆写请求接口对象注意点:跨源脚本异常的捕获日志上报的方式异步请求上报new img上报扩展阅读业界已有的异常监控平台几个跟异常监控有关的问题 ...

November 9, 2018 · 2 min · jiezi