JS高級調試技巧:捕獲和分析 JavaScript Error詳解

前端工程師都知道 JavaScript 有基本的異常處理能力。咱們能夠 throw new Error(),瀏覽器也會在咱們調用 API 出錯時拋出異常。但估計絕大多數前端工程師都沒考慮過收集這些異常信息前端

反正只要 JavaScript 出錯後刷新不復現,那用戶就能夠經過刷新解決問題,瀏覽器不會崩潰,當沒有發生過好了。這種假設在 Single Page App 流行以前仍是成立的。如今的 Single Page App 運行一段時間後狀態複雜無比,用戶可能進行了若干輸入操做纔來到這裏的,說刷新就刷新啊?以前的操做豈不要徹底重作?因此咱們仍是有必要捕獲和分析這些異常信息的,而後咱們就能夠修改代碼避免影響用戶體驗。canvas

捕獲異常的方式

咱們本身寫的 throw new Error() 想要捕獲固然能夠捕獲,由於咱們很清楚 throw 寫在哪裏了。可是調用瀏覽器 API 時發生的異常就不必定那麼容易捕獲了,有些 API 在標準裏就寫着會拋出異常,有些 API 只有個別瀏覽器由於實現差別或者有缺陷而拋出異常。對於前者咱們還能經過 try-catch 捕獲,對於後者咱們必須監聽全局的異常而後捕獲。瀏覽器

try-catch

若是有些瀏覽器 API 是已知會拋出異常的,那咱們就須要把調用放到 try-catch 裏面,避免由於出錯而致使整個程序進入非法狀態。例如說 window.localStorage 就是這樣的一個 API,在寫入數據超過容量限制後就會拋出異常,在 Safari 的隱私瀏覽模式下也會如此。安全

try {
 localStorage.setItem('date', Date.now());
} catch (error) {
 reportError(error);
}

另外一個常見的 try-catch 適用場景是回調。由於回調函數的代碼是咱們不可控的,代碼質量如何,會不會調用其它會拋出異常的 API,咱們一律不知道。爲了避免要由於回調出錯而致使調用回調後的其它代碼沒法執行,因此把調用回到放到 try-catch 裏面是必須的。服務器

listeners.forEach(function(listener) {
 try {
 listener();
 } catch (error) {
 reportError(error);
 }
});

window.onerror

對於 try-catch 覆蓋不到的地方,若是出現異常就只能經過 window.onerror 來捕獲了。前端工程師

window.onerror =
 function(errorMessage, scriptURI, lineNumber) {
 reportError({
 message: errorMessage,
 script: scriptURI,
 line: lineNumber
 });
}

注意不要耍小聰明使用 window.addEventListener 或 window.attachEvent 的形式去監聽 window.onerror。不少瀏覽器只實現了 window.onerror,或者是隻有 window.onerror 的實現是標準的。考慮到標準草案定義的也是 window.onerror,咱們使用 window.onerror 就行了。框架

屬性丟失

假設咱們有一個 reportError 函數用來收集捕獲到的異常,而後批量發送到服務器端存儲以便查詢分析,那麼咱們會想要收集哪些信息呢?比較有用的信息包括:錯誤類型(name)、錯誤消息(message)、腳本文件地址(script)、行號(line)、列號(column)、堆棧跟蹤(stack)。若是一個異常是經過try-catch 捕獲到的,這些信息都在 Error 對象上(主流瀏覽器都支持),因此 reportError 也能收集到這些信息。但若是是經過 window.onerror 捕獲到的,咱們都知道這個事件函數只有 3 個參數,因此這 3 個參數意外的信息就丟失了。ide

序列化消息

若是 Error 對象是咱們本身建立的話,那麼 error.message 就是由咱們控制的。基本上咱們把什麼放進error.message 裏面,window.onerror 的第一個參數(message)就會是什麼。(瀏覽器其實會略做修改,例如加上 'Uncaught Error: ' 前綴。)所以咱們能夠把咱們關注的屬性序列化(例如 JSON.Stringify)後存放到 error.message 裏面,而後在 window.onerror 讀取出來反序列化就能夠了。固然,這僅限於咱們本身建立的 Error 對象。函數

第五個參數

瀏覽器廠商也知道你們在使用 window.onerror 時受到的限制,因此開始往 window.onerror 上面添加新的參數。考慮到只有行號沒有列號好像不是很對稱的樣子,IE 首先把列號加上了,放在第四個參數。然而你們更關心的是可否拿到完整的堆棧,因而 Firefox 說不如把堆棧放在第五個參數吧。但 Chrome 說那還不如把整個 Error 對象放在第五個參數,你們想讀取什麼屬性均可以了,包括自定義屬性。結果因爲 Chrome 動做比較快,在 Chrome 30 實現了新的 window.onerror 簽名,致使標準草案也就跟着這樣寫了。工具

