配合源码阅读体验更佳。

最近收到用户吐槽 @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的优势有两点:

  1. 不侵入SDK原本逻辑,接入成本很低,只需要几行代码;
  2. TypeScript将Decorator编译为ES5语法之后有固定的格式,可以方便地在Error堆栈中找出对应的代码行,为精简Error堆栈提供便利。

写到这里其实大体的思路就定型了,步骤如下:

  1. 给API添加Decorator;
  2. 在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错误堆栈的更多相关文章

  1. 利用Image对象,建立Javascript前台错误日志记录

    手记:摘自Javascript高级程序设计(第三版),利用Image对象发送请求,确实有很多优点,有时候这也许就是一个创意点,再次做个笔记供自己和大家参考. 原文: 开发 Web 应用程序过程中的一种 ...

  2. 利用模板将HTML从JavaScript中抽离

    利用模板将HTML从JavaScript中抽离 一.当需要注入大段的HTML标签到页面中时,应该使用服务器渲染(从服务器加载HTML标签) 该方法将模板放置于服务器中使用XMLHttpRequest对 ...

  3. JavaScript错误/异常处理

    JavaScript Try...Catch 语句 介绍:JavaScript中的try...carch语句的作用和C#中的try...catch语句的作用一样, 都是捕获并处理异常. 语法: try ...

  4. javascript错误处理与调试(转)

    JavaScript 在错误处理调试上一直是它的软肋,如果脚本出错,给出的提示经常也让人摸不着头脑. ECMAScript 第 3 版为了解决这个问题引入了 try...catch 和 throw 语 ...

  5. JavaScript错误处理

    JavaScript 错误 - Throw.Try 和 Catch JavaScript 测试和捕捉 try 语句允许我们定义在执行时进行错误测试的代码块. catch 语句允许我们定义当 try 代 ...

  6. 第一百二十三节,JavaScript错误处理与调试

    JavaScript错误处理与调试 学习要点: 1.浏览器错误报告 2.错误处理 3.错误事件 4.错误处理策略 5.调试技术 6.调试工具 JavaScript在错误处理调试上一直是它的软肋,如果脚 ...

  7. 【转】Javascript错误处理——try…catch

    无论我们编程多么精通,脚本错误怎是难免.可能是我们的错误造成,或异常输入,错误的服务器端响应以及无数个其他原因. 通常,当发送错误时脚本会立刻停止,打印至控制台. 但try...catch语法结构可以 ...

  8. [置顶] 利用Global.asax的Application_Error实现错误记录,错误日志

    利用Global.asax的Application_Error实现错误记录 错误日志 void Application_Error(object sender, EventArgs e) { // 在 ...

  9. 工作经验:Java 系统记录调用日志,并且记录错误堆栈

    前言:现在有一个系统,主要是为了给其他系统提供数据查询接口的,这个系统上线不会轻易更新,更不会跟随业务系统的更新而更新(这也是有一个数据查询接口系统的原因,解耦).这时,这个系统就需要有一定的方便的线 ...

随机推荐

  1. electron-react-umi模板

    electron-react-umi-tpl github English Version 更新日志: 2020-06-08 添加全量更新功能 2020-06-29 添加远程增量更新功能,无需下载包来 ...

  2. 【NOIP2017】跳房子 题解(单调队列优化线性DP)

    前言:把鸽了1个月的博客补上 ----------------- 题目链接 题目大意:机器人的灵敏性为$d$.每次可以花费$g$个金币来改造机器人,那么机器人向右跳的范围为$[min(d-g,1),m ...

  3. iOS开发实战之搜索控制器UISearchController使用

    当tableView中的数据过多的时候,在tableView上加一个搜索框就变的很必要了,本文就讨论搜索控制器的使用,以及谓词的简单实现. .m文件中代码如下 添加搜索控制器的各种协议 <UIS ...

  4. Android 给服务器发送网络请求

    今天听得有点蒙,因为服务器的问题,这边建立服务器的话,学长用的是Idea建立的Spring之类的方法去搞服务器. 然后就是用Android去给这个服务器发送请求,大致效果还是懂的,就是像网站发送请求, ...

  5. Android后台数据接口交互实现注册功能

    首先,在ecplise里面新建一个叫做TestServices的web工程.在WebContent--WEB-INF--libs文件夹下导入两个jar包:mysql-connector-java-6. ...

  6. C++/C socket编程

    目录 socket()函数 何为socket Internet套接字 流格式套接字SOCK_STREAM 数据报格式套接字SOCK_DGRAM TCP/IP协议族 创建套接字 加载套接字库 Windo ...

  7. Python中对象实例的__dict__属性

    实例的__dict__并不是一个方法,而是存储与该实例相关的实例属性的字典,对类中定义的方法(函数),方法名也是属性变量,类的__dict__存储所有实例共享的变量和函数(类属性,方法等),类的__d ...

  8. 2020-05-21:es底层读写原理?倒排索引原理?

    福哥答案2020-05-21: es不熟悉,答案仅供参考:es写数据过程1.客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点)2.coordinatin ...

  9. Qt 信号发射部分 undefined reference to错误

    在使用信号与槽很容易发生 undefined reference to 发射信号  ①继承QObject ②添加Q_OBJECT ③执行qmake ④构建 然后就可以运行啦!但是不知道是为什么,悄咪咪 ...

  10. js中几种常用的数组处理方法的总结

    一.filter()用法 功能:用于筛选数组中满足条件的元素,返回一个筛选后的新数组. <script> $(function(){ var arr = [1,-2,3,4,-5]; va ...