首頁 > web前端 > js教程 > 主體

JavaScript 異常處理 詳解_javascript技巧

WBOY
發布: 2016-05-16 16:15:30
原創
903 人瀏覽過

  前端工程師都知道 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 把內容下載回來,再建立 <script> 標籤注入到頁面當中。在頁面當中內嵌的程式碼當然是同源的啦。 </script>

這說起來很簡單,但實作起來卻有很多細節問題。用一個簡單的例子來說:

複製程式碼 程式碼如下:

>

 
我們都知道這個 step1、step2、step3 如果有依賴關係的話,則必須嚴格按照這個順序執行,否則就可能出錯。瀏覽器可以並行請求 step1 和 step3 的文件,但在執行時順序是可以保證的。如果我們自己透過 XMLHttpRequest 取得 step1 和 step3 的檔案內容,我們就需要自行保證其順序正確性。另外不要忘記了 step2,在 step1 以非阻塞形式下載的時候 step2 就可以被執行了,所以我們還必須人為幹預 step2 讓它等待 step1 完成後再執行。

如果我們已經有一整套工具來產生網站上不同頁面的 <script> 標籤的話,我們就需要調整一下這套工具讓它對 <script> 標籤做出改變:</script>

複製程式碼 程式碼如下:

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

 
我們需要實作 scheduleRemoteScript 和 scheduleInlineScript 這兩個函數,並且保證它們在第一個引用外部腳本檔案的 <script> 標籤之前就被定義好,然後餘下的 <script> 標籤都會被改寫成上面這種形式。注意原本立即執行的 step2 函數被放到了一個更大的 code 函數裡面了。 code 函數並不會被執行,它只是一個容器而已,這使得原本 step2 的程式碼不需要轉義就能保留下來,但又不會被立即執行。 <p>接下來我們還需要實現一套完整的機制,保證這些由 scheduleRemoteScript 根據地址下載回來的檔案內容和由 scheduleInlineScript 直接獲取到的程式碼能夠按照正確的順序一個接一個地執行。詳細的程式碼我就不在這裡給了,大家有興趣可以自己去實作。 <p><strong>行號反查 <p>透過 CORS 取得內容再把程式碼注入頁面能夠突破安全限制,但會引入一個新的問題,那就是行號衝突。原本透過 error.script 可以定位到唯一的腳本文件,再透過 error.line 可以定位到唯一的行號。現在由於都是頁面內嵌的程式碼,多個<script> 標籤並不能透過error.script 來區分,然而每一個<script> 標籤內部的行號都是從1 算起的,結果就導致我們無法利用異常訊息定位錯誤所在的原始碼位置。 <p>為了避免行號衝突,我們可以浪費一些行號,使得每一個 <script> 標籤中有實際程式碼所使用的行號區間互相不重疊。舉個例子來說,假設每個<script> 標籤中的實際程式碼都不超過1000 行,那麼我可以讓第一個<script> 標籤中的程式碼佔用第1–1000 行,讓第二個<script > 標籤中的程式碼佔用第1001–2000 行(前面插入1000 行空行),第三個<script> 標籤種的程式碼佔用第2001–3000 行(前面插入2000 行空行),以此類推。然後我們使用 data-* 屬性記錄這些資訊,以便於反查。 <p><div class="codetitle"><span><a style="CURSOR: pointer" data="94831" class="copybut" id="copybut94831" onclick="doCopy('code94831')"><U>複製程式碼 程式碼如下:<div class="codebody" id="code94831"><br /> <script<br />  data-src="<a href="http://cdn.com/step1.js">http://cdn.com/step1.js"<br />  data-line-start="1"<br /> ><br />  // code for step 1<br /> </script>

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


 
經過這樣處理後,如果一個錯誤的error.line 是3005 的話,那意味著實際的error.script 應該是'http://cdn.com/step3.js',而實際的error.line 則應該是5 。我們可以在之前提到的 reportError 函數裡面完成這項行號反查工作。

當然,由於我們沒辦法保證每個腳本檔案只有 1000 行,也有可能有些腳本檔案明顯小於 1000 行,所以其實不需要固定分配 1000 行的區間給每一個 <script> 標籤。我們可以根據實際腳本行數來分配區間,只要保證每一個 <script> 標籤所使用的區間互不重疊就可以了。 </script>

crossorigin 屬性

瀏覽器對於不同來源的內容進行的安全限制當然不僅限於 <script> 標籤。既然 XMLHttpRequest 可以透過 CORS 來突破這個限制,為什麼直接透過標籤引用的資源就不行呢?這當然是可以的。 </script>

針對 <script> 標籤引用不同來源腳本檔案的限制同樣作用於 <img alt="JavaScript 異常處理 詳解_javascript技巧" > 標籤引用不同來源圖片檔案。如果一個 <img alt="JavaScript 異常處理 詳解_javascript技巧" > 標籤是不同來源的話,一旦在 <canvas> 繪圖時用到了,該 <canvas> 將變成只寫狀態,保證網站不能透過 JavaScript 竊取未經授權的不同來源圖片資料。後來 <img alt="JavaScript 異常處理 詳解_javascript技巧" > 標籤透過引入 crossorigin 屬性解決了這個問題。如果使用 crossorigin="anonymous",則相當於匿名 CORS;如果使用 `crossorigin=“use-credentials”,則相當於具有認證的 CORS。 </script>

既然 JavaScript 異常處理 詳解_javascript技巧 標籤能這樣做,為什麼 <script> 標籤就不能這樣做?於是瀏覽器廠商就為 <script> 標籤加入了相同的 crossorigin 屬性來解決上述安全限制問題。現在 Chrome 和 Firefox 對這個屬性的支援是完全沒有問題的。 Safari 則會把 crossorigin="anonymous" 當做 crossorigin="use-credentials" 處理,結果是如果伺服器只支援匿名 CORS 則 Safari 會當做認證失敗。由於 CDN 伺服器出於效能的考量而設計為只能傳回靜態內容,因此不可能動態的根據請求返回認證 CORS 所需的 HTTP Header,Safari 相當於不能利用此特性來解決上述問題。 </script>

總結

JavaScript 異常處理看起來很簡單,跟其它語言沒什麼區別,但真的要把異常都捕獲了然後對屬性做分析,其實還不是那麼容易的事情。現在儘管有一些第三方服務提供捕捉 JavaScript 異常的類 Google Analytics 服務,但如果要弄清楚其中的細節和原理還是必須自己親手做一次。

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!