利用Decorator和SourceMap优化JavaScript错误堆栈
配合源码阅读体验更佳。
最近收到用户吐槽 @cloudbase/js-sdk(云开发Cloudbase的JavaScript SDK)的报错信息不够清晰,比如下面这条报错:

这属于业务型报错,对于熟悉云开发能力细节的用户一眼就能看出错误的症结出在安全规则配置上,但是对于刚接触云开发的新用户或者之前没有遇到类似问题的用户来说,看到这样简短的错误信息肯定会一头雾水,分不清楚到底是业务报错还是代码写的不对。所以大部分人的第一反应是按照Error的堆栈信息进行debug,试图找到抛出Error的具体代码。然后就会遇到另一个让人头疼的问题:Error堆栈太深了,要想找到是哪一行代码引起的报错并不是一件很容易的事。
虽然云开发是一款toB的产品,相对来说B端开发者的容忍度会「略」高于C端用户,但是糟糕的开发体验肯定是会拉低开发者对产品的好感和认可度。所以优化报错信息成了一件必须要做的事情。
在详述优化方案之前,先看一下最终的优化效果:

图中打印的错误跟第一张图是同一个,代表当前的登录类型受到函数的安全规则限制,导致没有调用函数的权限。错误信息分为两部分:
- 上半部分的黑色字体提示包含了后端 API 返回的错误信息以及针对此类问题的一些解决方案建议;
- 下半部分的红色字体是经优化后的错误堆栈,第一条直接定位到 SDK 源码(
index.ts),第二条直接定位到调用报错 API 的业务源码(App.callFn)。
看到index.ts这样的信息估计大部分人都明白这里用到了SourceMap。确实SourceMap是支撑这套优化方案的必备要素,借助SourceMap可以定位到SDK的源码。但只有SourceMap是不够的,优化的核心点在于:如何把原始错误冗长的堆栈中直接定位到关键代码行?
这就是优化的目标。
有了目标之后的第一步要做的不是立即去扣实现细节,而是设计整体方案,包括两部分:
- 第一是确定优化的对象。
是不是所有的类型的报错堆栈都需要优化?答案是否定的。优化的对象应该是业务报错,具体到代码就是SDK的public API。其他类型的错误(比如SDK自身的语法错误)是应该在发布SDK之前开发团队自测解决的,不应该被带给用户。只针对业务报错这一前提给优化方案一个基调:所有的错误信息格式是固定的(如果做不到这一点就说明SDK不合格)。 - 第二是确定接入方式。
优化的目的是改善体验,必须做到一点:不侵入SDK的原本逻辑。这个前提堵死了一条最容易也是最笨的路:直接改SDK的API代码,对所有的关键代码块加一层try-catch。所以接入的方式必然是一种类似插件的机制,并且成本低、可定制。
除了以上两点之外,还有一个重要的问题需要提前确定:Error应该在SDK代码的什么位置抛出?举个例子,当业务代码调用SDK提供的callFunction API后,SDK内部再发起网络请求之前有一些前序逻辑,比如判断入参是否正确、获取本地登录态信息等等。如果不做任何处理的话,当发生错误时抛出的Error堆栈是最内层的代码行,如下图:

