前言
这是学习源码整体架构第四篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。
其余三篇分别是:
1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习underscore源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
导读
本文通过梳理前端错误监控知识、介绍 sentry错误监控原理、 sentry初始化、 Ajax上报、 window.onerror、window.onunhandledrejection几个方面来学习 sentry的源码。
开发微信小程序,想着搭建小程序错误监控方案。最近用了丁香园 开源的 Sentry 小程序 SDKsentry-miniapp。
顺便研究下 sentry-javascript仓库 的源码整体架构,于是有了这篇文章。
本文分析的是打包后未压缩的源码,源码总行数五千余行,链接地址是:https://browser.sentry-cdn.com/5.7.1/bundle.js, 版本是 v5.7.1。
本文示例等源代码在这我的 github博客中github blog sentry,需要的读者可以点击查看,如果觉得不错,可以顺便 star一下。
看源码前先来梳理下前端错误监控的知识。
前端错误监控知识
摘抄自 慕课网视频教程:前端跳槽面试必备技巧
别人做的笔记:前端跳槽面试必备技巧-4-4 错误监控类
前端错误的分类
1.即时运行错误:代码错误
try...catch
window.onerror (也可以用 DOM2事件监听)
2.资源加载错误
object.onerror: dom对象的 onerror事件
performance.getEntries()
Error事件捕获
3.使用 performance.getEntries()获取网页图片加载错误
varallImgs=document.getElementsByTagName('image')
varloadedImgs=performance.getEntries().filter(i=>i.initiatorType==='img')
最后 allIms和 loadedImgs对比即可找出图片资源未加载项目
Error事件捕获代码示例

上报错误的基本原理
1.采用 Ajax通信的方式上报
2.利用 Image对象上报 (主流方式)
Image上报错误方式: (newImage()).src='https://lxchuan12.cn/error?name=若川'
Sentry 前端异常监控基本原理
1.重写 window.onerror 方法、重写 window.onunhandledrejection 方法
如果不了解 onerror和onunhandledrejection方法的读者,可以看相关的 MDN文档。这里简要介绍一下:
MDN GlobalEventHandlers.onerror

参数:
message:错误信息(字符串)。可用于 HTML onerror=""处理程序中的 event。
source:发生错误的脚本 URL(字符串)
lineno:发生错误的行号(数字)
colno:发生错误的列号(数字)
error: Error对象(对象)
MDN unhandledrejection
当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件;这可能发生在 window 下,但也可能发生在 Worker 中。 这对于调试回退错误处理非常有用。
Sentry 源码可以搜索 global.onerror 定位到具体位置

同样,可以搜索 global.onunhandledrejection 定位到具体位置

2.采用 Ajax上传
支持 fetch 使用 fetch,否则使用 XHR。

2.1 fetch

2.2 XMLHttpRequest

接下来主要通过Sentry初始化、如何 Ajax上报和 window.onerror、window.onunhandledrejection三条主线来学习源码。
如果看到这里,暂时不想关注后面的源码细节,直接看后文小结1和2的两张图。或者可以点赞或收藏这篇文章,后续想看了再看。
Sentry 源码入口和出口

Sentry.init 初始化 之 init 函数
初始化

getGlobalObject、inNodeEnv 函数
很多地方用到这个函数 getGlobalObject。其实做的事情也比较简单,就是获取全局对象。浏览器中是 window。

继续看 initAndBind 函数
initAndBind 函数之 new BrowserClient(options)

可以看出 initAndBind(),第一个参数是 BrowserClient 构造函数,第二个参数是初始化后的 options。
接着先看 构造函数 BrowserClient。
另一条线 getCurrentHub().bindClient() 先不看。
BrowserClient 构造函数

从代码中可以看出: BrowserClient 继承自 BaseClient,并且把 BrowserBackend, options传参给 BaseClient调用。
先看 BrowserBackend,这里的 BaseClient,暂时不看。
看 BrowserBackend之前,先提一下继承、继承静态属性和方法。
__extends、extendStatics 打包代码实现的继承
未打包的源码是使用 ES6extends实现的。这是打包后的对 ES6的 extends的一种实现。
1. // 继承静态方法和属性
2. var extendStatics = function(d, b) {
3.  // 如果支持 Object.setPrototypeOf 这个函数,直接使用
4.  // 不支持,则使用原型__proto__ 属性,
5.  // 如何还不支持(但有可能__proto__也不支持,毕竟是浏览器特有的方法。)
6.  // 则使用for in 遍历原型链上的属性,从而达到继承的目的。
7.  extendStatics = Object.setPrototypeOf ||
8.  ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
9.  function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
10.  return extendStatics(d, b);
11. };
12.
13. function __extends(d, b) {
14.  extendStatics(d, b);
15.  // 申明构造函数__ 并且把 d 赋值给 constructor
16.  function __() { this.constructor = d; }
17.  // (__.prototype = b.prototype, new __()) 这种逗号形式的代码,最终返回是后者,也就是 new __()
18.  // 比如 (typeof null, 1) 返回的是1
19.  // 如果 b === null 用Object.create(b) 创建 ,也就是一个不含原型链等信息的空对象 {}
20.  // 否则使用 new __() 返回
21.  d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
22. }
不得不说这打包后的代码十分严谨,上面说的我的文章 面试官问:JS的继承 中没有提到不支持 __proto__的情况。看来这文章可以进一步严谨修正了。
让我想起 Vue源码中对数组检测代理判断是否支持 __proto__的判断。

