js的異常處理方法

2018-08-10 14:37 更新

考慮到 JS 中的錯(cuò)誤可比服務(wù)器端的代碼產(chǎn)生的錯(cuò)誤要多得多,并且還難以發(fā)現(xiàn)及修正,所以 JS 代碼必須有異常處理以及全局一場(chǎng)處理。

try {
  //這段代碼從上往下運(yùn)行,其中任何一個(gè)語(yǔ)句拋出異常該代碼塊就結(jié)束運(yùn)行
}
catch (e) {
  // 如果try代碼塊中拋出了異常,catch代碼塊中的代碼就會(huì)被執(zhí)行。
  //e是一個(gè)局部變量,用來(lái)指向Error對(duì)象或者其他拋出的對(duì)象
}
finally {
  //無(wú)論try中代碼是否有異常拋出(甚至是try代碼塊中有return語(yǔ)句),finally代碼塊中始終會(huì)被執(zhí)行。
}

一:Error屬性

Error有兩個(gè)基本的屬性 name 和 message 。message用來(lái)表示異常的詳細(xì)信息。而name指的是Error對(duì)象的構(gòu)造函數(shù)。此外,不同的js引擎對(duì)Error還各自提供了一些擴(kuò)展,例如mozilla提供了fileName(異常出現(xiàn)的文件名稱)和linenumber(異常出現(xiàn)的行號(hào))的擴(kuò)展,而IE提供了number(錯(cuò)誤號(hào))的支持。不過(guò)name和message是兩個(gè)基本的屬性,在firefox和ie中都能夠支持。


二:Error類型

Javascript中Error還有幾個(gè)子類EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError,

EvalError:錯(cuò)誤發(fā)生在eval()中 
SyntaxError:語(yǔ)法錯(cuò)誤,錯(cuò)誤發(fā)生在eval()中,因?yàn)槠渌c(diǎn)發(fā)生SyntaxError會(huì)無(wú)法通過(guò)解釋器 
RangeError:數(shù)值超出范圍 
ReferenceError:引用不可用 
TypeError:變量類型不是預(yù)期的 
URIError:錯(cuò)誤發(fā)生在encodeURI()或decodeURI()中

 

三:全局異常處理

javascript的window對(duì)象有一個(gè)特別的屬性onerror,如果你將某個(gè)function賦值給window的onerror屬性,那么但凡這個(gè)window中有javascript錯(cuò)誤出現(xiàn),該function都會(huì)被調(diào)用,也就是說(shuō)這個(gè)function會(huì)成為這個(gè)window的錯(cuò)誤處理句柄。不過(guò),需要注意的是,我們需要讓下面這段代碼成為文件中的第一行代碼:

window.onerror = function(msg, url, line) { 
        return true; 
}

onerror句柄會(huì)3個(gè)參數(shù)分別是錯(cuò)誤信息提示,產(chǎn)生錯(cuò)誤的javascript的document ulr,錯(cuò)誤出現(xiàn)的行號(hào)。

onerroe句柄的返回值也很重要,如果句柄返回true,表示瀏覽器無(wú)需在對(duì)該錯(cuò)誤做額外的處理,也就是說(shuō)瀏覽器不需要再顯示錯(cuò)誤信息。而如果返回的是false,瀏覽器還是會(huì)提示錯(cuò)誤信息。


前端工程師都知道 JavaScript 有基本的異常處理能力。我們可以 throw new Error(),瀏覽器也會(huì)在我們調(diào)用 API 出錯(cuò)時(shí)拋出異常。但估計(jì)絕大多數(shù)前端工程師都沒(méi)考慮過(guò)收集這些異常信息。反正只要 JavaScript 出錯(cuò)后刷新不復(fù)現(xiàn),那用戶就可以通過(guò)刷新解決問(wèn)題,瀏覽器不會(huì)崩潰,當(dāng)沒(méi)有發(fā)生過(guò)好了。這種假設(shè)在 Single Page App 流行之前還是成立的。現(xiàn)在的 Single Page App 運(yùn)行一段時(shí)間后狀態(tài)復(fù)雜無(wú)比,用戶可能進(jìn)行了若干輸入操作才來(lái)到這里的,說(shuō)刷新就刷新???之前的操作豈不要完全重做?所以我們還是有必要捕獲和分析這些異常信息的,然后我們就可以修改代碼避免影響用戶體驗(yàn)。

