本文首發於公衆號: 符合預期的CoyPan
在前端項目中,因爲JavaScript自己是一個弱類型語言,加上瀏覽器環境的複雜性,網絡問題等等,很容易發生錯誤。作好網頁錯誤監控,不斷優化代碼,提升代碼健壯性是一項很重要的工做。本文將從Error開始,講到如何捕獲頁面中的異常。文章較長,細節較多,請耐心觀看。javascript
JavaScript中,Error
是一個構造函數,經過它建立一個錯誤對象。當運行時錯誤產生時,Error的實例對象會被拋出。構造一個Error的語法以下:html
// message: 錯誤描述 // fileName: 可選。被建立的Error對象的fileName屬性值。默認是調用Error構造器代碼所在的文件的名字。 // lineNumber: 可選。被建立的Error對象的lineNumber屬性值。默認是調用Error構造器代碼所在的文件的行號。 new Error([message[, fileName[, lineNumber]]])
Error有兩個標準屬性:前端
Error.prototype.name
:錯誤的名字Error.prototype.message
:錯誤的描述例如,在chrome控制檯中輸入如下代碼:vue
var a = new Error('錯誤測試'); console.log(a); // Error: 錯誤測試 // at <anonymous>:1:9 console.log(a.name); // Error console.log(a.message); // 錯誤測試
Error只有一個標準方法:java
Error.prototype.toString
:返回表示一個表示錯誤的字符串。接上面的代碼:node
a.toString(); // "Error: 錯誤測試"
各個瀏覽器廠商對於Error都有本身的實現。好比下面這些屬性:react
Error.prototype.fileName
:產生錯誤的文件名。Error.prototype.lineNumber
:產生錯誤的行號。Error.prototype.columnNumber
:產生錯誤的列號。Error.prototype.stack
:堆棧信息。這個比較經常使用。這些屬性均不是標準屬性,在生產環境中謹慎使用。不過現代瀏覽器差很少都支持了。git
除了通用的Error構造函數外,JavaScript還有7個其餘類型的錯誤構造函數。github
當JavaScript運行過程當中出錯時,會拋出上8種(上述7種加上通用錯誤類型)錯誤中的其中一種錯誤。錯誤類型能夠經過error.name拿到。ajax
你也能夠基於Error構造本身的錯誤類型,這裏就不展開了。
上面介紹的都是JavaScript自己運行時會發生的錯誤。頁面中還會有其餘的異常,好比錯誤地操做了DOM。
DOMException是W3C DOM核心對象,表示調用一個Web Api時發生的異常。什麼是Web Api呢?最多見的就是DOM元素的一系列方法,其餘還有XMLHttpRequest、Fetch等等等等,這裏就不一一說明了。直接看下面一個操做DOM的例子:
var node = document.querySelector('#app'); var refnode = node.nextSibling; var newnode = document.createElement('div'); node.insertBefore(newnode, refnode); // 報錯:Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
單從JS代碼邏輯層面來看,沒有問題。可是代碼的操做不符合DOM的規則。
DOMException
構造函數的語法以下:
// message: 可選,錯誤描述。 // name: 可選,錯誤名稱。常量,具體值能夠在這裏找到:https://developer.mozilla.org/zh-CN/docs/Web/API/DOMException new DOMException([message[, name]]);
DOMException
有如下三個屬性:
DOMException.code
:錯誤編號。DOMException.message
:錯誤描述。DOMException.name
:錯誤名稱。以上面那段錯誤代碼爲例,其拋出的DOMException各屬性的值爲:
code: 8 message: "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node." name: "NotFoundError"
Promise
產生的異常在Promise
中,若是Promise
被reject
了,就會拋出異常:PromiseRejectionEvent
。注意,下面兩種狀況都會致使Promise
被reject
:
Promise.reject
。Promise
中的代碼出錯。PromiseRejectionEvent
的構造函數目前在瀏覽器中大多都不兼容,這裏就不說了。
PromiseRejectionEvent
的屬性有兩個:
PromiseRejectionEvent.promise
:被reject
的Promise
。PromiseRejectionEvent.reason
:Promise
被reject
的緣由。會傳遞給reject
。Promsie
的catch
中的參數。因爲網絡,安全等緣由,網頁加載資源失敗,請求接口出錯等,也是一種常見的錯誤。
一個網頁在運行過程當中,可能發生四種錯誤:
我認爲,對於前兩種錯誤,咱們在平時的開發過程當中,不用特別去區分,能夠統一成:【代碼出錯】。
網頁發生錯誤,開發者如何捕獲這些錯誤呢 ? 常見的有如下方法。
try...catch…
你們都不陌生了。通常用來在具體的代碼邏輯中捕獲錯誤。
try { throw new Error("oops"); } catch (ex) { console.log("error", ex.message); // error oops }
當try-block
中的代碼發生異常時,能夠在catck-block
中將異常接住,瀏覽器便不會拋出錯誤。可是,這種方式並不能捕獲異步代碼中的錯誤,如:
try { setTimeout(function(){ throw new Error('lala'); },0); } catch(e) { console.log('error', e.message); }
這個時候,瀏覽器依然會拋出錯誤:Uncaught Error: lala
。
試想如下,若是咱們將全部的代碼合理的劃分,而後都用try catch
包起來,是否是就能夠捕獲到全部的錯誤了呢?能夠經過編譯工具來實現這個功能。不過,try catch
是比較耗費性能的。
window.onerror = function(message, source, lineno, colno, error) { ... }
函數參數:
message
:錯誤信息(字符串)source
:發生錯誤的腳本URL(字符串)lineno
:發生錯誤的行號(數字)colno
:發生錯誤的列號(數字)error
:Error對象(對象)注意,若是這個函數返回true
,那麼將會阻止執行瀏覽器默認的錯誤處理函數。
window.addEventListener('error', function(event) { ... })
咱們調用Object.prototype.toString.call(event)
,返回的是[object ErrorEvent]
。能夠看到event
是ErrorEvent
對象的實例。ErrorEvent
是事件對象在腳本發生錯誤時產生,從Event
繼承而來。因爲是事件,天然能夠拿到target
屬性。ErrorEvent
還包括了錯誤發生時的信息。
注意,這裏的ErrorEvent.prototype.error
對應的Error對象,就是上文提到的Error
, InternalError
,RangeError
,EvalError
,ReferenceError
,SyntaxError
,TypeError
,URIError
,DOMException
中的一種。
window.addEventListener('unhandledrejection', function (event) { ... });
在使用Promise
的時候,若是沒有聲明catch
代碼塊,Promise
的異常會被拋出。只能經過這個方法或者window.onunhandledrejection
才能捕獲到該異常。
event
就是上文提到的PromiseRejectionEvent
。咱們只須要關注其reason
就行。
Event
接口的error
事件,並執行該元素上的onerror()
處理函數。但這些error事件不會向上冒泡到window。不過,這些error
事件能被window.addEventListener('error')
在事件的捕獲階段監聽到。也就是說,面對資源加載失敗的錯誤,只能用window.addEventListerner('error', function(){}, true)
,window.onerror
無效。我認爲,在開發的過程當中,對於容易出錯的地方,可使用try{}catch(){}
來進行錯誤的捕獲,作好兜底處理,避免頁面掛掉。而對於全局的錯誤捕獲,在現代瀏覽器中,我傾向於只使用window.addEventListener('error')
(在錯誤事件的捕獲階段捕獲事件)以及window.addEventListener('unhandledrejection')
。若是須要考慮兼容性,須要加上window.onerror
,三者同時使用,window.addEventListener('error')
專門用來捕獲資源加載錯誤。
在進行錯誤捕獲的過程當中,不少時候並不能拿到完整的錯誤信息,獲得的僅僅是一個"Script Error"
。
因爲12年前這篇文章裏提到的安全問題:https://blog.jeremiahgrossman...,瀏覽器們都對內核進行了升級:
當加載自不一樣域的腳本中發生語法錯誤時,爲避免信息泄露,語法錯誤的細節將不會報告,而是使用簡單的"Script error."
代替。
通常而言,頁面的JS文件都是放在CDN的,和頁面自身的URL產生了跨域問題,因此引發了"Script Error"
。
服務端添加Access-Control-Allow-Origin
,頁面在script
標籤中配置 crossorigin="anonymous"
。這樣,便解決了由於跨域而帶來的"Script Error"
問題。
Script Error
麼上面介紹了"Script Error"
的標準解決方案。可是,並非全部的瀏覽器都支持crossorigin="anonymous"
,也不是全部的服務端都能及時配置Access-Control-Allow-Origin
,這種狀況下,還有什麼方法能在全局捕獲到全部的錯誤,並拿到詳細信息呢?
看一個例子:
const nativeAddEventListener = EventTarget.prototype.addEventListener; // 先將原生方法保存起來。 EventTarget.prototype.addEventListener = function (type, func, options) { // 重寫原生方法。 const wrappedFunc = function (...args) { // 將回調函數包裹一層try catch try { return func.apply(this, args); } catch (e) { const errorObj = { ... error_name: e.name || '', error_msg: e.message || '', error_stack: e.stack || (e.error && e.error.stack), error_native: e, ... }; // 接下來能夠將errorObj統一進行處理。 } } return nativeAddEventListener.call(this, type, wrappedFunc, options); // 調用原生的方法,保證addEventListener正確執行 }
咱們劫持了原生的addEventListener
代碼,對addEventListener
代碼中的回調函數加了一層try{}catch(){}
,這樣,回調函數中拋出的錯誤會被catch
住,瀏覽器不會對try-catch
起來的異常進行跨域攔截,因此咱們能夠拿到詳細的錯誤信息。經過上面的操做,咱們能夠拿到全部監聽事件的回調函數中的錯誤啦。其餘的場景怎麼辦呢?繼續劫持原生方法。
一個前端項目中,除了事件監聽,接口請求也是一個頻繁出現的場景。接着上面的代碼,下面咱們來劫持一下Ajax。
if (!XMLHttpRequest) { return; } const nativeAjaxSend = XMLHttpRequest.prototype.send; // 首先將原生的方法保存。 const nativeAjaxOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (mothod, url, ...args) { // 劫持open方法,是爲了拿到請求的url const xhrInstance = this; xhrInstance._url = url; return nativeAjaxOpen.apply(this, [mothod, url].concat(args)); } XMLHttpRequest.prototype.send = function (...args) { // 對於ajax請求的監控,主要是在send方法裏處理。 const oldCb = this.onreadystatechange; const oldErrorCb = this.onerror; const xhrInstance = this; xhrInstance.addEventListener('error', function (e) { // 這裏捕獲到的error是一個ProgressEvent。e.target 的值爲 XMLHttpRequest的實例。當網絡錯誤(ajax並無發出去)或者發生跨域的時候,會觸發XMLHttpRequest的error, 此時,e.target.status 的值爲:0,e.target.statusText 的值爲:'' const errorObj = { ... error_msg: 'ajax filed', error_stack: JSON.stringify({ status: e.target.status, statusText: e.target.statusText }), error_native: e, ... } /*接下來能夠對errorObj進行統一處理*/ }); xhrInstance.addEventListener('abort', function (e) { // 主動取消ajax的狀況須要標註,不然可能會產生誤報 if (e.type === 'abort') { xhrInstance._isAbort = true; } }); this.onreadystatechange = function (...innerArgs) { if (xhrInstance.readyState === 4) { if (!xhrInstance._isAbort && xhrInstance.status !== 200) { // 請求不成功時,拿到錯誤信息 const errorObj = { error_msg: JSON.stringify({ code: xhrInstance.status, msg: xhrInstance.statusText, url: xhrInstance._url }), error_stack: '', error_native: xhrInstance }; /*接下來能夠對errorObj進行統一處理*/ } } oldCb && oldCb.apply(this, innerArgs); } return nativeAjaxSend.apply(this, args); } }
咱們引用框架時,某些框架會用console.error
的方法拋出錯誤。咱們能夠劫持console.error
,來捕獲錯誤。
const nativeConsoleError = window.console.error; window.console.error = function (...args) { args.forEach(item => { if (typeDetect.isError(item)) { ... } else { ... } }); nativeConsoleError.apply(this, args); }
原生的方法有不少,還好比fetch
、setTimeout
等。這裏不一一列舉了。可是使用劫持原生方法以覆蓋全部的場景是十分困難的。
咱們主要來看一下React
和Vue
是怎麼解決錯誤捕獲問題的。
在React
v16之前,可使用unstable_handleError
來處理捕獲的錯誤。React
v16之後,使用componentDidCatch
來處理捕獲的錯誤。若需全局捕獲錯誤,能夠在最外層包裹一層組件,在componentDidCatch
中捕獲錯誤信息。具體用法參考官方文檔:https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html
在React
中,錯誤會被throw
出來。在寫做本文的時候,我遇到一個問題,若是在加載react
相關的代碼前,按照上文的方法劫持addEventListener
,那麼React
將不會正常工做了,可是沒有任何報錯。React
有一套本身的事件系統,會不會和這個有關呢?以前沒有研究過React
源碼,粗略調試瞭如下,沒有發現問題所在。後續會仔細研究。
研究結果在這裏:https://segmentfault.com/a/11...
Vue
的源碼中,在關鍵函數(好比鉤子函數等)執行的時候,都加上try{}catch(){}
,在cacth
中處理捕獲到的錯誤。看下面的源碼。
... // vue源碼片斷 function callHook (vm, hook) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget(); var handlers = vm.$options[hook]; if (handlers) { for (var i = 0, j = handlers.length; i < j; i++) { try { handlers[i].call(vm); } catch (e) { handleError(e, vm, (hook + " hook")); } } } if (vm._hasHookEvent) { vm.$emit('hook:' + hook); } popTarget(); } ... function globalHandleError (err, vm, info) { if (config.errorHandler) { try { return config.errorHandler.call(null, err, vm, info) } catch (e) { logError(e, null, 'config.errorHandler'); } } logError(err, vm, info); } function logError (err, vm, info) { { warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); } /* istanbul ignore else */ if ((inBrowser || inWeex) && typeof console !== 'undefined') { console.error(err); } else { throw err } }
Vue中提供了
Vue.config.errorHandler`來處理捕獲到的錯誤。
// err: 捕獲到的錯誤對象。 // vm: 出錯的VueComponent. // info: Vue 特定的錯誤信息,好比錯誤所在的生命週期鉤子 Vue.config.errorHandler = function (err, vm, info) {}
若是開發者沒有配置Vue.config.errorHandler
,那麼捕獲到的錯誤會以console.error
的方式輸出。
捕獲到錯誤後,如何上報呢?最多見、最簡單的方式就是經過<img>
了。代碼簡單,且沒有跨域煩惱。
function logError(error){ var img = new Image(); img.onload = img.onerror = function(){ img = null; } img.src = `${上報地址}?${processErrorParam(error)}`; }
當上報數據比較多時,可使用post
的方式進行上報。
錯誤的上報實際上是一項複雜的工程,涉及到上報策略、上報分類等等。特別是在項目的業務比較複雜的時候,更應該關注上報的質量,避免影響到業務功能的正常運行。使用了打包工具處理的代碼,每每還須要結合sourceMap
進行代碼定位。本文就不作介紹了。
要創建一套完整、可用的前端錯誤監控體系是一項複雜、浩大的工程。可是,這項工程每每是必備的。本文主要介紹了你可能沒關注過的Error的一些細節,以及如何捕獲頁面中的錯誤。關於劫持原生方法部分的代碼,你能夠在https://github.com/CoyPan/Fec找到。
符合預期。
歡迎關注個人公衆號: 符合預期的CoyPan,
這裏只有乾貨,符合你的預期。