前言
你好,我是若川。这是
学习源码整体架构系列
第四篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。
学习源码整体架构系列
文章如下:
1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
感兴趣的读者可以点击阅读。
导读
本文通过梳理前端错误监控知识、介绍sentry
错误监控原理、sentry
初始化、Ajax
上报、window.onerror、window.onunhandledrejection
几个方面来学习sentry
的源码。
开发微信小程序,想着搭建小程序错误监控方案。最近用了丁香园 开源的Sentry
小程序 SDK
sentry-miniapp。
顺便研究下sentry-javascript
仓库 的源码整体架构,于是有了这篇文章。
本文分析的是打包后未压缩的源码,源码总行数五千余行,链接地址是:browser.sentry-cdn.com/5.7.1/bundl…, 版本是v5.7.1
。
本文示例等源代码在这我的github
博客中github blog sentry,需要的读者可以点击查看,如果觉得不错,可以顺便star
一下。
看源码前先来梳理下前端错误监控的知识。
前端错误监控知识
前端错误的分类
1.即时运行错误:代码错误
try...catch
window.onerror
(也可以用DOM2
事件监听)
2.资源加载错误
object.onerror
: dom
对象的onerror
事件
performance.getEntries()
Error
事件捕获
3.使用
performance.getEntries()
获取网页图片加载错误
var allImgs = document.getElementsByTagName('image')
var loadedImgs = performance.getEntries().filter(i => i.initiatorType === 'img')
最后allIms
和loadedImgs
对比即可找出图片资源未加载项目
Error事件捕获代码示例
1 | 复制代码window.addEventListener('error', function(e) { |
上报错误的基本原理
1.采用Ajax
通信的方式上报
2.利用Image
对象上报 (主流方式)
Image
上报错误方式:(new Image()).src = 'https://lxchuan12.cn/error?name=若川'
Sentry 前端异常监控基本原理
1.重写
window.onerror
方法、重写window.onunhandledrejection
方法
如果不了解onerror和onunhandledrejection
方法的读者,可以看相关的MDN
文档。这里简要介绍一下:
MDN GlobalEventHandlers.onerror
1 | 复制代码window.onerror = function (message, source, lineno, colno, error) { |
参数:
message
:错误信息(字符串)。可用于HTML onerror=""
处理程序中的event
。
source
:发生错误的脚本URL
(字符串)
lineno
:发生错误的行号(数字)
colno
:发生错误的列号(数字)
error
:Error
对象(对象)
当
Promise
被reject
且没有reject
处理器的时候,会触发unhandledrejection
事件;这可能发生在window
下,但也可能发生在Worker
中。 这对于调试回退错误处理非常有用。
Sentry
源码可以搜索 global.onerror
定位到具体位置
1 | 复制代码 GlobalHandlers.prototype._installGlobalOnErrorHandler = function () { |
同样,可以搜索global.onunhandledrejection
定位到具体位置
1 | 复制代码GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function () { |
2.采用
Ajax
上传
支持 fetch
使用 fetch
,否则使用 XHR
。
1 | 复制代码BrowserBackend.prototype._setupTransport = function () { |
2.1
fetch
1 | 复制代码FetchTransport.prototype.sendEvent = function (event) { |
2.2
XMLHttpRequest
1 | 复制代码XHRTransport.prototype.sendEvent = function (event) { |
接下来主要通过Sentry初始化、如何Ajax上报
和window.onerror、window.onunhandledrejection
三条主线来学习源码。
如果看到这里,暂时不想关注后面的源码细节,直接看后文小结1和2的两张图。或者可以点赞或收藏这篇文章,后续想看了再看。
Sentry 源码入口和出口
1 | 复制代码 var Sentry = (function(exports){ |
Sentry.init 初始化 之 init 函数
初始化
1 | 复制代码// 这里的dsn,是sentry.io网站会生成的。 |
1 | 复制代码// options 是 {dsn: '...'} |
getGlobalObject、inNodeEnv 函数
很多地方用到这个函数getGlobalObject
。其实做的事情也比较简单,就是获取全局对象。浏览器中是window
。
1 | 复制代码/** |
继续看 initAndBind
函数
initAndBind 函数之 new BrowserClient(options)
1 | 复制代码function initAndBind(clientClass, options) { |
可以看出 initAndBind()
,第一个参数是 BrowserClient
构造函数,第二个参数是初始化后的options
。
接着先看 构造函数 BrowserClient
。
另一条线 getCurrentHub().bindClient()
先不看。
BrowserClient 构造函数
1 | 复制代码var BrowserClient = /** @class */ (function (_super) { |
从代码中可以看出
:BrowserClient
继承自BaseClient
,并且把BrowserBackend
,options
传参给BaseClient
调用。
先看 BrowserBackend
,这里的BaseClient
,暂时不看。
看BrowserBackend
之前,先提一下继承、继承静态属性和方法。
__extends、extendStatics 打包代码实现的继承
未打包的源码是使用ES6 extends
实现的。这是打包后的对ES6
的extends
的一种实现。
如果对继承还不是很熟悉的读者,可以参考我之前写的文章。面试官问:JS的继承
1 | 复制代码// 继承静态方法和属性 |
不得不说这打包后的代码十分严谨,上面说的我的文章《面试官问:JS
的继承》中没有提到不支持__proto__
的情况。看来这文章可以进一步严谨修正了。
让我想起Vue
源码中对数组检测代理判断是否支持__proto__
的判断。
1 | 复制代码// vuejs 源码:https://github.com/vuejs/vue/blob/dev/dist/vue.js#L526-L527 |
看完打包代码实现的继承,继续看 BrowserBackend
构造函数
BrowserBackend 构造函数 (浏览器后端)
1 | 复制代码var BrowserBackend = /** @class */ (function (_super) { |
BrowserBackend
又继承自 BaseBackend
。
BaseBackend 构造函数 (基础后端)
1 | 复制代码/** |
通过一系列的继承后,回过头来看 BaseClient
构造函数。
BaseClient 构造函数(基础客户端)
1 | 复制代码var BaseClient = /** @class */ (function () { |
小结1. new BrowerClient 经过一系列的继承和初始化
可以输出下具体new clientClass(options)
之后的结果:
1 | 复制代码function initAndBind(clientClass, options) { |
最终输出得到这样的数据。我画了一张图表示。重点关注的原型链用颜色标注了,其他部分收缩了。
sentry new BrowserClient 实例图 By@若川
initAndBind 函数之 getCurrentHub().bindClient()
继续看 initAndBind
的另一条线。
1 | 复制代码function initAndBind(clientClass, options) { |
获取当前的控制中心 Hub
,再把new BrowserClient()
的实例对象绑定在Hub
上。
getCurrentHub 函数
1 | 复制代码// 获取当前Hub 控制中心 |
衍生的函数 getMainCarrier、getHubFromCarrier
1 | 复制代码function getMainCarrier() { |
1 | 复制代码// 获取控制中心 hub 从载体上 |
bindClient 绑定客户端在当前控制中心上
1 | 复制代码Hub.prototype.bindClient = function (client) { |
1 | 复制代码Hub.prototype.getStackTop = function () { |
小结2. 经过一系列的继承和初始化
再回过头来看 initAndBind
函数
1 | 复制代码function initAndBind(clientClass, options) { |
最终会得到这样的Hub
实例对象。笔者画了一张图表示,便于查看理解。
Hub 实例关系图
初始化完成后,再来看具体例子。
具体 captureMessage
函数的实现。
1 | 复制代码Sentry.captureMessage('Hello, 若川!'); |
captureMessage 函数
通过之前的阅读代码,知道会最终会调用Fetch
接口,所以直接断点调试即可,得出如下调用栈。
接下来描述调用栈的主要流程。
captureMessage 断点调试图
调用栈主要流程:
captureMessage
1 | 复制代码function captureMessage(message, level) { |
=> callOnHub
1 | 复制代码/** |
=> Hub.prototype.captureMessage
接着看Hub.prototype
上定义的 captureMessage
方法
1 | 复制代码Hub.prototype.captureMessage = function (message, level, hint) { |
=> Hub.prototype._invokeClient
1 | 复制代码/** |
=> BaseClient.prototype.captureMessage
1 | 复制代码BaseClient.prototype.captureMessage = function (message, level, hint, scope) { |
最后会调用 _processEvent
也就是
=> BaseClient.prototype._processEvent
这个函数最终会调用
1 | 复制代码_this._getBackend().sendEvent(finalEvent); |
也就是
=> BaseBackend.prototype.sendEvent
1 | 复制代码BaseBackend.prototype.sendEvent = function (event) { |
=> FetchTransport.prototype.sendEvent 最终发送了请求
FetchTransport.prototype.sendEvent
1 | 复制代码FetchTransport.prototype.sendEvent = function (event) { |
看完 Ajax 上报
主线,再看本文的另外一条主线 window.onerror
捕获。
window.onerror 和 window.onunhandledrejection 捕获 错误
例子:调用一个未申明的变量。
1 | 复制代码func(); |
Promise
不捕获错误
1 | 复制代码new Promise(() => { |
captureEvent
调用栈主要流程:
window.onerror
1 | 复制代码GlobalHandlers.prototype._installGlobalOnErrorHandler = function () { |
window.onunhandledrejection
1 | 复制代码GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function () { |
共同点:都会调用currentHub.captureEvent
1 | 复制代码currentHub.captureEvent(event, { |
=> Hub.prototype.captureEvent
最终又是调用 _invokeClient
,调用流程跟 captureMessage
类似,这里就不再赘述。
1 | 复制代码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,也可以看打包后出来未压缩的代码。
读源码比较耗费时间,写文章记录下来更加费时间(比如写这篇文章跨度十几天…),但收获一般都比较大。
如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持。万分感谢。
推荐阅读
掘金BlackHole1:JavaScript集成Sentry
丁香园 开源的Sentry
小程序 SDK
sentry-miniapp
笔者往期文章
前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并
关于
作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客-若川,使用vuepress
重构了,阅读体验可能更好些
掘金专栏,欢迎关注~
segmentfault
前端视野专栏,欢迎关注~
知乎前端视野专栏,欢迎关注~
github blog,相关源码和资源都放在这里,求个star
^_^~
欢迎加微信交流 微信公众号
可能比较有趣的微信公众号,长按扫码关注。欢迎加笔者微信ruochuan12
(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~
若川视野
本文使用 mdnice 排版
本文转载自: 掘金