捕獲異常的方式

我們自己寫的 throw new Error() 想要捕獲當(dāng)然可以捕獲,因?yàn)槲覀兒芮宄?nbsp;throw 寫在哪里了。但是調(diào)用瀏覽器 API 時(shí)發(fā)生的異常就不一定那么容易捕獲了,有些 API 在標(biāo)準(zhǔn)里就寫著會(huì)拋出異常,有些 API 只有個(gè)別瀏覽器因?yàn)閷?shí)現(xiàn)差異或者有缺陷而拋出異常。對(duì)于前者我們還能通過(guò) try-catch 捕獲,對(duì)于后者我們必須監(jiān)聽全局的異常然后捕獲。

try-catch

如果有些瀏覽器 API 是已知會(huì)拋出異常的,那我們就需要把調(diào)用放到 try-catch 里面,避免因?yàn)槌鲥e(cuò)而導(dǎo)致整個(gè)程序進(jìn)入非法狀態(tài)。例如說(shuō) window.localStorage 就是這樣的一個(gè) API,在寫入數(shù)據(jù)超過(guò)容量限制后就會(huì)拋出異常,在 Safari 的隱私瀏覽模式下也會(huì)如此。

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

另一個(gè)常見(jiàn)的 try-catch 適用場(chǎng)景是回調(diào)。因?yàn)榛卣{(diào)函數(shù)的代碼是我們不可控的,代碼質(zhì)量如何,會(huì)不會(huì)調(diào)用其它會(huì)拋出異常的 API,我們一概不知道。為了不要因?yàn)榛卣{(diào)出錯(cuò)而導(dǎo)致調(diào)用回調(diào)后的其它代碼無(wú)法執(zhí)行,所以把調(diào)用回到放到 try-catch 里面是必須的。

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

對(duì)于 try-catch 覆蓋不到的地方,如果出現(xiàn)異常就只能通過(guò) window.onerror 來(lái)捕獲了。

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

注意不要耍小聰明使用 window.addEventListener 或 window.attachEvent 的形式去監(jiān)聽 window.onerror。很多瀏覽器只實(shí)現(xiàn)了 window.onerror,或者是只有 window.onerror 的實(shí)現(xiàn)是標(biāo)準(zhǔn)的??紤]到標(biāo)準(zhǔn)草案定義的也是 window.onerror,我們使用window.onerror 就好了。

屬性丟失

假設(shè)我們有一個(gè) reportError 函數(shù)用來(lái)收集捕獲到的異常,然后批量發(fā)送到服務(wù)器端存儲(chǔ)以便查詢分析,那么我們會(huì)想要收集哪些信息呢?比較有用的信息包括:錯(cuò)誤類型(name)、錯(cuò)誤消息(message)、腳本文件地址(script)、行號(hào)(line)、列號(hào)(column)、堆棧跟蹤(stack)。如果一個(gè)異常是通過(guò) try-catch 捕獲到的,這些信息都在 Error 對(duì)象上(主流瀏覽器都支持),所以 reportError 也能收集到這些信息。但如果是通過(guò) window.onerror 捕獲到的,我們都知道這個(gè)事件函數(shù)只有 3 個(gè)參數(shù),所以這 3 個(gè)參數(shù)以外的信息就丟失了。

序列化消息