但是用户关心的只是callFunction成功还是失败,不会在意这个API内部是如何工作的,内层的Error堆栈对于用户来说没有任何帮助甚至由于加深了堆栈层级反而加重了debug难度。所以期望最佳的效果是由callFunction所在的代码行抛出Error,最笨的实现方案就是为callFunction的逻辑块整体包一层try-catch统一抛出Error,但可惜这条路已经被堵死了。
那么剩下的唯一办法就是精简由内层逻辑抛出的Error的堆栈,把内层逻辑的堆栈全部剔除,只保留到最外层的callFunction。
梳理一下上面的内容可以得出优化方案的关键信息:
| 选项 | 说明 |
|---|---|
| 优化对象 | 只针对业务型逻辑报错,错误格式固定 |
| 接入方式 | 不侵入SDK原本逻辑,使用类似插件的机制 |
| 预期目标 | 精简Error堆栈,剔除无用条目直接定位到 SDK 的 API代码行 |
精简Error堆栈的基本思路是在SDK的API代码块内捕获内层逻辑抛出的Error,然后重新new一个Error对象抛出,这种方式可以将内层逻辑的堆栈全部消除。实现方式也很简单,在API代码块内用try-catch包装内存逻辑即可,但这样会涉及修改API原本逻辑,而且工作量也不小,所以行不通。
即不侵入API原本逻辑,又能够影响API的表现,首先想到的便是装饰器Decorator。
Decorator
Decorator的优势有两点:
- 不侵入SDK原本逻辑,接入成本很低,只需要几行代码;
- TypeScript将Decorator编译为ES5语法之后有固定的格式,可以方便地在Error堆栈中找出对应的代码行,为精简Error堆栈提供便利。
写到这里其实大体的思路就定型了,步骤如下:
- 给API添加Decorator;
- 在Decorator内将API重新赋值,保持原本逻辑的前提下,为原本逻辑包装
try-catch。
大致代码如下:
function catchErrorsDecorator(options){
return function(
target: any,
methodName: string,
descriptor: TypedPropertyDescriptor<Function>
){
const fn = descriptor.value;
// 重新被装饰的API原本逻辑
descriptor.value = function(...args:any[]) {
try {
return fn.apply(this, args);
} catch (err) {
throw err;
}
}
}
}
然后为API添加装饰器:
class Cloudbase {
@catchErrorsDecorator({
// ...options
})
public init(){
// ...
}
}
这样修改后调用API的行为方式被修改为执行Decorator的逻辑。但是在Decorator的catch代码块中抛出的Error对象没有经过任何处理,仍然是API抛出的Error对象,也就是说同样携带着API内层逻辑的堆栈信息。接下来的工作就是想办法把堆栈信息精简。
精简Error堆栈
首先缕一下当附加Decorator的API被调用时的堆栈顺序,同样是以上文提到的callFunction为例,当外层业务逻辑调用这个API时整体的链路如下图所示:

