web错误处理/错误捕获方案
前言
花了一些时间整理完善项目的错误处理/错误捕获能力,借此进行一次总结。为了方便阅读,先概括下大概的思路:
// 错误处理,避免报错导致程序无法继续执行
1、自行对重要步骤进行容灾和try...catch...finally等处理;
2、通过打包工具(我用的是vite,自己实现的是rollup的plugin)将绝大部分语句包裹try...catch语句,并补全错误信息(文件名,方法名);
// 错误捕获
1、onerror 事件捕获全局错误;
2、unhandledrejection 捕获异步错误;
3、框架层面错误监听(e.g. Vue.config.errorHandler, react ErrorBoundary);
3、axios等tcp/ip错误处理;
4、静态资源加载异常捕获(window.addEventListener('error',(event)=>{});
// 错误分析,补全错误信息
1、通过sourcemap解析错误信息,定位到具体错误代码;
错误处理
我实现的插件是rollup-plugin-trycatch,这个方案不算成熟:
涉及到业务代码的translate,单元测试覆盖面可能不足(目前仅处理几种类型函数,欢迎pr);
如果项目比较稳定,测试覆盖率较高,不建议使用此方案。
rollup-plugin-trycatch
跟webpack有些区别,rollup不分plugin和loader,只通过plugin来实现插件。主要流程如下:
1、创建rollup插件;
2、通过rollup的acorn插件将code转为ast语法树;
3、通过stack-utils插件将当前文件/文件名行数等信息添加到err里;
3、通过estree-walker遍历语法树,将相关的语句(函数)增加wrap节点;
4、通过escodegen插件将语法树转为code;
功能:
1、给所有函数(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, ObjectMethod)包一层try...catch捕获异常;
2、try...catch错误补全(增加报错信息,原文件名,方法名等错误分析,但仍不够精确,未能定位到具体报错的语句);
具体内容和使用方式可以查看源码,里面有vite的使用demo;
错误捕获
错误捕获的难点在于两个方面:
1、如何全面的捕获到错误;
2、如果通过错误分析到具体问题(一般来说线上代码都是打包压缩过的,如何通过打包压缩后的代码定位到问题);
全面(尽可能)的错误捕获
在我们尝试错误捕获之前,需要先了解有哪些错误,分三类(这里其实有很多分类的方式,我尝试用我自己的分类方式来解读):
1、远程资源加载错误;
window.addEventListener('error', (err) => {
let _url = ''
// 远程资源加载异常
if(err.target instanceof HTMLElement) {
if(err.target instanceof HTMLAnchorElement) {
_url = err.target.href
} else {
// maybe other htmlelement has src property
_url = (err.target as HTMLImageElement).src
}
// 这里进行上报
console.log(err, _url)
}
}, true) // 注意有三个参数,第三个参数表捕获阶段传播到该EventTarget时触发
2、同步代码错误 && 异步代码(setTimeout/setInterval,等);
window.onerror = (message, source, lineno, colno, error) => {
// 这里进行错误上报操作
console.log(message, source, lineno, colno, error)
}
3、异步代码错误;
// promise
window.addEventListener("unhandledrejection", (e) => {
// 这里进行上报
console.log(e.reason)
e.preventDefault() // 阻止错误冒泡
}, true)
4、框架提供的错误处理,e.g.
// vue3
app.config.errorHandler = (err, vm, info) => {
// 这里进行错误上报
console.log(err, vm, info)
}
5、xhr, fetch等异步请求方式;
// xhr
function xhr() {
const oReq = new XMLHttpRequest();
const url = "http://www.example.org/example.txt"
oReq.addEventListener("error", (err) => {
console.log(err, url)
});
oReq.open("GET", url);
oReq.send();
}
// fetch
function fetchMethod() {
const url = 'http://www.example.org/example.txt'
fetch(url,
{
method: 'GET',
mode: 'cors',
cache: 'default'
}
).then(data => {
console.log(data)
}).catch(err => {
// 错误处理
console.log(url, err)
})
}
具体代码可参考 demo;
错误位置
我们的代码一般是打包压缩后的代码,错误提示的位置有时候很难定位到具体的内容,特别是很难重现的错误内容,我们常常需要更精准的错误信息进行定位。
对于以上方案以及对应的处理方式如下
1、rollup-plugin-trycatch插件wrap一层try...catch;
在插件中已对错误记录路径,方法名等信息;
2、远程静态资源;
错误已记录路径信息;
3、promise异步代码错误;
建议自行全部加上catch处理错误;
4、框架内部的错误处理;
框架已提供错误定位信息(vue3, `info` is a Vue-specific error info, e.g. which lifecycle hook);
5、xhr, fetch等;
建议自行记录处理;
5、同步代码错误 && 异步代码(setTimeout/setInterval,等);
其实这个错误是主要错误之一,目前的方案是通过提前打包sourcemap来进行解析
sourcemap解析错误
这里主要介绍下如何实现解析功能(有些服务,e.g. sentry已提供sourcemap的服务,但我们是自己搭建的,所以需要自己来实现这个功能):
1、打包时候将最新的sourcemap覆盖上传到解析服务上(如果有不同版本的查询问题的需求,可以考虑多版本,我暂时没做);
现在大部分公司都是通过自动化工具(jenkins, gitlab等)在打包机进行打包编译,在打包成功后将sourcemap文件上传到解析的服务目录上即可(可以运维通过ssh上传,也可以自行搭建文件上传服务);
2、通过source-map解析文件,返回具体错误位置;
// 需要传入参数,source, lineno, colno
问题记录
1、Error.prototype.stack是实验性功能,在不同浏览器,不同版本有不同的处理方式(包括try...catch, unhandledrejection等error都是Error的实例)。
可以参考stackoverflow兼容主流浏览器(未测试);
另外一种就是像我的plugin中自动化wrap try...catch方法时记录下行信息;对于promise,建议尽量全部通过catch处理(或变成同步代码async await);
2、rollup或者acorn并没有提供ast->code的方法,如何进行转换?
在修改ast后需要通过另外的插件来实现ast->code,较多人在issue里推荐的是escodegen。
3、estree-walker在jest调用时候出现module not found的问题;
用2.0.2版本是ok的, 有issue跟进
4、有一些浏览器支持但rollup尚未支持的实验属性需要慎用。
1、class 私有属性 相关issue: https://github.com/rollup/rollup/issues/4292,可以通过插件@rollup/plugin-typescript转化后使用;
参考文档
1、 React,优雅的捕获异常: juejin.cn/post/697438…
2、Allow plugin transforms to only return AST: github.com/rollup/roll…
3、source-map-demo: github.com/Joeoeoe/sou…