如果 Error 對(duì)象是我們自己創(chuàng)建的話,那么 error.message 就是由我們控制的?;旧衔覀儼咽裁捶胚M(jìn) error.message 里面,window.onerror 的第一個(gè)參數(shù)(message)就會(huì)是什么。(瀏覽器其實(shí)會(huì)略作修改,例如加上 'Uncaught Error: ' 前綴。)因此我們可以把我們關(guān)注的屬性序列化(例如 JSON.Stringify)后存放到 error.message 里面,然后在 window.onerror 讀取出來(lái)反序列化就可以了。當(dāng)然,這僅限于我們自己創(chuàng)建的 Error 對(duì)象。

第五個(gè)參數(shù)

瀏覽器廠商也知道大家在使用 window.onerror 時(shí)受到的限制,所以開始往 window.onerror 上面添加新的參數(shù)??紤]到只有行號(hào)沒(méi)有列號(hào)好像不是很對(duì)稱的樣子,IE 首先把列號(hào)加上了,放在第四個(gè)參數(shù)。然而大家更關(guān)心的是能否拿到完整的堆棧,于是 Firefox 說(shuō)不如把堆棧放在第五個(gè)參數(shù)吧。但 Chrome 說(shuō)那還不如把整個(gè) Error 對(duì)象放在第五個(gè)參數(shù),大家想讀取什么屬性都可以了,包括自定義屬性。結(jié)果由于 Chrome 動(dòng)作比較快,在 Chrome 30 實(shí)現(xiàn)了新的 window.onerror 簽名,導(dǎo)致標(biāo)準(zhǔn)草案也就跟著這樣寫了。

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

屬性正規(guī)化

我們之前討論到的 Error 對(duì)象屬性,其名稱都是基于 Chrome 命名方式的,然而不同瀏覽器對(duì) Error 對(duì)象屬性的命名方式各不相同,例如腳本文件地址在 Chrome 叫做 script 但在 Firefox 叫做 filename。因此,我們還需要一個(gè)專門的函數(shù)來(lái)對(duì)Error 對(duì)象進(jìn)行正規(guī)化處理,也就是把不同的屬性名稱都映射到統(tǒng)一的屬性名稱上。盡管瀏覽器實(shí)現(xiàn)會(huì)更新,但人手維護(hù)一份這樣的映射表并不會(huì)太難。


類似的是堆棧跟蹤(stack)的格式。這個(gè)屬性以純文本的形式保存一份異常在發(fā)生時(shí)的堆棧信息,由于各個(gè)瀏覽器使用的文本格式不一樣,所以也需要人手維護(hù)一份正則表達(dá),用于從純文本中提取每一幀的函數(shù)名(identifier)、文件(script)、行號(hào)(line)和列號(hào)(column)。


安全限制

如果你也遇到過(guò)消息為 'Script error.' 的錯(cuò)誤,你會(huì)明白我在說(shuō)什么的,這其實(shí)是瀏覽器針對(duì)不同源(origin)腳本文件的限制。這個(gè)安全限制的理由是這樣的:假設(shè)一家網(wǎng)銀在用戶登錄后返回的 HTML 跟匿名用戶看到的 HTML 不一樣,一個(gè)第三方網(wǎng)站就能把這家網(wǎng)銀的 URI 放到 script.src 屬性里面。HTML 當(dāng)然不可能被當(dāng)做 JS 解析啦,所以瀏覽器會(huì)拋出異常,而這個(gè)第三方網(wǎng)站就能通過(guò)解析異常的位置來(lái)判斷用戶是否有登錄。為此瀏覽器對(duì)于不同源腳本文件拋出的異常一律進(jìn)行過(guò)濾,過(guò)濾得只剩下 'Script error.' 這樣一條不變的消息,其它屬性統(tǒng)統(tǒng)消失。

對(duì)于有一定規(guī)模的網(wǎng)站來(lái)說(shuō),腳本文件放在 CDN 上,不同源是很正常的?,F(xiàn)在就算是自己做個(gè)小網(wǎng)站,常見(jiàn)框架如 jQuery 和 Backbone 都能直接引用公共 CDN 上的版本,加速用戶下載。所以這個(gè)安全限制確實(shí)造成了一些麻煩,導(dǎo)致我們從 Chrome 和 Firefox 收集到的異常信息都是無(wú)用的 'Script error.'。


