萬盛學電腦網

 萬盛學電腦網 >> 腳本專題 >> javascript >> JS高級調試技巧:捕獲和分析 JavaScript Error詳解

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

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

  反正只要 JavaScript 出錯後刷新不復現,那用戶就可以通過刷新解決問題,浏覽器不會崩潰,當沒有發生過好了。這種假設在 Single Page App 流行之前還是成立的。現在的 Single Page App 運行一段時間後狀態復雜無比,用戶可能進行了若干輸入操作才來到這裡的,說刷新就刷新啊?之前的操作豈不要完全重做?所以我們還是有必要捕獲和分析這些異常信息的,然後我們就可以修改代碼避免影響用戶體驗。

  捕獲異常的方式

  我們自己寫的 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 個參數意外的信息就丟失了。

  序列化消息

  如果 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 把內容下載回來,再創建 我們都知道這個 step1、step2、step3 如果存在依賴關系的話,則必須嚴格按照這個順序執行,否則就可能出錯。浏覽器可以並行請求 step1 和 step3 的文件,但在執行時順序是保證的。如果我們自己通過 XMLHttpRequest 獲取 step1 和 step3 的文件內容,我們就需要自行保證其順序正確性。此外不要忘記了 step2,在 step1 以非阻塞形式下載的時候 step2 就可以被執行了,所以我們還必須人為干預 step2 讓它等待 step1 完成後再執行。

  如果我們已經有一整套工具來生成網站上不同頁面的 我們需要實現 scheduleRemoteScript 和 scheduleInlineScript 這兩個函數,並且保證它們在第一個引用外部腳本文件的 經過這樣處理後,如果一個錯誤的 error.line 是 3005 的話,那意味著實際的 error.script 應該是 'http://cdn.com/step3.js',而實際的 error.line 則應該是 5。我們可以在之前提到的 reportError 函數裡面完成這項行號反查工作。

  當然,由於我們沒辦法保證每一個腳本文件只有 1000 行,也有可能有些腳本文件明顯小於 1000 行,所以其實不需要固定分配 1000 行的區間給每一個 

copyright © 萬盛學電腦網 all rights reserved