window.onerror = function(
 errorMessage,
 scriptURI,
 lineNumber,
 columnNumber,
 error
) {
 if (error) {
 reportError(error);
 } else {
 reportError({
 message: errorMessage,
 script: scriptURI,
 line: lineNumber,
 column: columnNumber
 });
 }
}
屬性正規化

咱們以前討論到的 Error 對象屬性,其名稱都是基於 Chrome 命名方式的,然而不一樣瀏覽器對 Error 對象屬性的命名方式各不相同,例如腳本文件地址在 Chrome 叫作 script 但在 Firefox 叫作 filename。所以,咱們還須要一個專門的函數來對 Error 對象進行正規化處理,也就是把不一樣的屬性名稱都映射到統一的屬性名稱上。具體作法能夠參考這篇文章。儘管瀏覽器實現會更新,但人手維護一份這樣的映射表並不會太難。

相似的是堆棧跟蹤(stack)的格式。這個屬性以純文本的形式保存一份異常在發生時的堆棧信息,因爲各個瀏覽器使用的文本格式不同,因此也須要人手維護一份正則表達,用於從純文本中提取每一幀的函數名(identifier)、文件(script)、行號(line)和列號(column)。

安全限制

若是你也遇到過消息爲 'Script error.' 的錯誤,你會明白我在說什麼的,這實際上是瀏覽器針對不一樣源(origin)腳本文件的限制。這個安全限制的理由是這樣的:假設一家網銀在用戶登陸後返回的 HTML 跟匿名用戶看到的 HTML 不同,一個第三方網站就能把這家網銀的 URI 放到 script.src 屬性裏面。HTML 固然不可能被當作 JS 解析啦,因此瀏覽器會拋出異常,而這個第三方網站就能經過解析異常的位置來判斷用戶是否有登陸。爲此瀏覽器對於不一樣源腳本文件拋出的異常一概進行過濾,過濾得只剩下 'Script error.' 這樣一條不變的消息,其它屬性通通消失。

對於有必定規模的網站來講,腳本文件放在 CDN 上,不一樣源是很正常的。如今就算是本身作個小網站,常見框架如 jQuery 和 Backbone 都能直接引用公共 CDN 上的版本,加速用戶下載。因此這個安全限制確實形成了一些麻煩,致使咱們從 Chrome 和 Firefox 收集到的異常信息都是無用的 'Script error.'

CORS

想要繞過這個限制,只要保證腳本文件和頁面自己同源便可。但把腳本文件放在不經 CDN 加速的服務器上,豈不下降用戶下載速度?一個解決方案是,腳本文件繼續放在 CDN 上,利用 XMLHttpRequest 經過 CORS 把內容下載回來,再建立 <script> 標籤注入到頁面當中。在頁面當中內嵌的代碼固然是同源的啦。

這提及來很簡單,但實現起來卻有不少細節問題。用一個簡單的例子來講:

<script src="http://cdn.com/step1.js"></script>
<script>
 (function step2() {})();
</script>
<script src="http://cdn.com/step3.js"></script>

咱們都知道這個 step一、step二、step3 若是存在依賴關係的話,則必須嚴格按照這個順序執行,不然就可能出錯。瀏覽器能夠並行請求 step1 和 step3 的文件,但在執行時順序是保證的。若是咱們本身經過 XMLHttpRequest 獲取 step1 和 step3 的文件內容,咱們就須要自行保證其順序正確性。此外不要忘記了 step2,在 step1 以非阻塞形式下載的時候 step2 就能夠被執行了,因此咱們還必須人爲干預 step2 讓它等待 step1 完成後再執行。

若是咱們已經有一整套工具來生成網站上不一樣頁面的 <script> 標籤的話,咱們就須要調整一下這套工具讓它對 <script> 標籤作出改動:

<script>
 scheduleRemoteScript('http://cdn.com/step1.js');
</script>
<script>
 scheduleInlineScript(function code() {
 (function step2() {})();
 });
</script>
<script>
 scheduleRemoteScript('http://cdn.com/step3.js');
</script>

咱們須要實現 scheduleRemoteScript 和 scheduleInlineScript 這兩個函數,而且保證它們在第一個引用外部腳本文件的 <script> 標籤以前就被定義好,而後餘下的 <script> 標籤都會被改寫成上面這種形式。注意本來當即執行的 step2 函數被放到了一個更大的 code 函數裏面了。code 函數並不會被執行,它只是一個容器而已,這樣使得本來 step2 的代碼不須要轉義就能保留下來,但又不會被當即執行。