CORS

想要繞過(guò)這個(gè)限制,只要保證腳本文件和頁(yè)面本身同源即可。但把腳本文件放在不經(jīng) CDN 加速的服務(wù)器上,豈不降低用戶下載速度?一個(gè)解決方案是,腳本文件繼續(xù)放在 CDN 上,利用 XMLHttpRequest 通過(guò) CORS 把內(nèi)容下載回來(lái),再創(chuàng)建 <script>標(biāo)簽注入到頁(yè)面當(dāng)中。在頁(yè)面當(dāng)中內(nèi)嵌的代碼當(dāng)然是同源的啦。

這說(shuō)起來(lái)很簡(jiǎn)單,但實(shí)現(xiàn)起來(lái)卻有很多細(xì)節(jié)問(wèn)題。用一個(gè)簡(jiǎn)單的例子來(lái)說(shuō):

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

我們都知道這個(gè) step1、step2、step3 如果存在依賴關(guān)系的話,則必須嚴(yán)格按照這個(gè)順序執(zhí)行,否則就可能出錯(cuò)。瀏覽器可以并行請(qǐng)求 step1 和 step3 的文件,但在執(zhí)行時(shí)順序是保證的。如果我們自己通過(guò) XMLHttpRequest 獲取 step1 和 step3 的文件內(nèi)容,我們就需要自行保證其順序正確性。此外不要忘記了 step2,在 step1 以非阻塞形式下載的時(shí)候 step2 就可以被執(zhí)行了,所以我們還必須人為干預(yù) step2 讓它等待 step1 完成后再執(zhí)行。

如果我們已經(jīng)有一整套工具來(lái)生成網(wǎng)站上不同頁(yè)面的 <script> 標(biāo)簽的話,我們就需要調(diào)整一下這套工具讓它對(duì) <script> 標(biāo)簽做出改動(dòng):

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

我們需要實(shí)現(xiàn) scheduleRemoteScript 和 scheduleInlineScript 這兩個(gè)函數(shù),并且保證它們?cè)诘谝粋€(gè)引用外部腳本文件的 <script> 標(biāo)簽之前就被定義好,然后余下的 <script> 標(biāo)簽都會(huì)被改寫成上面這種形式。注意原本立即執(zhí)行的 step2 函數(shù)被放到了一個(gè)更大的 code 函數(shù)里面了。code 函數(shù)并不會(huì)被執(zhí)行,它只是一個(gè)容器而已,這樣使得原本 step2 的代碼不需要轉(zhuǎn)義就能保留下來(lái),但又不會(huì)被立即執(zhí)行。

接下來(lái)我們還需要實(shí)現(xiàn)一套完整的機(jī)制,保證這些由 scheduleRemoteScript 根據(jù)地址下載回來(lái)的文件內(nèi)容和由 scheduleInlineScript 直接獲取到的代碼能夠按照正確的順序一個(gè)接一個(gè)地執(zhí)行。詳細(xì)的代碼我就不在這里給出了,大家有興趣可以自己去實(shí)現(xiàn)。


行號(hào)反查

通過(guò) CORS 獲取內(nèi)容再把代碼注入頁(yè)面能夠突破安全限制,但會(huì)引入一個(gè)新的問(wèn)題,那就是行號(hào)沖突。原本通過(guò) error.script 可以定位到唯一的腳本文件,再通過(guò) error.line 可以定位到唯一的行號(hào)?,F(xiàn)在由于都是頁(yè)面內(nèi)嵌的代碼,多個(gè) <script> 標(biāo)簽并不能通過(guò) error.script 來(lái)區(qū)分,然而每一個(gè) <script> 標(biāo)簽內(nèi)部的行號(hào)都是從 1 算起的,結(jié)果就導(dǎo)致我們無(wú)法利用異常信息定位錯(cuò)誤所在的源代碼位置。

