首頁 web前端 js教程 JavaScript 異常處理 詳解_javascript技巧

JavaScript 異常處理 詳解_javascript技巧

May 16, 2016 pm 04:15 PM
javascript 例外處理

  前端工程師都知道 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 服務,但如果要弄清楚其中的細節和原理還是必須自己親手做一次。

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

C++ 函式異常與多執行緒:並發環境下的錯誤處理 C++ 函式異常與多執行緒:並發環境下的錯誤處理 May 04, 2024 pm 04:42 PM

C++中函數異常處理對於多執行緒環境特別重要,以確保執行緒安全性和資料完整性。透過try-catch語句,可以在出現異常時擷取和處理特定類型的異常,以防止程式崩潰或資料損壞。

C++ 異常處理如何支援自訂錯誤處理例程? C++ 異常處理如何支援自訂錯誤處理例程? Jun 05, 2024 pm 12:13 PM

C++異常處理允許建立自訂錯誤處理例程,透過拋出異常並使用try-catch區塊捕捉異常來處理運行時錯誤。 1.建立一個派生自exception類別的自訂異常類別並覆寫what()方法;2.使用throw關鍵字拋出異常;3.使用try-catch區塊捕捉異常並指定可以處理的異常類型。

Java函數中遞歸呼叫與異常處理有何關係? Java函數中遞歸呼叫與異常處理有何關係? May 03, 2024 pm 06:12 PM

遞歸呼叫中的異常處理:限制遞歸深度:防止堆疊溢位。使用異常處理:使用try-catch語句處理異常。尾遞歸優化:避免堆疊溢位。

C++ Lambda 表達式如何進行異常處理? C++ Lambda 表達式如何進行異常處理? Jun 03, 2024 pm 03:01 PM

C++Lambda表達式中的異常處理沒有自己的作用域,預設不捕獲異常。要捕獲異常,可以使用Lambda表達式捕獲語法,它允許Lambda表達式捕獲其定義範圍內的變量,從而在try-catch區塊中進行異常處理。

C++ 技術中的例外處理:如何在多執行緒環境中正確處理例外狀況? C++ 技術中的例外處理:如何在多執行緒環境中正確處理例外狀況? May 09, 2024 pm 12:36 PM

在多執行緒C++中,例外處理遵循以下原則:及時性、執行緒安全性和明確性。在實戰中,可以透過使用mutex或原子變數來確保異常處理程式碼線程安全。此外,還要考慮異常處理程式碼的重入性、效能和測試,以確保其在多執行緒環境中安全有效地運作。

PHP異常處理:透過異常追蹤了解系統行為 PHP異常處理:透過異常追蹤了解系統行為 Jun 05, 2024 pm 07:57 PM

PHP異常處理:透過異常追蹤了解系統行為異常是PHP用來處理錯誤的機制,由異常處理程序處理異常。異常類別Exception代表一般異常,而Throwable類別代表所有異常。使用throw關鍵字拋出異常,並使用try...catch語句定義異常處理程序。在實戰案例中,透過異常處理捕獲並處理calculate()函數可能拋出的DivisionByZeroError,確保應用程式在發生錯誤時也能優雅地失敗。

您如何在PHP中有效處理異常(嘗試,捕捉,最後,投擲)? 您如何在PHP中有效處理異常(嘗試,捕捉,最後,投擲)? Apr 05, 2025 am 12:03 AM

在PHP中,異常處理通過try,catch,finally,和throw關鍵字實現。 1)try塊包圍可能拋出異常的代碼;2)catch塊處理異常;3)finally塊確保代碼始終執行;4)throw用於手動拋出異常。這些機制幫助提升代碼的健壯性和可維護性。

C++ 技術中的異常處理:如何最佳化異常處理的效能? C++ 技術中的異常處理:如何最佳化異常處理的效能? May 09, 2024 am 10:39 AM

為了優化C++中的異常處理效能,可以實現以下四種技術:避免不必要的異常拋出。使用輕量級異常類別。優先考慮效率,設計只包含必要資訊的異常類別。利用編譯器選項實現最佳效能和穩定性平衡。

See all articles