JavaScript网页异常捕获一、异常大概分类 一般我们想要捕获的异常大概分类:语法错误onerror事件代码块与语法错误代码块不在一起,例如trycatche或者同在一个代码块,但是语法错误代码块异步执行以上情况都可以用onerror捕获语法错误setTimeout((){eval(function())},1000);UncaughtSyntaxError:Functionstatementsrequireafunctionname复制代码引用错误,类型错误,uri错误,范围错误等等非trycatch包裹情况下,可以使用onerror捕获同步错误、异步错误console。log(a)UncaughtReferenceError:aisnotdefinedArray。test()调用了Array上不存在的test,值为undefined,作为函数执行,则会抛出类型错误UncaughtTypeError:Array。testisnotafunctionnewArray(12221312312)UncaughtRangeError:InvalidarraylengthdecodeURI()UncaughtURIError:URImalformed复制代码try{}catch{}若try代码块报错,只能在catch中捕获。但是try代码块中若有异步错误代码,catch无法捕获,会被onerror捕获try{setTimeout((){console。log(a,a)可以被onerror捕获},1000)}catch(e){console。log(e,e)}UncaughtReferenceError:aisnotdefined复制代码Promise抛出错误没有设置catch捕获letpnewPromise((resolve,reject){reject(1)})这里没有做catch处理Uncaught(inpromise)1复制代码在catch中报错没有捕获;(asyncfunctionxx(){try{throw1}catch(e){console。log(a,a)这里出错可以使用unhandledrejection来捕获}})()UncaughtReferenceError:aisnotdefined复制代码上面两种情况可以监听unhandledrejection捕获错误window。addEventListener(unhandledrejection,function(e){e。preventDefault();阻止异常向上抛出console。log(捕获到异常unhandledrejection:,e)})复制代码静态资源加载失败在资源上添加onerror事件htmljsletimgIDdocument。getElementById(imgID)imgID。onerrorfunction(e){console。log(imgloaderror:,e);}注意:onerror需要定义之后,再设置图片路径,才能捕获到加载失败imgID。srchttp:xxx。png复制代码静态资源网络请求失败事件不会冒泡,需要在捕获阶段捕获chrome、FF中可以通过冒泡方式监听error事件捕获资源加载失败注意:此处会与上面的onerror事件一起触发,将导致日志重复上报可以只使用addEventListener捕获模式统一监听,就不需要注册window。onerror了window。addEventListener(error,error{console。log(addEventListener捕获到异常:,error)},true)复制代码网页崩溃网页加载后,埋入一个标志,表示正在加载初次进来,将埋入一个标志,值为pending,正常退出后,会设置为true若网页崩溃,第二次回来页面后,读取当前标志,如果值存在且为true则表示正常退出如果不是true,则表示上次可能是崩溃了,需要上报之前定时更新的时间值if(localStorage。getItem(goodexit)localStorage。getItem(goodexit)!true){localStorage。getItem(timebeforecrash)日志上报,将崩溃之前的时间一起上报}window。addEventListener(load,function(){localStorage。setItem(goodexit,pending);定时更新崩溃之前的网页时间setInterval(function(){localStorage。setItem(timebeforecrash,newDate()。toString());},10000);})window。addEventListener(beforeunload,function(){网页正常退出后,将埋入标志,设置成true,表示正常退出localStorage。setItem(goodexit,true);})复制代码上面是在第二次进入页面才知道网页崩溃,那么有什么方法可以在网页崩溃之后就可以上报呢可以使用ServiceWorker来进行监控其生命周期与页面无关(关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动)1、注册serviceworkerjs2、每间隔10秒,就向worker发送消息消息中包含:{type字段:active表示正常活跃,exit表示正常退出time字段:表示当前时间}在页面要退出后,发送type:exit,表示正常退出3、worker内部注册有消息接收事件,接收页面发送过来的消息接收到type为active,表示正常活跃,更新内部time的值接收到type为exit,表示正常退出,更新内部time值为0worker内部维护一个状态对象,包含时间,值为页面发送过来每隔15秒检查一次,若时间与上一次没有改变,则说明页面可能崩溃(注意区分time为0的情况)复制代码ScriptError跨域脚本的错误信息,因为处于保护信息的原因,只会展示ScriptError,通过以下方式解决1、外链脚本增加crossorigin属性:2、同时脚本的AccessControlAllowOrigin:设置为或者当前域名复制代码iframeError复制代码vue自身trycatch处理了错误,导致我们无法捕获使用Vue提供的errorHandler方法捕获Vue。config。errorHandlerfunction(err,vm,info){let{message,异常信息name,异常名称script,异常脚本urlline,异常行号column,异常列号stack异常堆栈信息}errvm为抛出异常的Vue实例info为Vue特定的错误信息,比如错误所在的生命周期钩子console。log(vueerr,vm,info:,err,vm,info)}复制代码 所以捕获错误总结下来:1、trycatch中的catch错误捕获可以在webpack打包时候,使用AST方式解析并在catch中插入日志上报代码2、error事件window。addEventListener(error,error{letdata{}此处与上面的onerror会重复事件let{colno,lineno,message,filename,error,stack}error不一定所有浏览器都支持colno参数letcolcolno(window。eventwindow。event。errorCharacter)0data。urlurldata。linelinedata。colcolif(!!stack){如果浏览器有堆栈信息直接使用data。msgstack。toString()}elseif(!!arguments。callee){尝试通过callee拿堆栈信息letext〔〕letfarguments。callee。caller,c3这里只拿三层堆栈信息while(f(c0)){ext。push(f。toString())if(ff。caller){break如果有环}ff。caller}extext。join(,)data。msgext}console。log(3、addEventListenererror捕获阶段异常:,data)},true)3、unhandledrejectionwindow。addEventListener(unhandledrejection,function(e){e。preventDefault();阻止异常向上抛出console。log(4、promise异常unhandledrejection:,e)})4、errorHandlerVue。config。errorHandlerfunction(err,vm,info){vm为抛出异常的Vue实例info为Vue特定的错误信息,比如错误所在的生命周期钩子let{message,异常信息name,异常名称script,异常脚本urlline,异常行号column,异常列号stack异常堆栈信息}errconsole。log(2、vueerrorHandler:,err,vm,info)}复制代码二、错误日志上报 既然异常已经捕获到了,那我们怎么处理呢,如何上报,需要上报哪些内容?日志分类 1、一般日志分类等级log、debug、info、warn、error复制代码 2、分场景使用日志上报类型log:记录流程信息debug:记录调试关键信息info:记录业务功能点,是否触发成功或者失败warn:页面警告信息error:页面错误或者业务异常信息复制代码 3、日志上报信息附带信息1、用户id、session、用户名2、当前错误信息3、可以用来重现、推断当前错误发生的信息4、上报时间5、日志等级等等复制代码 4、日志上报策略1、达量上传,设置一个缓存数量,到达即上报。因为不能一发生错误就要上报,会影响用户的网络。2、日志埋点处各自有各自的上报等级。需要有一个总配置地方,配置当前的上报等级这样各处埋点可以判断当前需要上报的日志等级,等级小于设置值的话,不可上报。3、本地缓存若暂存过多,需要删除前面的数据4、抽样上报5、设置缓存有效时间等等复制代码 上报之后,接下来的步骤就是在服务端收集分析归类展示,基于badjs我们搭建一整套日志解析系统三、日志上报 badjs服务安装 1、前期预备工作 为了快速搭建,我们统一使用docker安装 备注:windows环境使用docker,需要安装DockerDesktopmysql安装docker安装mysql备注:mysql安装好后,需要从badjsweb(需要先把项目下载下来)项目中的db目录下。使用create。sql初始化web相关的数据库mongodb安装(不可设置密码)docker安装mongo 2、项目安装 github克隆项目到本地gitclonehttps:github。comBetterJSbadjsinstaller复制代码 子项目下载以及依赖安装克隆下载badjsacceptor、badjsmq、badjsstorage、badjsweb项目yarnclone安装各项目的依赖yarninstall复制代码 3、修改配置项修改badjsacceptor项目的project。debug。jsonproject。json注意:这里是日志上报的地方,客户端初始化badjsreport时候需要设置的url属性即是这里的服务地址http:{badjsacceptor:port}badjs修改port属性:从80改为8083;因为node默认没有80端口的权限,需要你使用管理员权限才可以使用{port:8083}复制代码修改badjsweb项目的project。debug。jsonproject。json修改mysql属性,配置我们docker安装好的mysql用户密码与端口{mysql:{url:mysql:root:123456localhost:3306badjs}}复制代码 4、启动项目 yarnstart 查看badjsweb的启动端口,访问http:localhost:port可以看到日志后台管理服务页面四、badjs各模块 1、badjsacceptor接受客户端上报的日志badjsacceptor收到日志上报,发送到badjsmqpackage。json中配置dispatcher分发属性,表示向badjsmq请求的信息:如请求端口(10001)复制代码 2、badjsmq消息队列,保证消息有序稳定被接受badjsmq接收badjsacceptor的请求package。json中配置acceptor接收属性,表示用来接收信息所配置的接口信息:如端口(10001)。badjsmq再分发到badjsstoragepackage。json中配置dispatcher分发属性,表示向badjsstorage请求的信息:如端口(10000)复制代码 3、badjsstorage存储模块badjsstorage:接收来自badjsmq的请求,再写入到mongodbpackage。json中配置acceptor接收属性,表示用来接收信息所配置的接口信息:如端口(10000)复制代码 4、badjsweb日志后台管理系统badjsweb查询日志存储,分类查看日志信息,解析日志内容package。json中配置acceptor接收属性,badjsacceptor可请求的端口package。json中配置storage存储属性,查询mongodb数据package。json中配置mysql数据库属性,查询mysql数据等等复制代码五、上报日志插件badjsreport badjsreport重写了window。onerror来捕获错误 1、安装yarnaddbadjsreport复制代码 2、初始化importbadjsfrombadjsreportbadjs。init({必须配置项id:1此id为badjsweb启动后,申请的项目的id,上报的日志根据该id区分业务模块url:http:badjsacceptor启动后的地址,日志上报到的地方选择配置uin:123,指定用户的id(该插件默认读取qquin)delay:1000,延迟多少毫秒,合并缓冲区中的上报(默认1000)ignore:〔Scripterrori〕,忽略某个错误,遇到该错误不进行上报random:1,抽样上报,值可以设置01之间。1表示100上报(默认为1)repeat:5,重复上报次数(对于同一个错误超过多少次不上报;避免单个用户同一错误上报过多的情况)onReport:function(id,errObj){},上报日志之后的回调。id为上报的id,errorObj为上报的错误对象submit:function(url){},覆盖原来的上报方式,原来是使用newImage()形式上报,可以修改成自己想要上报的方式,比如使用post内部构造好的urlext:{},扩展属性,后端做扩展处理属性。设置了ext的值,就会作为ext设置的值合并到构造好的上报url中offlineLog:false,是否开启离线日志(默认不开启为false)offlineLogExp:5离线有效时间(默认最近5天)})复制代码 3、手动上报a、badjs。report(errormsg)b、badjs。report({msg:errormsg,需要上报的错误信息target:error。js,发生错误的js文件rowNum:1,发生错误的行数colNum:2发生错误的列数})复制代码 4、延迟上报 暂存badjs。push(errormsg)badjs。push({msg:errormsg,需要上报的错误信息target:error。js,发生错误的js文件rowNum:1,发生错误的行数colNum:2发生错误的列数})复制代码 立即上报badjs。report({msg:errormsg,需要上报的错误信息target:error。js,发生错误的js文件rowNum:1,发生错误的行数colNum:2发生错误的列数})复制代码 5、上报离线日志badjs。reportOfflineLog()复制代码六、项目应用importBJREPORTfrombadjsreportimportVuefromvue环境ID枚举letENVIDENUM{DEV:1,SIT:2,UAT:3,PRO:4}letcurEnv,originwindow。location。originif(origin。indexOf(dev。xxx。com)1){http:dev。xxx。comDEV环境curEnvDEV}elseif(origin。indexOf(sit。xxx。com)1){http:sit。xxx。comSIT环境curEnvSIT}elseif(origin。indexOf(uat。xxx。com)1){http:uat。xxx。comUAT环境curEnvUAT}elseif(origin。indexOf(m。xxx。com)1){http:pro。xxx。comPRO环境curEnvPRO}letenvIDENVIDENUM〔curEnv〕ENVIDENUM。DEV初始化日志上报插件BJREPORT。init({id:envID,不指定id将不上报,url:http:{badjsacceptor:port}badjs})初始化项目时,可以暴露一个全局的vm实例,方便上传需要的信息window。rootvmnewVue({})初始化项目初始化监听异常init(window。rootvm)functioninit(rootInstance){Vue。config。errorHandlerfunction(err,curInstance,info){vm为抛出异常的Vue实例info为Vue特定的错误信息,比如错误所在的生命周期钩子let{message,异常信息name,异常名称script,异常脚本urlline,异常行号column,异常列号stack异常堆栈信息}errlog(message,stack,line,column,curInstance)console。log(vueerrorHandler:,err,curInstance,info)}window。addEventListener(error,e{let{colno,lineno,message,filename}elog(message,filename,lineno,colno,rootInstance)console。log(addEventListenererror捕获阶段异常:,e)},true)window。addEventListener(unhandledrejection,function(e){e。preventDefault();阻止异常向上抛出let{reason}elog(JSON。stringify(reason),,,,rootInstance)console。log(Promise异常unhandledrejection:,e)})}functionlog(msg,target,rowNum,colNums,vminstance){letmsgs〔{msg}〕,state(vminstancevminstance。storevminstance。store。state){}用户信息if(state。userInfo){let{phoneNumber,userId}state。userInfomsgs〔phone:{phoneNumber}userId:{userId}〕}路由信息let{name,fullPath}(vminstancevminstance。route){}msgs〔routername:{name}routerfullpath:{fullPath}〕BJREPORT。report({msg:msgs,target,rowNum,colNums})}