為了避免行號(hào)沖突,我們可以浪費(fèi)一些行號(hào),使得每一個(gè) <script> 標(biāo)簽中有實(shí)際代碼所使用的行號(hào)區(qū)間互相不重疊。舉個(gè)例子來(lái)說(shuō),假設(shè)每個(gè) <script> 標(biāo)簽中的實(shí)際代碼都不超過(guò) 1000 行,那么我可以讓第一個(gè) <script> 標(biāo)簽中的代碼占用第 1–1000 行,讓第二個(gè) <script> 標(biāo)簽中的代碼占用第 1001–2000 行(前面插入 1000 行空行),第三個(gè) <script> 標(biāo)簽種的代碼占用第 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>

經(jīng)過(guò)這樣處理后,如果一個(gè)錯(cuò)誤的 error.line 是 3005 的話,那意味著實(shí)際的 error.script 應(yīng)該是 'http://cdn.com/step3.js',而實(shí)際的 error.line 則應(yīng)該是 5。我們可以在之前提到的 reportError 函數(shù)里面完成這項(xiàng)行號(hào)反查工作。

當(dāng)然,由于我們沒(méi)辦法保證每一個(gè)腳本文件只有 1000 行,也有可能有些腳本文件明顯小于 1000 行,所以其實(shí)不需要固定分配 1000 行的區(qū)間給每一個(gè) <script> 標(biāo)簽。我們可以根據(jù)實(shí)際腳本行數(shù)來(lái)分配區(qū)間,只要保證每一個(gè) <script> 標(biāo)簽所使用的區(qū)間互不重疊就可以了。


crossorigin 屬性

瀏覽器對(duì)于不同源的內(nèi)容進(jìn)行的安全限制當(dāng)然不僅限于 <script> 標(biāo)簽。既然 XMLHttpRequest 可以通過(guò) CORS 來(lái)突破這個(gè)限制,為什么直接通過(guò)標(biāo)簽引用的資源就不可以呢?這當(dāng)然是可以的。

針對(duì) <script> 標(biāo)簽引用不同源腳本文件的限制同樣作用于 <img> 標(biāo)簽引用不同源圖片文件。如果一個(gè) <img> 標(biāo)簽是不同源的話,一旦在 <canvas> 繪圖時(shí)用到了,該 <canvas> 將變?yōu)橹粚憼顟B(tài),保證網(wǎng)站不能通過(guò) JavaScript 竊取未授權(quán)的不同源圖片數(shù)據(jù)。后來(lái) <img> 標(biāo)簽通過(guò)引入 crossorigin 屬性解決了這個(gè)問(wèn)題。如果使用 crossorigin="anonymous",則相當(dāng)于匿名 CORS;如果使用 `crossorigin=“use-credentials”,則相當(dāng)于帶認(rèn)證的 CORS。

既然 <img> 標(biāo)簽?zāi)苓@樣做,為什么 <script> 標(biāo)簽就不能這樣做?于是瀏覽器廠商就為 <script> 標(biāo)簽加入了同樣的 crossorigin 屬性用于解決上述安全限制問(wèn)題。現(xiàn)在 Chrome 和 Firefox 對(duì)這個(gè)屬性的支持是完全沒(méi)有問(wèn)題的。Safari 則會(huì)把crossorigin="anonymous" 當(dāng)做 crossorigin="use-credentials" 處理,結(jié)果是如果服務(wù)器只支持匿名 CORS 則 Safari 會(huì)當(dāng)做認(rèn)證失敗。由于 CDN 服務(wù)器出于性能的考慮被設(shè)計(jì)為只能返回靜態(tài)內(nèi)容,不可能動(dòng)態(tài)的根據(jù)請(qǐng)求返回認(rèn)證 CORS 所需的 HTTP Header,Safari 相當(dāng)于不能利用此特性來(lái)解決上述問(wèn)題。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)