你好,我是若川。這是
學習源碼總體架構系列
第四篇。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。文章學習的是打包整合後的代碼,不是實際倉庫中的拆分的代碼。javascript
學習源碼總體架構系列
文章以下:html
1.學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫
2.學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫
3.學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫
4.學習 sentry 源碼總體架構,打造屬於本身的前端異常監控SDK
5.學習 vuex 源碼總體架構,打造屬於本身的狀態管理庫
6.學習 axios 源碼總體架構,打造屬於本身的請求庫
7.學習 koa 源碼的總體架構,淺析koa洋蔥模型原理和co原理
8.學習 redux 源碼總體架構,深刻理解 redux 及其中間件原理前端
感興趣的讀者能夠點擊閱讀。vue
導讀
本文經過梳理前端錯誤監控知識、介紹sentry
錯誤監控原理、sentry
初始化、Ajax
上報、window.onerror、window.onunhandledrejection
幾個方面來學習sentry
的源碼。java
開發微信小程序,想着搭建小程序錯誤監控方案。最近用了丁香園 開源的Sentry
小程序 SDK
sentry-miniapp。 順便研究下sentry-javascript
倉庫 的源碼總體架構,因而有了這篇文章。node
本文分析的是打包後未壓縮的源碼,源碼總行數五千餘行,連接地址是:browser.sentry-cdn.com/5.7.1/bundl…, 版本是v5.7.1
。webpack
本文示例等源代碼在這個人github
博客中github blog sentry,須要的讀者能夠點擊查看,若是以爲不錯,能夠順便star
一下。ios
看源碼前先來梳理下前端錯誤監控的知識。git
摘抄自 慕課網視頻教程:前端跳槽面試必備技巧
別人作的筆記:前端跳槽面試必備技巧-4-4 錯誤監控類github
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
對比便可找出圖片資源未加載項目
window.addEventListener('error', function(e) { console.log('捕獲', e) }, true) // 這裏只有捕獲才能觸發事件,冒泡是不能觸發 複製代碼
1.採用Ajax
通訊的方式上報
2.利用Image
對象上報 (主流方式)
Image
上報錯誤方式: (new Image()).src = 'https://lxchuan12.cn/error?name=若川'
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
對象(對象)
當
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()) { return new FetchTransport(transportOptions); } return new XHRTransport(transportOptions); }; 複製代碼
2.1
fetch
FetchTransport.prototype.sendEvent = function (event) { var defaultOptions = { body: JSON.stringify(event), method: 'POST', referrerPolicy: (supportsReferrerPolicy() ? 'origin' : ''), }; return this._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; return this._buffer.add(new SyncPromise(function (resolve, reject) { // 熟悉的 XMLHttpRequest var request = new XMLHttpRequest(); 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的兩張圖。或者能夠點贊或收藏這篇文章,後續想看了再看。
var Sentry = (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; }({})); 複製代碼
初始化
// 這裏的dsn,是sentry.io網站會生成的。 Sentry.init({ dsn: 'xxx' }); 複製代碼
// options 是 {dsn: '...'} function init(options) { // 若是options 是undefined,則賦值爲 空對象 if (options === void 0) { 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
。其實作的事情也比較簡單,就是獲取全局對象。瀏覽器中是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 return Object.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
函數
function initAndBind(clientClass, options) { // 這裏沒有開啓debug模式,logger.enable() 這句不會執行 if (options.debug === true) { logger.enable(); } getCurrentHub().bindClient(new clientClass(options)); } 複製代碼
能夠看出 initAndBind()
,第一個參數是 BrowserClient
構造函數,第二個參數是初始化後的options
。 接着先看 構造函數 BrowserClient
。 另外一條線 getCurrentHub().bindClient()
先不看。
var BrowserClient = /** @class */ (function (_super) { // `BrowserClient` 繼承自`BaseClient` __extends(BrowserClient, _super); /** * Creates a new Browser SDK instance. * * @param options Configuration options for this SDK. */ function BrowserClient(options) { if (options === void 0) { options = {}; } // 把`BrowserBackend`,`options`傳參給`BaseClient`調用。 return _super.call(this, BrowserBackend, options) || this; } return BrowserClient; }(BaseClient)); 複製代碼
從代碼中能夠看出
: BrowserClient
繼承自BaseClient
,而且把BrowserBackend
,options
傳參給BaseClient
調用。
先看 BrowserBackend
,這裏的BaseClient
,暫時不看。
看BrowserBackend
以前,先提一下繼承、繼承靜態屬性和方法。
未打包的源碼是使用ES6 extends
實現的。這是打包後的對ES6
的extends
的一種實現。
若是對繼承還不是很熟悉的讀者,能夠參考我以前寫的文章。面試官問:JS的繼承
// 繼承靜態方法和屬性 var extendStatics = function(d, b) { // 若是支持 Object.setPrototypeOf 這個函數,直接使用 // 不支持,則使用原型__proto__ 屬性, // 如何還不支持(但有可能__proto__也不支持,畢竟是瀏覽器特有的方法。) // 則使用for in 遍歷原型鏈上的屬性,從而達到繼承的目的。 extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && 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
構造函數
var BrowserBackend = /** @class */ (function (_super) { __extends(BrowserBackend, _super); function BrowserBackend() { 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) { return new this._options.transport(transportOptions); } // 支持Fetch則返回 FetchTransport 實例,不然返回 XHRTransport實例, // 這兩個構造函數具體代碼在開頭已有提到。 if (supportsFetch()) { return new FetchTransport(transportOptions); } return new XHRTransport(transportOptions); }; // code ... return BrowserBackend; }(BaseBackend)); 複製代碼
BrowserBackend
又繼承自 BaseBackend
。
/** * This is the base implemention of a Backend. * @hidden */ var BaseBackend = /** @class */ (function () { /** Creates a new backend instance. */ function BaseBackend(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 () { return new NoopTransport(); }; // 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 () { return this._transport; }; return BaseBackend; }()); 複製代碼
經過一系列的繼承後,回過頭來看 BaseClient
構造函數。
var BaseClient = /** @class */ (function () { /** * Initializes this client instance. * * @param backendClass A constructor function to create the backend. * @param options Options for the client. */ function BaseClient(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 = new Dsn(options.dsn); } if (this._isEnabled()) { this._integrations = setupIntegrations(this._options); } } // code ... return BaseClient; }()); 複製代碼
能夠輸出下具體new clientClass(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
,再把new BrowserClient()
的實例對象綁定在Hub
上。
// 獲取當前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, new Hub()); } // 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); } 複製代碼
function 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 = new Hub(); return carrier.__SENTRY__.hub; } 複製代碼
Hub.prototype.bindClient = function (client) { // 獲取最後一個 var top = this.getStackTop(); // 把 new BrowerClient() 實例 綁定到top上 top.client = client; }; 複製代碼
Hub.prototype.getStackTop = function () { // 獲取最後一個 return this._stack[this._stack.length - 1]; }; 複製代碼
再回過頭來看 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, 若川!'); 複製代碼
經過以前的閱讀代碼,知道會最終會調用Fetch
接口,因此直接斷點調試便可,得出以下調用棧。 接下來描述調用棧的主要流程。
調用棧主要流程:
captureMessage
function captureMessage(message, level) { var syntheticException; try { throw new Error(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)); } throw new Error("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.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發送請求 return this._buffer.add(global$2.fetch(this.url, defaultOptions).then(function (response) { return ({ status: exports.Status.fromHttpCode(response.status), }); })); }; 複製代碼
看完 Ajax 上報
主線,再看本文的另一條主線 window.onerror
捕獲。
例子:調用一個未申明的變量。
func();
複製代碼
Promise
不捕獲錯誤
new Promise(() => { fun(); }) .then(res => { console.log('then'); }) 複製代碼
調用棧主要流程:
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); } return false; }; 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); } return false; }; 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
小程序 SDK
sentry-miniapp
sentry
官網
sentry-javascript
倉庫
面試官問:JS的繼承
面試官問:JS的this指向
面試官問:可否模擬實現JS的call和apply方法
面試官問:可否模擬實現JS的bind方法
面試官問:可否模擬實現JS的new操做符
前端使用puppeteer 爬蟲生成《React.js 小書》PDF併合並
做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
我的博客-若川,使用vuepress
重構了,閱讀體驗可能更好些
掘金專欄,歡迎關注~
segmentfault
前端視野專欄,歡迎關注~
知乎前端視野專欄,歡迎關注~
github blog,相關源碼和資源都放在這裏,求個star
^_^~
可能比較有趣的微信公衆號,長按掃碼關注。歡迎加筆者微信ruochuan12
(註明來源,基原本者不拒),拉您進【前端視野交流羣】,長期交流學習~
本文使用 mdnice 排版