考慮到 JS 中的錯誤可比服務(wù)器端的代碼產(chǎn)生的錯誤要多得多,并且還難以發(fā)現(xiàn)及修正,所以 JS 代碼必須有異常處理以及全局一場處理。
try { //這段代碼從上往下運行,其中任何一個語句拋出異常該代碼塊就結(jié)束運行} catch (e) { // 如果try代碼塊中拋出了異常,catch代碼塊中的代碼就會被執(zhí)行。 //e是一個局部變量,用來指向Error對象或者其他拋出的對象 } finally {//無論try中代碼是否有異常拋出(甚至是try代碼塊中有return語句),finally代碼塊中始終會被執(zhí)行。 }
一:Error屬性
Error有兩個基本的屬性 name 和 message 。message用來表示異常的詳細(xì)信息。而name指的是Error對象的構(gòu)造函數(shù)。此外,不同的js引擎對Error還各自提供了一些擴(kuò)展,例如mozilla提供了fileName(異常出現(xiàn)的文件名稱)和linenumber(異常出現(xiàn)的行號)的擴(kuò)展,而IE提供了number(錯誤號)的支持。不過name和message是兩個基本的屬性,在firefox和ie中都能夠支持。
二:Error類型
Javascript中Error還有幾個子類EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError,
EvalError:錯誤發(fā)生在eval()中
SyntaxError:語法錯誤,錯誤發(fā)生在eval()中,因為其它點發(fā)生SyntaxError會無法通過解釋器
RangeError:數(shù)值超出范圍
ReferenceError:引用不可用
TypeError:變量類型不是預(yù)期的
URIError:錯誤發(fā)生在encodeURI()或decodeURI()中
三:全局異常處理
javascript的window對象有一個特別的屬性onerror,如果你將某個function賦值給window的onerror屬性,那么但凡這個window中有javascript錯誤出現(xiàn),該function都會被調(diào)用,也就是說這個function會成為這個window的錯誤處理句柄。不過,需要注意的是,我們需要讓下面這段代碼成為文件中的第一行代碼:
window.onerror = function(msg, url, line) {
return true;
}
onerror句柄會3個參數(shù)分別是錯誤信息提示,產(chǎn)生錯誤的javascript的document ulr,錯誤出現(xiàn)的行號。
onerroe句柄的返回值也很重要,如果句柄返回true,表示瀏覽器無需在對該錯誤做額外的處理,也就是說瀏覽器不需要再顯示錯誤信息。而如果返回的是false,瀏覽器還是會提示錯誤信息。
前端工程師都知道 JavaScript 有基本的異常處理能力。我們可以
throw new Error()
,瀏覽器也會在我們調(diào)用 API 出錯時拋出異常。但估計絕大多數(shù)前端工程師都沒考慮過收集這些異常信息。反正只要 JavaScript 出錯后刷新不復(fù)現(xiàn),那用戶就可以通過刷新解決問題,瀏覽器不會崩潰,當(dāng)沒有發(fā)生過好了。這種假設(shè)在 Single Page App 流行之前還是成立的?,F(xiàn)在的 Single Page App 運行一段時間后狀態(tài)復(fù)雜無比,用戶可能進(jìn)行了若干輸入操作才來到這里的,說刷新就刷新啊?之前的操作豈不要完全重做?所以我們還是有必要捕獲和分析這些異常信息的,然后我們就可以修改代碼避免影響用戶體驗。捕獲異常的方式
我們自己寫的
throw new Error()
想要捕獲當(dāng)然可以捕獲,因為我們很清楚throw
寫在哪里了。但是調(diào)用瀏覽器 API 時發(fā)生的異常就不一定那么容易捕獲了,有些 API 在標(biāo)準(zhǔn)里就寫著會拋出異常,有些 API 只有個別瀏覽器因為實現(xiàn)差異或者有缺陷而拋出異常。對于前者我們還能通過try-catch
捕獲,對于后者我們必須監(jiān)聽全局的異常然后捕獲。try-catch
如果有些瀏覽器 API 是已知會拋出異常的,那我們就需要把調(diào)用放到
try-catch
里面,避免因為出錯而導(dǎo)致整個程序進(jìn)入非法狀態(tài)。例如說window.localStorage
就是這樣的一個 API,在寫入數(shù)據(jù)超過容量限制后就會拋出異常,在 Safari 的隱私瀏覽模式下也會如此。try { localStorage.setItem('date', Date.now()); } catch (error) { reportError(error); }
另一個常見的
try-catch
適用場景是回調(diào)。因為回調(diào)函數(shù)的代碼是我們不可控的,代碼質(zhì)量如何,會不會調(diào)用其它會拋出異常的 API,我們一概不知道。為了不要因為回調(diào)出錯而導(dǎo)致調(diào)用回調(diào)后的其它代碼無法執(zhí)行,所以把調(diào)用回到放到try-catch
里面是必須的。listeners.forEach(function(listener) { try { listener(); } catch (error) { reportError(error); } });
window.onerror
對于
try-catch
覆蓋不到的地方,如果出現(xiàn)異常就只能通過window.onerror
來捕獲了。window.onerror = function(errorMessage, scriptURI, lineNumber) { reportError({ message: errorMessage, script: scriptURI, line: lineNumber }); }
注意不要耍小聰明使用
window.addEventListener
或window.attachEvent
的形式去監(jiān)聽window.onerror
。很多瀏覽器只實現(xiàn)了window.onerror
,或者是只有window.onerror
的實現(xiàn)是標(biāo)準(zhǔn)的??紤]到標(biāo)準(zhǔn)草案定義的也是window.onerror
,我們使用window.onerror
就好了。屬性丟失
假設(shè)我們有一個
reportError
函數(shù)用來收集捕獲到的異常,然后批量發(fā)送到服務(wù)器端存儲以便查詢分析,那么我們會想要收集哪些信息呢?比較有用的信息包括:錯誤類型(name
)、錯誤消息(message
)、腳本文件地址(script
)、行號(line
)、列號(column
)、堆棧跟蹤(stack
)。如果一個異常是通過try-catch
捕獲到的,這些信息都在Error
對象上(主流瀏覽器都支持),所以reportError
也能收集到這些信息。但如果是通過window.onerror
捕獲到的,我們都知道這個事件函數(shù)只有 3 個參數(shù),所以這 3 個參數(shù)以外的信息就丟失了。序列化消息
如果
Error
對象是我們自己創(chuàng)建的話,那么error.message
就是由我們控制的?;旧衔覀儼咽裁捶胚M(jìn)error.message
里面,window.onerror
的第一個參數(shù)(message
)就會是什么。(瀏覽器其實會略作修改,例如加上'Uncaught Error: '
前綴。)因此我們可以把我們關(guān)注的屬性序列化(例如JSON.Stringify
)后存放到error.message
里面,然后在window.onerror
讀取出來反序列化就可以了。當(dāng)然,這僅限于我們自己創(chuàng)建的Error
對象。第五個參數(shù)
瀏覽器廠商也知道大家在使用
window.onerror
時受到的限制,所以開始往window.onerror
上面添加新的參數(shù)??紤]到只有行號沒有列號好像不是很對稱的樣子,IE 首先把列號加上了,放在第四個參數(shù)。然而大家更關(guān)心的是能否拿到完整的堆棧,于是 Firefox 說不如把堆棧放在第五個參數(shù)吧。但 Chrome 說那還不如把整個Error
對象放在第五個參數(shù),大家想讀取什么屬性都可以了,包括自定義屬性。結(jié)果由于 Chrome 動作比較快,在 Chrome 30 實現(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
對象屬性,其名稱都是基于 Chrome 命名方式的,然而不同瀏覽器對Error
對象屬性的命名方式各不相同,例如腳本文件地址在 Chrome 叫做script
但在 Firefox 叫做filename
。因此,我們還需要一個專門的函數(shù)來對Error
對象進(jìn)行正規(guī)化處理,也就是把不同的屬性名稱都映射到統(tǒng)一的屬性名稱上。盡管瀏覽器實現(xiàn)會更新,但人手維護(hù)一份這樣的映射表并不會太難。
類似的是堆棧跟蹤(
stack
)的格式。這個屬性以純文本的形式保存一份異常在發(fā)生時的堆棧信息,由于各個瀏覽器使用的文本格式不一樣,所以也需要人手維護(hù)一份正則表達(dá),用于從純文本中提取每一幀的函數(shù)名(identifier
)、文件(script
)、行號(line
)和列號(column
)。
安全限制
如果你也遇到過消息為
'Script error.'
的錯誤,你會明白我在說什么的,這其實是瀏覽器針對不同源(origin)腳本文件的限制。這個安全限制的理由是這樣的:假設(shè)一家網(wǎng)銀在用戶登錄后返回的 HTML 跟匿名用戶看到的 HTML 不一樣,一個第三方網(wǎng)站就能把這家網(wǎng)銀的 URI 放到script.src
屬性里面。HTML 當(dāng)然不可能被當(dāng)做 JS 解析啦,所以瀏覽器會拋出異常,而這個第三方網(wǎng)站就能通過解析異常的位置來判斷用戶是否有登錄。為此瀏覽器對于不同源腳本文件拋出的異常一律進(jìn)行過濾,過濾得只剩下'Script error.'
這樣一條不變的消息,其它屬性統(tǒng)統(tǒng)消失。對于有一定規(guī)模的網(wǎng)站來說,腳本文件放在 CDN 上,不同源是很正常的?,F(xiàn)在就算是自己做個小網(wǎng)站,常見框架如 jQuery 和 Backbone 都能直接引用公共 CDN 上的版本,加速用戶下載。所以這個安全限制確實造成了一些麻煩,導(dǎo)致我們從 Chrome 和 Firefox 收集到的異常信息都是無用的
'Script error.'
。
CORS
想要繞過這個限制,只要保證腳本文件和頁面本身同源即可。但把腳本文件放在不經(jīng) CDN 加速的服務(wù)器上,豈不降低用戶下載速度?一個解決方案是,腳本文件繼續(xù)放在 CDN 上,利用
XMLHttpRequest
通過 CORS 把內(nèi)容下載回來,再創(chuàng)建<script>
標(biāo)簽注入到頁面當(dāng)中。在頁面當(dāng)中內(nèi)嵌的代碼當(dāng)然是同源的啦。這說起來很簡單,但實現(xiàn)起來卻有很多細(xì)節(jié)問題。用一個簡單的例子來說:
<script src="http://cdn.com/step1.js"></script> <script> (function step2() {})(); </script> <script src="http://cdn.com/step3.js"></script>
我們都知道這個 step1、step2、step3 如果存在依賴關(guān)系的話,則必須嚴(yán)格按照這個順序執(zhí)行,否則就可能出錯。瀏覽器可以并行請求 step1 和 step3 的文件,但在執(zhí)行時順序是保證的。如果我們自己通過
XMLHttpRequest
獲取 step1 和 step3 的文件內(nèi)容,我們就需要自行保證其順序正確性。此外不要忘記了 step2,在 step1 以非阻塞形式下載的時候 step2 就可以被執(zhí)行了,所以我們還必須人為干預(yù) step2 讓它等待 step1 完成后再執(zhí)行。如果我們已經(jīng)有一整套工具來生成網(wǎng)站上不同頁面的
<script>
標(biāo)簽的話,我們就需要調(diào)整一下這套工具讓它對<script>
標(biāo)簽做出改動:<script> scheduleRemoteScript('http://cdn.com/step1.js'); </script> <script> scheduleInlineScript(function code() { (function step2() {})(); }); </script> <script> scheduleRemoteScript('http://cdn.com/step3.js'); </script>
我們需要實現(xiàn)
scheduleRemoteScript
和scheduleInlineScript
這兩個函數(shù),并且保證它們在第一個引用外部腳本文件的<script>
標(biāo)簽之前就被定義好,然后余下的<script>
標(biāo)簽都會被改寫成上面這種形式。注意原本立即執(zhí)行的step2
函數(shù)被放到了一個更大的code
函數(shù)里面了。code
函數(shù)并不會被執(zhí)行,它只是一個容器而已,這樣使得原本 step2 的代碼不需要轉(zhuǎn)義就能保留下來,但又不會被立即執(zhí)行。接下來我們還需要實現(xiàn)一套完整的機制,保證這些由
scheduleRemoteScript
根據(jù)地址下載回來的文件內(nèi)容和由scheduleInlineScript
直接獲取到的代碼能夠按照正確的順序一個接一個地執(zhí)行。詳細(xì)的代碼我就不在這里給出了,大家有興趣可以自己去實現(xiàn)。
行號反查
通過 CORS 獲取內(nèi)容再把代碼注入頁面能夠突破安全限制,但會引入一個新的問題,那就是行號沖突。原本通過
error.script
可以定位到唯一的腳本文件,再通過error.line
可以定位到唯一的行號?,F(xiàn)在由于都是頁面內(nèi)嵌的代碼,多個<script>
標(biāo)簽并不能通過error.script
來區(qū)分,然而每一個<script>
標(biāo)簽內(nèi)部的行號都是從 1 算起的,結(jié)果就導(dǎo)致我們無法利用異常信息定位錯誤所在的源代碼位置。為了避免行號沖突,我們可以浪費一些行號,使得每一個
<script>
標(biāo)簽中有實際代碼所使用的行號區(qū)間互相不重疊。舉個例子來說,假設(shè)每個<script>
標(biāo)簽中的實際代碼都不超過 1000 行,那么我可以讓第一個<script>
標(biāo)簽中的代碼占用第 1–1000 行,讓第二個<script>
標(biāo)簽中的代碼占用第 1001–2000 行(前面插入 1000 行空行),第三個<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)過這樣處理后,如果一個錯誤的
error.line
是3005
的話,那意味著實際的error.script
應(yīng)該是'http://cdn.com/step3.js'
,而實際的error.line
則應(yīng)該是5
。我們可以在之前提到的reportError
函數(shù)里面完成這項行號反查工作。當(dāng)然,由于我們沒辦法保證每一個腳本文件只有 1000 行,也有可能有些腳本文件明顯小于 1000 行,所以其實不需要固定分配 1000 行的區(qū)間給每一個
<script>
標(biāo)簽。我們可以根據(jù)實際腳本行數(shù)來分配區(qū)間,只要保證每一個<script>
標(biāo)簽所使用的區(qū)間互不重疊就可以了。
crossorigin 屬性
瀏覽器對于不同源的內(nèi)容進(jìn)行的安全限制當(dāng)然不僅限于
<script>
標(biāo)簽。既然XMLHttpRequest
可以通過 CORS 來突破這個限制,為什么直接通過標(biāo)簽引用的資源就不可以呢?這當(dāng)然是可以的。針對
<script>
標(biāo)簽引用不同源腳本文件的限制同樣作用于<img>
標(biāo)簽引用不同源圖片文件。如果一個<img>
標(biāo)簽是不同源的話,一旦在<canvas>
繪圖時用到了,該<canvas>
將變?yōu)橹粚憼顟B(tài),保證網(wǎng)站不能通過 JavaScript 竊取未授權(quán)的不同源圖片數(shù)據(jù)。后來<img>
標(biāo)簽通過引入crossorigin
屬性解決了這個問題。如果使用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
屬性用于解決上述安全限制問題?,F(xiàn)在 Chrome 和 Firefox 對這個屬性的支持是完全沒有問題的。Safari 則會把crossorigin="anonymous"
當(dāng)做crossorigin="use-credentials"
處理,結(jié)果是如果服務(wù)器只支持匿名 CORS 則 Safari 會當(dāng)做認(rèn)證失敗。由于 CDN 服務(wù)器出于性能的考慮被設(shè)計為只能返回靜態(tài)內(nèi)容,不可能動態(tài)的根據(jù)請求返回認(rèn)證 CORS 所需的 HTTP Header,Safari 相當(dāng)于不能利用此特性來解決上述問題。
更多建議: