這是學習源碼整體架構第四篇。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是代碼整體結構,不深究其他不是主線的具體函數的實現。文章學習的是打包整合後的代碼,不是實際倉庫中的拆分的代碼。
其餘三篇分別是:
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事件捕獲代碼示例window.addEventListener('error', function(e) {
console.log('捕獲', e)
}, true) // 這裡只有捕獲才能觸發事件,冒泡是不能觸發
上報錯誤的基本原理1.採用 Ajax通信的方式上報
2.利用 Image對象上報 (主流方式)
Image上報錯誤方式:(newImage()).src='https://lxchuan12.cn/error?name=若川'
Sentry 前端異常監控基本原理1.重寫 window.onerror 方法、重寫 window.onunhandledrejection方法
如果不了解 onerror和onunhandledrejection方法的讀者,可以看相關的 MDN文檔。這裡簡要介紹一下:
MDN GlobalEventHandlers.onerror
window.onerror = function(message, source, lineno, colno, error) {
console.log('message, source, lineno, colno, error', message, source, lineno, colno, error);
}
參數:
message:錯誤信息(字符串)。可用於 HTML onerror=""處理程序中的 event。
source:發生錯誤的腳本 URL(字符串)
lineno:發生錯誤的行號(數字)
colno:發生錯誤的列號(數字)
error:Error對象(對象)
MDN unhandledrejection
當 Promise 被 reject 且沒有 reject 處理器的時候,會觸發 unhandledrejection 事件;這可能發生在 window 下,但也可能發生在 Worker 中。這對於調試回退錯誤處理非常有用。
Sentry 源碼可以搜索 global.onerror 定位到具體位置
GlobalHandlers.prototype._installGlobalOnErrorHandler = function() {
// 代碼有刪減
// 這裡的 this._global 在瀏覽器中就是 window
this._oldOnErrorHandler = this._global.onerror;
this._global.onerror = function(msg, url, line, column, error) {}
// code ...
}
同樣,可以搜索 global.onunhandledrejection 定位到具體位置
GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function() {
// 代碼有刪減
this._oldOnUnhandledRejectionHandler = this._global.onunhandledrejection;
this._global.onunhandledrejection = function(e) {}
}
2.採用 Ajax上傳
支持 fetch 使用 fetch,否則使用 XHR。
BrowserBackend.prototype._setupTransport = function() {
// 代碼有刪減
if(supportsFetch()) {
returnnewFetchTransport(transportOptions);
}
returnnewXHRTransport(transportOptions);
};
2.1 fetch
FetchTransport.prototype.sendEvent = function(event) {
var defaultOptions = {
body: JSON.stringify(event),
method: 'POST',
referrerPolicy: (supportsReferrerPolicy() ? 'origin': ''),
};
returnthis._buffer.add(global$2.fetch(this.url, defaultOptions).then(function(response) { return({
status: exports.Status.fromHttpCode(response.status),
}); }));
};
2.2 XMLHttpRequest
XHRTransport.prototype.sendEvent = function(event) {
var _this = this;
returnthis._buffer.add(newSyncPromise(function(resolve, reject) {
// 熟悉的 XMLHttpRequest
var request = newXMLHttpRequest();
request.onreadystatechange = function() {
if(request.readyState !== 4) {
return;
}
if(request.status === 200) {
resolve({
status: exports.Status.fromHttpCode(request.status),
});
}
reject(request);
};
request.open('POST', _this.url);
request.send(JSON.stringify(event));
}));
}
接下來主要通過Sentry初始化、如何 Ajax上報和 window.onerror、window.onunhandledrejection三條主線來學習源碼。
如果看到這裡,暫時不想關注後面的源碼細節,直接看後文小結1和2的兩張圖。或者可以點讚或收藏這篇文章,後續想看了再看。
Sentry 源碼入口和出口varSentry= (function(exports){
// code ...
var SDK_NAME = 'sentry.javascript.browser';
var SDK_VERSION = '5.7.1';
// code ...
// 省略了導出的Sentry的若干個方法和屬性
// 只列出了如下幾個
exports.SDK_NAME = SDK_NAME;
exports.SDK_VERSION = SDK_VERSION;
// 重點關注 captureMessage
exports.captureMessage = captureMessage;
// 重點關注 init
exports.init = init;
return exports;
}({}));
Sentry.init 初始化 之 init 函數初始化
// 這裡的dsn,是sentry.io網站會生成的。
Sentry.init({ dsn: 'xxx'});
// options 是 {dsn: '...'}
function init(options) {
// 如果options 是undefined,則賦值為 空對象
if(options === void0) { options = {}; }
// 如果沒傳 defaultIntegrations 則賦值默認的
if(options.defaultIntegrations === undefined) {
options.defaultIntegrations = defaultIntegrations;
}
// 初始化語句
if(options.release === undefined) {
var window_1 = getGlobalObject();
// 這是給 sentry-webpack-plugin 插件提供的,webpack插件注入的變量。這裡沒用這個插件,所以這裡不深究。
// This supports the variable that sentry-webpack-plugin injects
if(window_1.SENTRY_RELEASE && window_1.SENTRY_RELEASE.id) {
options.release = window_1.SENTRY_RELEASE.id;
}
}
// 初始化並且綁定
initAndBind(BrowserClient, options);
}
getGlobalObject、inNodeEnv 函數很多地方用到這個函數 getGlobalObject。其實做的事情也比較簡單,就是獲取全局對象。瀏覽器中是 window。
/**
* 判斷是否是node環境
* Checks whether we're in the Node.js or Browser environment
*
* @returns Answer to given question
*/
function isNodeEnv() {
// tslint:disable:strict-type-predicates
returnObject.prototype.toString.call(typeof process !== 'undefined'? process : 0) === '[object process]';
}
var fallbackGlobalObject = {};
/**
* Safely get global scope object
*
* @returns Global scope object
*/
function getGlobalObject() {
return(isNodeEnv()
// 是 node 環境 賦值給 global
? global
: typeof window !== 'undefined'
? window
// 不是 window self 不是undefined 說明是 Web Worker 環境
: typeof self !== 'undefined'
? self
// 都不是,賦值給空對象。
: fallbackGlobalObject);
繼續看 initAndBind 函數
initAndBind 函數之 new BrowserClient(options)function initAndBind(clientClass, options) {
// 這裡沒有開啟debug模式,logger.enable() 這句不會執行
if(options.debug === true) {
logger.enable();
}
getCurrentHub().bindClient(new clientClass(options));
}
可以看出 initAndBind(),第一個參數是 BrowserClient 構造函數,第二個參數是初始化後的 options。接著先看 構造函數 BrowserClient。另一條線 getCurrentHub().bindClient() 先不看。
BrowserClient 構造函數varBrowserClient= /** @class */(function(_super) {
// `BrowserClient` 繼承自`BaseClient`
__extends(BrowserClient, _super);
/**
* Creates a new Browser SDK instance.
*
* @param options Configuration options for this SDK.
*/
functionBrowserClient(options) {
if(options === void0) { options = {}; }
// 把`BrowserBackend`,`options`傳參給`BaseClient`調用。
return _super.call(this, BrowserBackend, options) || this;
}
returnBrowserClient;
}(BaseClient));
從代碼中可以看出:BrowserClient 繼承自 BaseClient,並且把 BrowserBackend, options傳參給 BaseClient調用。
先看 BrowserBackend,這裡的 BaseClient,暫時不看。
看 BrowserBackend之前,先提一下繼承、繼承靜態屬性和方法。
__extends、extendStatics 打包代碼實現的繼承未打包的源碼是使用 ES6extends實現的。這是打包後的對 ES6的 extends的一種實現。
如果對繼承還不是很熟悉的讀者,可以參考我之前寫的文章。面試官問:JS的繼承
// 繼承靜態方法和屬性
var extendStatics = function(d, b) {
// 如果支持 Object.setPrototypeOf 這個函數,直接使用
// 不支持,則使用原型__proto__ 屬性,
// 如何還不支持(但有可能__proto__也不支持,畢竟是瀏覽器特有的方法。)
// 則使用for in 遍歷原型鏈上的屬性,從而達到繼承的目的。
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceofArray&& function(d, b) { d.__proto__ = b; }) ||
function(d, b) { for(var p in b) if(b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
// 申明構造函數__ 並且把 d 賦值給 constructor
function __() { this.constructor = d; }
// (__.prototype = b.prototype, new __()) 這種逗號形式的代碼,最終返回是後者,也就是 new __()
// 比如 (typeof null, 1) 返回的是1
// 如果 b === null 用Object.create(b) 創建 ,也就是一個不含原型鏈等信息的空對象 {}
// 否則使用 new __() 返回
d.prototype = b === null? Object.create(b) : (__.prototype = b.prototype, new __());
}
不得不說這打包後的代碼十分嚴謹,上面說的我的文章 面試官問:JS的繼承 中沒有提到不支持 __proto__的情況。看來這文章可以進一步嚴謹修正了。讓我想起 Vue源碼中對數組檢測代理判斷是否支持 __proto__的判斷。
// vuejs 源碼:https://github.com/vuejs/vue/blob/dev/dist/vue.js#L526-L527
// can we use __proto__?
var hasProto = '__proto__' in {};
看完打包代碼實現的繼承,繼續看 BrowserBackend 構造函數
BrowserBackend 構造函數 (瀏覽器後端)varBrowserBackend= /** @class */(function(_super) {
__extends(BrowserBackend, _super);
functionBrowserBackend() {
return _super !== null&& _super.apply(this, arguments) || this;
}
/**
* 設置請求
*/
BrowserBackend.prototype._setupTransport = function() {
if(!this._options.dsn) {
// We return the noop transport here in case there is no Dsn.
// 沒有設置dsn,調用BaseBackend.prototype._setupTransport 返回空函數
return _super.prototype._setupTransport.call(this);
}
var transportOptions = __assign({}, this._options.transportOptions, { dsn: this._options.dsn });
if(this._options.transport) {
returnnewthis._options.transport(transportOptions);
}
// 支持Fetch則返回 FetchTransport 實例,否則返回 XHRTransport實例,
// 這兩個構造函數具體代碼在開頭已有提到。
if(supportsFetch()) {
returnnewFetchTransport(transportOptions);
}
returnnewXHRTransport(transportOptions);
};
// code ...
returnBrowserBackend;
}(BaseBackend));
BrowserBackend 又繼承自 BaseBackend。
BaseBackend 構造函數 (基礎後端)/**
* This is the base implemention of a Backend.
* @hidden
*/
varBaseBackend= /** @class */(function() {
/** Creates a new backend instance. */
functionBaseBackend(options) {
this._options = options;
if(!this._options.dsn) {
logger.warn('No DSN provided, backend will not do anything.');
}
// 調用設置請求函數
this._transport = this._setupTransport();
}
/**
* Sets up the transport so it can be used later to send requests.
* 設置發送請求空函數
*/
BaseBackend.prototype._setupTransport = function() {
returnnewNoopTransport();
};
// code ...
BaseBackend.prototype.sendEvent = function(event) {
this._transport.sendEvent(event).then(null, function(reason) {
logger.error("Error while sending event: "+ reason);
});
};
BaseBackend.prototype.getTransport = function() {
returnthis._transport;
};
returnBaseBackend;
}());
通過一系列的繼承後,回過頭來看 BaseClient 構造函數。
BaseClient 構造函數(基礎客戶端)varBaseClient= /** @class */(function() {
/**
* Initializes this client instance.
*
* @param backendClass A constructor function to create the backend.
* @param options Options for the client.
*/
functionBaseClient(backendClass, options) {
/** Array of used integrations. */
this._integrations = {};
/** Is the client still processing a call? */
this._processing = false;
this._backend = new backendClass(options);
this._options = options;
if(options.dsn) {
this._dsn = newDsn(options.dsn);
}
if(this._isEnabled()) {
this._integrations = setupIntegrations(this._options);
}
}
// code ...
returnBaseClient;
}());
小結1. new BrowerClient 經過一系列的繼承和初始化可以輸出下具體 newclientClass(options)之後的結果:
function initAndBind(clientClass, options) {
if(options.debug === true) {
logger.enable();
}
var client = new clientClass(options);
console.log('new clientClass(options)', client);
getCurrentHub().bindClient(client);
// 原來的代碼
// getCurrentHub().bindClient(new clientClass(options));
}
最終輸出得到這樣的數據。我畫了一張圖表示。重點關注的原型鏈用顏色標註了,其他部分收縮了。
繼續看 initAndBind 的另一條線。
function initAndBind(clientClass, options) {
if(options.debug === true) {
logger.enable();
}
getCurrentHub().bindClient(new clientClass(options));
}
獲取當前的控制中心 Hub,再把 newBrowserClient() 的實例對象綁定在 Hub上。
getCurrentHub 函數// 獲取當前Hub 控制中心
function getCurrentHub() {
// Get main carrier (global for every environment)
var registry = getMainCarrier();
// 如果沒有控制中心在載體上,或者它的版本是老版本,就設置新的。
// If there's no hub, or its an old API, assign a new one
if(!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) {
setHubOnCarrier(registry, newHub());
}
// node 才執行
// Prefer domains over global if they are there (applicable only to Node environment)
if(isNodeEnv()) {
return getHubFromActiveDomain(registry);
}
// 返回當前控制中心來自載體上。
// Return hub that lives on a global object
return getHubFromCarrier(registry);
}
衍生的函數 getMainCarrier、getHubFromCarrierfunction getMainCarrier() {
// 載體 這裡是window
// 通過一系列new BrowerClient() 一系列的初始化
// 掛載在 carrier.__SENTRY__ 已經有了三個屬性,globalEventProcessors, hub, logger
var carrier = getGlobalObject();
carrier.__SENTRY__ = carrier.__SENTRY__ || {
hub: undefined,
};
return carrier;
}
// 獲取控制中心 hub 從載體上
function getHubFromCarrier(carrier) {
// 已經有了則返回,沒有則new Hub
if(carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub) {
return carrier.__SENTRY__.hub;
}
carrier.__SENTRY__ = carrier.__SENTRY__ || {};
carrier.__SENTRY__.hub = newHub();
return carrier.__SENTRY__.hub;
}
bindClient 綁定客戶端在當前控制中心上Hub.prototype.bindClient = function(client) {
// 獲取最後一個
var top = this.getStackTop();
// 把 new BrowerClient() 實例 綁定到top上
top.client = client;
};
Hub.prototype.getStackTop = function() {
// 獲取最後一個
returnthis._stack[this._stack.length - 1];
};
小結2. 經過一系列的繼承和初始化再回過頭來看 initAndBind函數
function initAndBind(clientClass, options) {
if(options.debug === true) {
logger.enable();
}
var client = new clientClass(options);
console.log(client, options, 'client, options');
var currentHub = getCurrentHub();
currentHub.bindClient(client);
console.log('currentHub', currentHub);
// 原始碼
// getCurrentHub().bindClient(new clientClass(options));
}
最終會得到這樣的 Hub實例對象。筆者畫了一張圖表示,便於查看理解。
初始化完成後,再來看具體例子。具體 captureMessage 函數的實現。
Sentry.captureMessage('Hello, 若川!');
captureMessage 函數通過之前的閱讀代碼,知道會最終會調用 Fetch接口,所以直接斷點調試即可,得出如下調用棧。接下來描述調用棧的主要流程。
調用棧主要流程:
captureMessage
function captureMessage(message, level) {
var syntheticException;
try{
thrownewError(message);
}
catch(exception) {
syntheticException = exception;
}
// 調用 callOnHub 方法
return callOnHub('captureMessage', message, level, {
originalException: message,
syntheticException: syntheticException,
});
}
=> callOnHub
/**
* This calls a function on the current hub.
* @param method function to call on hub.
* @param args to pass to function.
*/
function callOnHub(method) {
// 這裡method 傳進來的是 'captureMessage'
// 把method除外的其他參數放到args數組中
var args = [];
for(var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
// 獲取當前控制中心 hub
var hub = getCurrentHub();
// 有這個方法 把args 數組展開,傳遞給 hub[method] 執行
if(hub && hub[method]) {
// tslint:disable-next-line:no-unsafe-any
return hub[method].apply(hub, __spread(args));
}
thrownewError("No hub defined or "+ method + " was not found on the hub, please open a bug report.");
}
=> Hub.prototype.captureMessage
接著看 Hub.prototype 上定義的 captureMessage 方法
Hub.prototype.captureMessage = function(message, level, hint) {
var eventId = (this._lastEventId = uuid4());
var finalHint = hint;
// 代碼有刪減
this._invokeClient('captureMessage', message, level, __assign({}, finalHint, { event_id: eventId }));
return eventId;
};
=> Hub.prototype._invokeClient
/**
* Internal helper function to call a method on the top client if it exists.
*
* @param method The method to call on the client.
* @param args Arguments to pass to the client function.
*/
Hub.prototype._invokeClient = function(method) {
// 同樣:這裡method 傳進來的是 'captureMessage'
// 把method除外的其他參數放到args數組中
var _a;
var args = [];
for(var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
var top = this.getStackTop();
// 獲取控制中心的 hub,調用客戶端也就是new BrowerClient () 實例中繼承自 BaseClient 的 captureMessage 方法
// 有這個方法 把args 數組展開,傳遞給 hub[method] 執行
if(top && top.client && top.client[method]) {
(_a = top.client)[method].apply(_a, __spread(args, [top.scope]));
}
};
=> BaseClient.prototype.captureMessage
BaseClient.prototype.captureMessage = function(message, level, hint, scope) {
var _this = this;
var eventId = hint && hint.event_id;
this._processing = true;
var promisedEvent = isPrimitive(message)
? this._getBackend().eventFromMessage(""+ message, level, hint)
: this._getBackend().eventFromException(message, hint);
// 代碼有刪減
promisedEvent
.then(function(event) { return _this._processEvent(event, hint, scope); })
// 代碼有刪減
return eventId;
};
最後會調用 _processEvent 也就是
=> BaseClient.prototype._processEvent
這個函數最終會調用
_this._getBackend().sendEvent(finalEvent);
也就是
=> BaseBackend.prototype.sendEvent
BaseBackend.prototype.sendEvent = function(event) {
this._transport.sendEvent(event).then(null, function(reason) {
logger.error("Error while sending event: "+ reason);
});
};
=> FetchTransport.prototype.sendEvent 最終發送了請求
FetchTransport.prototype.sendEventFetchTransport.prototype.sendEvent = function(event) {
var defaultOptions = {
body: JSON.stringify(event),
method: 'POST',
// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
// https://caniuse.com/#feat=referrer-policy
// It doesn't. And it throw exception instead of ignoring this parameter...
// REF: https://github.com/getsentry/raven-js/issues/1233
referrerPolicy: (supportsReferrerPolicy() ? 'origin': ''),
};
// global$2.fetch(this.url, defaultOptions) 使用fetch發送請求
returnthis._buffer.add(global$2.fetch(this.url, defaultOptions).then(function(response) { return({
status: exports.Status.fromHttpCode(response.status),
}); }));
};
看完 Ajax上報 主線,再看本文的另外一條主線 window.onerror 捕獲。
window.onerror 和 window.onunhandledrejection 捕獲 錯誤例子:調用一個未申明的變量。
func();
Promise 不捕獲錯誤
newPromise(() => {
fun();
})
.then(res => {
console.log('then');
})
captureEvent調用棧主要流程:
window.onerror
GlobalHandlers.prototype._installGlobalOnErrorHandler = function() {
if(this._onErrorHandlerInstalled) {
return;
}
var self = this; // tslint:disable-line:no-this-assignment
// 瀏覽器中這裡的 this._global. 就是window
this._oldOnErrorHandler = this._global.onerror;
this._global.onerror = function(msg, url, line, column, error) {
var currentHub = getCurrentHub();
// 代碼有刪減
currentHub.captureEvent(event, {
originalException: error,
});
if(self._oldOnErrorHandler) {
return self._oldOnErrorHandler.apply(this, arguments);
}
returnfalse;
};
this._onErrorHandlerInstalled = true;
};
window.onunhandledrejection
GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function() {
if(this._onUnhandledRejectionHandlerInstalled) {
return;
}
var self = this; // tslint:disable-line:no-this-assignment
this._oldOnUnhandledRejectionHandler = this._global.onunhandledrejection;
this._global.onunhandledrejection = function(e) {
// 代碼有刪減
var currentHub = getCurrentHub();
currentHub.captureEvent(event, {
originalException: error,
});
if(self._oldOnUnhandledRejectionHandler) {
return self._oldOnUnhandledRejectionHandler.apply(this, arguments);
}
returnfalse;
};
this._onUnhandledRejectionHandlerInstalled = true;
};
共同點:都會調用 currentHub.captureEvent
currentHub.captureEvent(event, {
originalException: error,
});
=> 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,也可以看打包後出來未壓縮的代碼。
讀源碼比較耗費時間,寫文章記錄下來更加費時間(比如寫這篇文章跨度十幾天...),但收穫一般都比較大。
如果讀者發現有不妥或可改善之處,再或者哪裡沒寫明白的地方,歡迎評論指出。另外覺得寫得不錯,對您有些許幫助,可以點讚、評論、轉發分享,也是對筆者的一種支持。萬分感謝。
推薦閱讀知乎滴滴云:超詳細!搭建一個前端錯誤監控系統
掘金BlackHole1:JavaScript集成Sentry
丁香園 開源的 Sentry 小程序 SDKsentry-miniapp
sentry官網
sentry-javascript倉庫
如果你覺得這篇內容對你有所幫助,我想邀請你幫我兩個小忙:
推薦閱讀:
Chrome 80發布,新特性將對用戶產生深遠影響
金三銀四?這20道高頻面試題值得了解下
淺談讓前端頭疼的性能監控~
來自騰訊CDC團隊的前端異常監控解決方案~
基於 React 的可視化編輯平臺實踐
頁面可視化配置搭建工具技術要點