这只是源码的链路,实际上使用TypeScript或ES6语法编写的源码需要经过语法转换或者引入polyfill才能在浏览器中运行,所以实际上的链路长度远远大于上图,尤其是async函数(因为目前的语法转译通常会把async/await转化为generator)。这也是造成错误堆栈层次太深的主要原因之一。
上文提到的catchErrorsDecorator的工作分两步:
- 第一步是Decorator自身的逻辑,也就是复写API原本逻辑的代码块,这一步是给API添加Decorator之后立即执行的;
- 第二步是当外层逻辑调用
callFunction之后,执行descriptor.value内部逻辑。
这两个步骤并不是连续的,而是分属于两条链路,第一条发生在SDK初始化时,第二条发生在外层逻辑调用API时。
在SDK初始化的链路内,Decorator的第一步逻辑的前序环节是初始化被装饰的API,所以在这里可以拿到原API的源码行,可以借助Error.stack取到,如下:
/**
* decorate在stack中一般都特定的规范
*/
const REG_STACK_DECORATE = isFirefox ?
/(\.js\/)?__decorate(\$\d+)?<@.*\d$/ :
/(\/\w+\.js\.)?__decorate(\$\d+)?\s*\(.*\)$/;
const REG_STACK_LINK = /https?\:\/\/.+\:\d*\/.*\.js\:\d+\:\d+/;
function catchErrorsDecorator(options){
return function(
target: any,
methodName: string,
descriptor: TypedPropertyDescriptor<Function>
){
let sourceLink = '';
const outterErrStacks = (new Error()).stack.split('\n');
const indexOfDecorator = outterErrStacks.findIndex(str=>REG_STACK_DECORATE.test(str));
if(indexOfDecorator!==-1){
const match = REG_STACK_LINK.exec(outterErrStacks[indexOfDecorator+1]||'');
sourceLink = match?match[0]:'';
}
const fn = descriptor.value;
// 重新被装饰的API原本逻辑
descriptor.value = function(...args:any[]) {
const innerErr = getRewritedError({
err: new Error(),
className,
methodName: fnName,
sourceLink
})
try {
return fn.apply(this, args);
} catch (err) {
throw err;
}
}
}
}
之所以把获取原API代码行的逻辑放在Decorator的第一步,是由于此时距离原API的堆栈层数比较浅,而如果放到第二步(即descriptor.value内部)获取,则有可能由于堆栈太深取不到。
这里需要说明的一点,获取原API代码行是通过匹配Error.stack信息。调用throw Error或console.error后在浏览器的控制台打印的堆栈是完整的,但是浏览器在返回Error.stack信息时并不是将全部的堆栈返回,而是只返回最前列的几条,一般是5-10条。这也是为何将获取原API代码行的逻辑放在descriptor.value外执行的主要原因。
另外在上述代码中添加了如下一段逻辑:
const innerErr = getRewritedError({
err: new Error(),
className,
methodName: fnName,
sourceLink
})
其中工具函数getRewritedError的作用是在Error.stack中找到执行descriptor.value的前一条信息,这条信息便是外层逻辑调用callFunction时执行被复写的callFunction API的代码行,而这条信息之前(Error堆栈是倒序排列)的所有堆栈都是callFunction的内层逻辑,是要被剔除的无用信息。
getRewritedError函数的代码比较长就不写了,感兴趣的可以去看源码。
接下来的工作就简单了,从Error.stack中过滤无用的信息,然后把descriptor.value条目的链接替换为先前拿到的原API代码行,最后new一个Error对象将其stack替换为处理之后的在抛出即可。
边角料工作
截止到这里,优化工作的核心内容就已经完成了,剩下的就是完善一下逻辑支持更丰富的场景,比如:
- 支持同步和异步两种模式;
- 用
console.group打印错误信息和解决方案建议; - 兼容多种构建工具(Webpack和Rollup,不同的构建工具混淆后的Decorator堆栈有略微差异);
- 兼容多种浏览器(不同浏览器内核的堆栈格式有差异)
等等。这些小事就不写了,感兴趣的可以去阅读源码。
最终的接入方式就是import这个Decorator,然后为API添加装饰器,如下:
class Cloudbase {
@catchErrorsDecorator({
//同步模式
mode: 'sync',
// title和message是错误提示信息,可定制
title: 'Cloudbase 初始化失败',
messages: [
'请确认以下各项:',
' 1 - 调用 cloudbase.init() 的语法或参数是否正确',
' 2 - 如果是非浏览器环境,是否配置了安全应用来源(https://docs.cloudbase.net/api-reference/webv2/adapter.html#jie-ru-liu-cheng)',
`如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`
]
})
public init(options){
// ...
}
}
最后值得一提的是,这种优化方案只支持在开发环境下使用,一是因为逻辑比较繁琐,带入到生产环境中会产生不必要的资源消耗;二是由于生产环境的js通常是所有模块打包到一起并且经过混淆,造成堆栈信息难以定位。
利用Decorator和SourceMap优化JavaScript错误堆栈的更多相关文章
- 利用Image对象,建立Javascript前台错误日志记录
手记:摘自Javascript高级程序设计(第三版),利用Image对象发送请求,确实有很多优点,有时候这也许就是一个创意点,再次做个笔记供自己和大家参考. 原文: 开发 Web 应用程序过程中的一种 ...
- 利用模板将HTML从JavaScript中抽离
利用模板将HTML从JavaScript中抽离 一.当需要注入大段的HTML标签到页面中时,应该使用服务器渲染(从服务器加载HTML标签) 该方法将模板放置于服务器中使用XMLHttpRequest对 ...
- JavaScript错误/异常处理
JavaScript Try...Catch 语句 介绍:JavaScript中的try...carch语句的作用和C#中的try...catch语句的作用一样, 都是捕获并处理异常. 语法: try ...
- javascript错误处理与调试(转)
JavaScript 在错误处理调试上一直是它的软肋,如果脚本出错,给出的提示经常也让人摸不着头脑. ECMAScript 第 3 版为了解决这个问题引入了 try...catch 和 throw 语 ...
- JavaScript错误处理
JavaScript 错误 - Throw.Try 和 Catch JavaScript 测试和捕捉 try 语句允许我们定义在执行时进行错误测试的代码块. catch 语句允许我们定义当 try 代 ...
- 第一百二十三节,JavaScript错误处理与调试
JavaScript错误处理与调试 学习要点: 1.浏览器错误报告 2.错误处理 3.错误事件 4.错误处理策略 5.调试技术 6.调试工具 JavaScript在错误处理调试上一直是它的软肋,如果脚 ...
- 【转】Javascript错误处理——try…catch
无论我们编程多么精通,脚本错误怎是难免.可能是我们的错误造成,或异常输入,错误的服务器端响应以及无数个其他原因. 通常,当发送错误时脚本会立刻停止,打印至控制台. 但try...catch语法结构可以 ...
- [置顶] 利用Global.asax的Application_Error实现错误记录,错误日志
利用Global.asax的Application_Error实现错误记录 错误日志 void Application_Error(object sender, EventArgs e) { // 在 ...
- 工作经验:Java 系统记录调用日志,并且记录错误堆栈
前言:现在有一个系统,主要是为了给其他系统提供数据查询接口的,这个系统上线不会轻易更新,更不会跟随业务系统的更新而更新(这也是有一个数据查询接口系统的原因,解耦).这时,这个系统就需要有一定的方便的线 ...
随机推荐
- HDU 1756 Cupid's Arrow 计算几何 判断一个点是否在多边形内
LINK:Cupid's Arrow 前置函数 atan2 返回一个向量的幅角.范围为[Pi,-Pi) 值得注意的是 返回的是 相对于x轴正半轴的辐角. 而判断一个点是否在一个多边形内 通常有三种方法 ...
- loj #6039 「雅礼集训 2017 Day5」珠宝 分组背包 决策单调性优化
LINK:珠宝 去年在某个oj上写过这道题 当时懵懂无知wa的不省人事 终于发现这个东西原来是有决策单调性的. 可以发现是一个01背包 但是过不了 冷静分析 01背包的复杂度有下界 如果过不了说明必然 ...
- 【JSOI2007】文本生成器 题解(AC自动机+动态规划)
题目链接 题目大意:给定$n$个子串,要求构造一个长度为$m$的母串使得至少有一个子串是其子串.问方案数. ------------------------ 我们可以对要求进行转化:求出不合法的方案数 ...
- 树形DP 学习笔记(树形DP、树的直径、树的重心)
前言:寒假讲过树形DP,这次再复习一下. -------------- 基本的树形DP 实现形式 树形DP的主要实现形式是$dfs$.这是因为树的特殊结构决定的——只有确定了儿子,才能决定父亲.划分阶 ...
- TF签名为什么这么稳定?TF签名找微导流!
TF签名作为目前最稳定的签名方式收到了业界开发者们的认可,而在如今鱼龙混杂的签名平台中,应该如何选择客厅的TF签名平台呢?下面就一起来看看TF签名为什么这么稳定?TF签名找微导流! TF签名的 ...
- JSP中contentType、pageEncoding和meta charset的区别
1.创建JSP 使用Eclipse创建JSP文件: <%@ page language="java" contentType="text/html; charset ...
- PyTorch上路
PyTorch torch.autograd模块 深度学习的算法本质上是通过反向传播求导数, PyTorch的autograd模块实现了此功能, 在Tensor上的所有操作, autograd都会为它 ...
- ROS 八叉树地图构建 - 使用 octomap_server 建图过程总结!
构建语义地图时,最开始用的是 octomap_server,后面换成了 semantic_slam: octomap_generator,不过还是整理下之前的学习笔记. 一.增量构建八叉树地图步骤 为 ...
- [算法入门]——深度优先搜索(DFS)
深度优先搜索(DFS) 深度优先搜索叫DFS(Depth First Search).OK,那么什么是深度优先搜索呢?_? 样例: 举个例子,你在一个方格网络中,可以简单理解为我们的地图,要从A点到B ...
- Eclipse怎么切换工作空间
1.进行点击Eclipse编辑代码的窗口界面中,进行点击菜单中file的选项. 2.弹出了下拉菜单中进行选择为“switch workspace”的选项. 3.弹出了下一级菜单中进行选择为other的 ...