前言
这是学习源码整体架构第四篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。
其余三篇分别是:
1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习underscore源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
导读
本文通过梳理前端错误监控知识、介绍 sentry
错误监控原理、 sentry
初始化、 Ajax
上报、 window.onerror、window.onunhandledrejection
几个方面来学习 sentry
的源码。
开发微信小程序,想着搭建小程序错误监控方案。最近用了丁香园 开源的 Sentry
小程序 SDK
sentry-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,也可以看打包后出来未压缩的代码。