接下來咱們還須要實現一套完整的機制,保證這些由 scheduleRemoteScript 根據地址下載回來的文件內容和由 scheduleInlineScript 直接獲取到的代碼可以按照正確的順序一個接一個地執行。詳細的代碼我就不在這裏給出了,你們有興趣能夠本身去實現。

行號反查

經過 CORS 獲取內容再把代碼注入頁面可以突破安全限制,但會引入一個新的問題,那就是行號衝突。本來經過 error.script 能夠定位到惟一的腳本文件,再經過 error.line 能夠定位到惟一的行號。如今因爲都是頁面內嵌的代碼,多個 <script> 標籤並不能經過 error.script 來區分,然而每個 <script> 標籤內部的行號都是從 1 算起的,結果就致使咱們沒法利用異常信息定位錯誤所在的源代碼位置。

爲了不行號衝突,咱們能夠浪費一些行號,使得每個 <script> 標籤中有實際代碼所使用的行號區間互相不重疊。舉個例子來講,假設每一個 <script> 標籤中的實際代碼都不超過 1000 行,那麼我可讓第一個<script> 標籤中的代碼佔用第 1–1000 行,讓第二個 <script> 標籤中的代碼佔用第 1001–2000 行(前面插入 1000 行空行),第三個 <script> 標籤種的代碼佔用第 2001–3000 行(前面插入 2000 行空行),以此類推。而後咱們使用 data-* 屬性記錄這些信息,便於反查。

<script
 data-src="http://cdn.com/step1.js"
 data-line-start="1"
>
 // code for step 1
</script>
<script data-line-start="1001">
 // '\n' * 1000
 // code for step 2
</script>
<script
 data-src="http://cdn.com/step3.js"
 data-line-start="2001"
>
 // '\n' * 2000
 // code for step 3
</script>

通過這樣處理後,若是一個錯誤的 error.line 是 3005 的話,那意味着實際的 error.script 應該是 'http://cdn.com/step3.js',而實際的 error.line 則應該是 5。咱們能夠在以前提到的 reportError 函數裏面完成這項行號反查工做。

固然,因爲咱們沒辦法保證每個腳本文件只有 1000 行,也有可能有些腳本文件明顯小於 1000 行,因此其實不須要固定分配 1000 行的區間給每個 <script> 標籤。咱們能夠根據實際腳本行數來分配區間,只要保證每個 <script> 標籤所使用的區間互不重疊就能夠了。

crossorigin 屬性

瀏覽器對於不一樣源的內容進行的安全限制固然不只限於 <script> 標籤。既然 XMLHttpRequest 能夠經過 CORS 來突破這個限制,爲何直接經過標籤引用的資源就不能夠呢?這固然是能夠的。

針對 <script> 標籤引用不一樣源腳本文件的限制一樣做用於 <img> 標籤引用不一樣源圖片文件。若是一個 <img> 標籤是不一樣源的話,一旦在 <canvas> 繪圖時用到了,該 <canvas> 將變爲只寫狀態,保證網站不能經過 JavaScript 竊取未受權的不一樣源圖片數據。後來 <img> 標籤經過引入 crossorigin 屬性解決了這個問題。若是使用 crossorigin="anonymous",則至關於匿名 CORS;若是使用 `crossorigin=「use-credentials」,則至關於帶認證的 CORS。

既然 <img> 標籤能這樣作,爲何 <script> 標籤就不能這樣作?因而瀏覽器廠商就爲 <script> 標籤加入了一樣的 crossorigin 屬性用於解決上述安全限制問題。如今 Chrome 和 Firefox 對這個屬性的支持是徹底沒有問題的。Safari 則會把 crossorigin="anonymous" 當作 crossorigin="use-credentials" 處理,結果是若是服務器只支持匿名 CORS 則 Safari 會當作認證失敗。因爲 CDN 服務器出於性能的考慮被設計爲只能返回靜態內容,不可能動態的根據請求返回認證 CORS 所需的 HTTP Header,Safari 至關於不能利用此特性來解決上述問題。

總結

JavaScript 異常處理看起來很簡單,跟其它語言沒什麼區別,但真的要把異常都捕獲了而後對屬性作分析,其實還不是那麼容易的事情。如今儘管有一些第三方服務提供捕獲 JavaScript 異常的類 Google Analytics 服務,但若是要弄明白其中的細節和原理仍是必須本身親手作一次。

相關文章
相關標籤/搜索