前端一直是离用户最近的一层。随着产品的完善,我们会更加注重用户体验,但是前端异常却一直萦绕在喉咙里,非常烦人。1、为什么要处理异常?异常是不可控的,会影响最终的渲染,但是我们有充分的理由去做这样的事情。1.提升用户体验;2、远程定位问题;3、未雨绸缪,早发现问题;4、无法重连,尤其是移动端、机型、系统;5、完善的前端解决方案和前端监控系统;对于JS,我们面对的只是例外。异常的发生不会直接导致JS引擎崩溃,最多只是终止当前正在执行的任务。2、需要处理哪些异常?对于前端,我们可以做很多的异常捕获。总结一下,大致有以下几种:JS语法错误、代码异常、AJAX请求异常、静态资源加载异常、Promise异常、iframe异常、跨域脚本错误、崩溃卡顿。下面我将针对每种具体情况说明如何处理这些异常。3、对Try-Catch的误解try-catch只能捕获同步运行时错误,对语法错误和异步错误无能为力。1.同步运行时错误:try{letname='jartto';console.log(nam);}catch(e){console.log('caughtexception:',e);}output:caughtexception:ReferenceError:namisnotdefinedat<匿名>:3:152。语法错误无法捕获,我们修改代码,去掉单引号:try{letname='jartto;console.log(nam);}catch(e){console.log('Caughtexception:',e);}Output:UncaughtSyntaxError:Invalidorunexpectedtoken但是我们开发阶段可以看到语法错误,应该是不能成功上传到线上环境。3.异步错误try{setTimeout(()=>{undefined.map(v=>v);},1000)}catch(e){console.log('caughtexception:',e);}看看Look在日志中:UncaughtTypeError:Cannotreadproperty'map'ofundefinedatsetTimeout(:3:11)didnotcatchexception,需要我们特别注意。4.window.onerror不保证。当发生JS运行时错误时,window会触发ErrorEvent接口的错误事件,执行window.onerror()。/***@param{String}消息错误信息*@param{String}来源错误文件*@param{Number}lineno行号*@param{Number}colno列号*@param{Object}errorError对象(object)*/window.onerror=function(message,source,lineno,colno,error){console.log('Exceptioncaught:',{message,source,lineno,colno,error});}1.首先尝试同步Runtimeerrorwindow.onerror=function(message,source,lineno,colno,error){//message:错误信息(字符串)。//source:发生错误的脚本URL(string)//lineno:发生错误的行号(number)//colno:发生错误的列号(number)//error:错误对象(object)console.log('捕获异常:',{message,source,lineno,colno,error});}Jartto;可以看到,我们捕获了一个异常:2.语法错误再试一次?window.onerror=function(message,source,lineno,colno,error){console.log('捕获异常:',{message,source,lineno,colno,error});}letname='Jartto控制台打印出这样一个exception:UncaughtSyntaxError:Invalidorunexpectedtoken什么,没有捕获语法错误?3.心存疑虑,让我们最终尝试异步运行时错误:window.onerror=function(message,source,lineno,colno,error){console.log('Exceptioncaught:',{message,source,lineno,colno,error});}setTimeout(()=>{Jartto;});控制台输出:捕获异常:{message:"UncaughtReferenceError:Jarttoisnotdefined",source:"http://127.0.0.1:8001/",lineno:36,colno:5,error:ReferenceError:JarttoisnotdefinedatsetTimeout(http://127.0.0.1:8001/:36:5)}4、接下来我们试试网络请求异常的情况:我们发现无论是静态资源异常还是接口异常,都无法捕捉到错误。还有一点:只有当window.onerror函数返回true时,才不会抛出异常向上,否则即使知道异常发生在控制台还是会显示UncaughtError:xxxxxwindow.onerror=function(message,source,lineno,colno,error){console.log('caughtexception:',{message,source,lineno,colno,error});returntrue;}setTimeout(()=>{Jartto;});控制台不会再出现这样的错误:UncaughtReferenceError:JarttoisnotdefinedatsetTimeout((index):36)注:onerror***写在所有JS脚本的前面,否则可能抓不到错误;onerror抓不到语法错误;这里基本清楚了:在实际使用中,onerror主要是抓意外错误,而try-catch是用于在可预见的情况下监控特定的错误,两者结合使用效率更高。又来了,抓不到静态资源加载异常怎么办?五、window.addEventListener作为资源(如图片或脚本)加载失败,加载该资源的元素会触发Event接口的error事件,对该元素执行onerror()处理函数。这些错误事件不会冒泡到窗口,但(至少在Firefox中)可以通过单个window.addEventListener捕获来处理。window.addEventListener('error',(error)=>{console.log('caughtexception:',error);},true)控制台输出:由于网络请求异常不会引起事件冒泡,所以一定要在capture阶段捕获.但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断HTTP的状态是404还是500之类的,所以需要配合服务器日志进行排查分析。注意:不同浏览器返回的错误对象可能不同,需要注意兼容性处理。需要注意避免addEventListener重复监听。6.PromiseCatch在promise中使用catch可以很方便的捕获异步错误,非常简单。没有catch的Promise抛出的错误,onerror或者try-catch都无法捕捉到,所以我们一定不要忘记在Promise中写catch来处理抛出的异常。解决方案:为了防止遗漏Promise异常,建议全局添加unhandledrejection监控器,全局监控UncaughtPromiseError。用法:window.addEventListener("unhandledrejection",function(e){console.log(e);});我们继续尝试:window.addEventListener("unhandledrejection",function(e){e.preventDefault()console.log('caughtexception:',e);returntrue;});Promise.reject('promiseerror');可以看到如下输出:如果Promise没有被捕获怎么办?window.addEventListener("unhandledrejection",function(e){e.preventDefault()console.log('捕获异常:',e);returntrue;});newPromise((resolve,reject)=>{reject('jartto:promiseerror');});嗯,原来正常也会被抓到。所以我们上面说了,为了防止遗漏Promise异常,建议全局添加一个unhandledrejection的监控器,全局监控UncaughtPromiseError。再补充一点:如果去掉控制台的异常显示,需要添加:event.preventDefault();七、VUEerrorHandlerVue.config.errorHandler=(err,vm,info)=>{console.error('vueerrorHandler捕获的错误');console.error(err);console.error(vm);console.error(info);}8.React异常捕获React16提供了一个内置函数componentDidCatch,可以使用它轻松获取Error信息componentDidCatch(error,info){console.log(error,info);}另外,我们可以这样理解:错误边界UI的某一部分引起的JS错误不应该破坏整个程序,为了帮助React用户解决这个问题,React16引入了错误边界的新概念。需要注意的是:错误边界不会捕获下面的错误。1.事件处理器2.异步代码3.服务器端渲染代码4.错误边界区的错误举个小例子,下面componentDIdCatch(error,info)中的class会成为一个错误边界:classErrorBoundaryextendsReact。Component{constructor(props){super(props);this.state={hasError:false};}componentDidCatch(error,info){//DisplayfallbackUIthis.setState({hasError:true});//你也可以将错误记录到错误报告服务logErrorToMyService(error,info);}render(){if(this.state.hasError){//YoucanrenderanycustomfallbackUIreturnSomethingwentwrong.
;}returnthis.props.children;}}然后我们像普通组件一样使用它:componentDidCatch()方法的作用类似于JS的catch{}模块,但是对于组件来说,只有类类型的组件(类组件)可以成为错误边界。其实大多数情况下我们可以在整个程序中定义一个错误边界组件,然后一直使用它!九、iframe异常对于iframe异常捕获,我们要用到window.onerror:window.onerror=function(message,source,lineno,colno,error){console.log('caughtexception:',{message,source,lineno,colno,error});}一个简单的例子可能如下所示:十、Scripterror一般如果有Scripterror之类的错误,基本可以确定是跨域问题。这个时候其他的辅助信息就不多了,解决的办法无外乎以下几种:跨源资源共享机制(CORS):我们在script标签中加入crossOrigin属性。或者动态添加js脚本:constscript=document.createElement('script');script.crossOrigin='anonymous';script.src=url;document.body.appendChild(脚本);特别注意,服务器端需要设置:Access-Control-Allow-Origin另外,我们也可以试试这个——另一种解决ScriptError的方法:constoriginAddEventListener=EventTarget.prototype。addEventListener;EventTarget.prototype.addEventListener=function(type,listener,options){constwrappedListener=function(...args){try{returnlistener.apply(this,args);}catch(err){throwerr;}}返回原点AddEventListener.call(this,type,wrappedListener,options);}简单说明:重写EventTarget的addEventListener方法;包装传入的侦听器,返回包装后的侦听器,并尝试捕获其执行;浏览器不会对try-catch抛出的异常进行跨域拦截,所以当catch到来的时候,是有栈信息的;再次抛出异常时,执行的是同域的代码,所以在捕获window.onerror时不会丢失堆栈信息;通过包装addEventListener,我们还可以达到“扩栈”的效果:(()=>{constriginAddEventListener=EventTarget.prototype.addEventListener;EventTarget.prototype.addEventListener=function(type,listener,options){+//添加事件时捕获栈+constaddStack=newError(`Event(${type})`).stack;constwrappedListener=function(...args){try{returnlistener.apply(this,args);}catch(err){+//发生异常时展开栈+err.stack+='\n'+addStack;throwerr;}}returnoriginAddEventListener.call(this,type,wrappedListener,选项);}})();11.crashandlag是指网页响应暂时变慢。JS可能没有及时执行,但是crash就不一样了。网页崩溃,JS不运行,还有什么怎么监控网页崩溃,报告网页崩溃?崩溃和死机也不容忽视,可能会导致您的用户流失。1、利用window对象的load和beforeunload事件实现对网页崩溃的监控。好文章,推荐阅读:LoggingInformationonBrowserCrashes。window.addEventListener('load',function(){sessionStorage.setItem('good_exit','pending');setInterval(function(){sessionStorage.setItem('time_before_crash',newDate().toString());},1000);});window.addEventListener('beforeunload',function(){sessionStorage.setItem('good_exit','true');});if(sessionStorage.getItem('good_exit')&&sessionStorage.getItem('good_exit')!=='true'){/*insertcrashloggingcodehere*/alert('Hey,welcomebackfromyourcrash,lookslikeyoucrashedon:'+sessionStorage.getItem('time_before_crash'));}2.基于以下原因,我们可以使用ServiceWorker实现对网页崩溃的监控:ServiceWorker有自己独立的工作线程,这一点与网页不同。如果一个网页崩溃了,一般情况下ServiceWorker不会崩溃;ServiceWorker生命周期一般比网页长,可以用来监控网页状态;网页可以通过navigator.serviceWorker.controller.postMessageAPI向自己负责的SW发送消息。十二、报错1.通过Ajax发送数据由于Ajax请求本身可能会出现异常,可能会导致跨域问题,所以一般推荐使用动态创建img标签的形式进行报错。2.以functionreport(error){letreportUrl='http://jartto.wang/report';newImage().src=`${reportUrl}?logs=${error}`;}的形式动态创建img标签到收集异常信息太多,怎么办?在实践中,我们不得不考虑这样一种情况:如果你的网站访问量很大,必然会有很多错误发送的信息。这时候我们需要设置采集率来降低服务器的压力:Reporter.send=function(data){//只采集30%if(Math.random()<0.3){send(data)//Reporterrorinformation}}收集率要根据实际情况设置,随机数,或者某些用户特征都是不错的选择。十三。总结回到我们一开始提出的问题,如何优雅地处理异常?1.在可疑区域添加Try-Catch2。全局监听JS异常window.onerror3。全局监听静态资源异常window.addEventListener4。在没有Catch的情况下捕获Promise异常:unhandledrejection5。VUEerrorHandler和ReactcomponentDidCatch6。监控网页崩溃:加载和卸载之前的窗口对象7。跨域crossOrigin的解决方法其实很简单,上面说了:使用组合的解决方法,按类型捕获异常,这样基本上80%-90%的问题是看不见的。