看完打包代码实现的继承,继续看 BrowserBackend 构造函数
BrowserBackend 构造函数 (浏览器后端)

BrowserBackend 又继承自 BaseBackend。
BaseBackend 构造函数 (基础后端)

通过一系列的继承后,回过头来看 BaseClient 构造函数。
BaseClient 构造函数(基础客户端)

小结1. new BrowerClient 经过一系列的继承和初始化
可以输出下具体 newclientClass(options)之后的结果:

最终输出得到这样的数据。我画了一张图表示。重点关注的原型链用颜色标注了,其他部分收缩了。

initAndBind 函数之 getCurrentHub().bindClient()
继续看 initAndBind 的另一条线。

获取当前的控制中心 Hub,再把 newBrowserClient() 的实例对象绑定在 Hub上。
getCurrentHub 函数

衍生的函数 getMainCarrier、getHubFromCarrier

bindClient 绑定客户端在当前控制中心上

小结2. 经过一系列的继承和初始化
再回过头来看 initAndBind函数

最终会得到这样的 Hub实例对象。笔者画了一张图表示,便于查看理解。

初始化完成后,再来看具体例子。
具体 captureMessage 函数的实现。
Sentry.captureMessage('Hello, 若川!');captureMessage 函数
通过之前的阅读代码,知道会最终会调用 Fetch接口,所以直接断点调试即可,得出如下调用栈。
接下来描述调用栈的主要流程。

调用栈主要流程:
captureMessage

=> callOnHub

=> Hub.prototype.captureMessage
接着看 Hub.prototype 上定义的 captureMessage 方法

=> Hub.prototype._invokeClient

=> BaseClient.prototype.captureMessage

最后会调用 _processEvent 也就是
=> BaseClient.prototype._processEvent
这个函数最终会调用
_this._getBackend().sendEvent(finalEvent);
也就是
=> BaseBackend.prototype.sendEvent

=> FetchTransport.prototype.sendEvent 最终发送了请求
FetchTransport.prototype.sendEvent

看完 Ajax上报 主线,再看本文的另外一条主线 window.onerror 捕获。
window.onerror 和 window.onunhandledrejection 捕获 错误
例子:调用一个未申明的变量。
func();
Promise 不捕获错误

captureEvent
调用栈主要流程:
window.onerror

window.onunhandledrejection

共同点:都会调用 currentHub.captureEvent

=> Hub.prototype.captureEvent
最终又是调用 _invokeClient ,调用流程跟 captureMessage 类似,这里就不再赘述。
this._invokeClient('captureEvent')=> Hub.prototype._invokeClient
=> BaseClient.prototype.captureEvent
=> BaseClient.prototype._processEvent
=> BaseBackend.prototype.sendEvent
=> FetchTransport.prototype.sendEvent
最终同样是调用了这个函数发送了请求。
可谓是殊途同归,行文至此就基本已经结束,最后总结一下。
总结
Sentry-JavaScript源码高效利用了 JS的原型链机制。可谓是惊艳,值得学习。
本文通过梳理前端错误监控知识、介绍 sentry错误监控原理、 sentry初始化、 Ajax上报、 window.onerror、window.onunhandledrejection几个方面来学习 sentry的源码。还有很多细节和构造函数没有分析。
总共的构造函数(类)有25个,提到的主要有9个,分别是: Hub、BaseClient、BaseBackend、BaseTransport、FetchTransport、XHRTransport、BrowserBackend、BrowserClient、GlobalHandlers。
其他没有提到的分别是 SentryError、Logger、Memo、SyncPromise、PromiseBuffer、Span、Scope、Dsn、API、NoopTransport、FunctionToString、InboundFilters、TryCatch、Breadcrumbs、LinkedErrors、UserAgent。
这些构造函数(类)中还有很多值得学习,比如同步的 Promise(SyncPromise)。
有兴趣的读者,可以看这一块官方仓库中采用 typescript写的源码SyncPromise,也可以看打包后出来未压缩的代码。










