那些年,我們解析過的前端異常

JavaDog發表於2019-02-27

前言

本文目的:

能讓非前端同學大致瞭解下,現代『前端異常解析』是怎麼做的,以及大部分的坑會是哪些

對於專業的前端同學,本文中也許有些坑,你還沒有踩到,也可以看下。

從 window.onerror 說起

相信大部分接觸過『前端錯誤監控』話題的同學都知道,通過瀏覽器提供的 window.onerror 能夠捕獲大部分的前端異常,比如 js 報錯(本文討論的話題),assets 載入等。

對於 js 報錯,這個 api 會提供出報錯訊息,原始檔,lineno, colno(行號列號),詳細的錯誤堆疊等訊息,如下所示:

window.onerror = function(message, source, lineno, colno, error) { ... }
複製程式碼

我們拿到這些訊息後就能知道,『哦,原來的我的某個 js 裡的第 x 行,第 y 列,我寫錯了啊』。

但是,事情真的這麼簡單嗎?

反解壓縮過的 js 程式碼

首先第一個現實問題,我線上的 js 程式碼一定是經過壓縮(壓縮檔案傳輸)的。所以,在 window.onerror 這個方法裡拿到的 lineno 通常是 1,colno 通常是 123456 (一個很大的數字),然後當你定位到對應原始碼位置的時候,通常看到這類程式碼:

什麼鬼?js 的壓縮程式碼不僅會壓縮程式碼行數,還會對某些可替換的臨時變數名做重新命名,換成簡短的形式。所以正常靠肉眼要定位到真正錯誤的原始碼是很難很費勁的。

source-map 登場

這個時候我們第一個王牌登場,既然知道是怎麼壓縮的,那這個壓縮的對應關係也一定是有的,這個對應關係就是 source-map。

然後我們通過一些反解析 source-map 的工具,把原始碼的 lineno, colno(行號,列號) 和 source-map 檔案輸入,就能得到知道哪行原始碼寫錯了,類似效果如下:

一些安全性的考慮

通常出於安全性考慮,source-map 檔案是不會隨著 js 資原始檔一起釋出的。js 資源的釋出又是很頻繁的,所以會造成大量 souce-map 檔案管理的問題,後面會講到一種方案。

現在看似我們能夠 cover 住所有的前端 js 異常了。但是事實上是,如果你的前端資源是通過 cdn 的方式來部署的話,你會收到很多 script error(可能 80% 都是這個錯誤).

script error

script error 大部分是瀏覽器 跨域 安全限制導致的。通常的解決方式是,對跨域的指令碼加上 crossorigin 屬性就好了,但還有一個必要條件是服務端要支援這個屬性。這個服務端包括:

  1. cdn 的伺服器。
  2. 低端的安卓系統版本(離線包攔截請求無法設定這個屬性)。

所以在開啟這個屬性前,一定要確認好以上兩個地方是否支援,不然的話,嘿嘿,白屏!

ok,到現在我們基本解決 window.onerror 報上來的問題了。

那還有其他的前端異常嗎?

成也框架,敗也框架

近些年,隨著前端工程化,元件化的流行,越來越多的 MVVM 的框架登上歷史舞臺(vue,react,angular 等)。

對前端監控來說,拿 vue 來舉例。

你寫的 vue 業務程式碼,會發現 window.onerror 是監控不到的。原因是這些框架他們內部會消化掉這些錯誤,必須得通過他們的 API 才能 catch 到。比如:

Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的錯誤資訊,比如錯誤所在的生命週期鉤子
}
複製程式碼

重點注意一下,這個函式不像 window.onerror 會把錯誤的 lineno, colno 一起上報。只能看到一個乾巴巴的錯誤訊息,比如:

TypeError: Cannot read property 'id' of null
    at a.next (https://xxx.com/0-1.0.0-5ba93bf.js:1:25746)
    at n (https://xxx.com/x2-1.0.0-5ba93bf.js:13:1417)
複製程式碼

這是線上程式碼,同樣也是經過壓縮的,而且由於被框架程式碼包了一層,在壓縮過的程式碼裡定位變得更加費勁。

那有什麼方法可以拿到 lineno, colno 然後通過 source-map 反解析原始碼麼?

答案其實就在上面,在錯誤堆疊裡。仔細看 https://xxx.com/0-1.0.0-5ba93bf.js:1:25746,按照 js stack 的生成規則,通常*.js:x:y 第一個冒號後面的 x 代表了 lineno 行數,第二個冒號後面的 y 代表了 colno 列數。

所以我們通過肉眼或者是可以正則解析出 lineno, colno 的,然後通過 source-map 工具能夠定位出具體的 vue 程式碼出錯位置了。

同樣要問一個,那還有其他的前端異常了嗎?

明明白屏了,咋不報錯呢

假設不小心,寫了如下的 vue 程式碼。

<template>
</template>

<script>
export default {
 mounted () {
   const somePromise = new Promise(_ => {
     let a
     a.doingNothing // 空指標,出錯啦!!!
   })

   somePromise.then(console.log)
 }
}
</script>
複製程式碼

明明裡面有一個空指標會導致白屏的問題,可是你會發現 window.onerror 捕捉不到,Vue.config.errorHandler 同樣也捕捉不到(不是說好框架全吃的嗎!)。

仔細看,哦,原來這是寫在 promise 裡的程式碼啊,這個程式碼被這個 promise 吃掉了,因為這裡沒有 catch,所以也就不知道了。

隨著 js 語言的發展,越來越多的特性被引入到瀏覽器中,按照新的規範,像 promise 裡面的錯誤應該 promise 由開發者自己負責來 catch 而不會被 window.onerror 捕獲。

但是理想很豐滿,現實卻還是 ---- 不能保證每個人都會寫 catch。

所以對於 promise 的錯誤,我們需要做全域性監聽,程式碼如下:

window.addEventListener('unhandledrejection', function (event) {
  const error = event && event.reason
  console && console.warn && console.warn('WARNING: Unhandled promise rejection. Shame on you! Reason: ' + error)

  // ... report error
}
複製程式碼

同樣對於這裡沒有上報 lineno, colno,但是有 error stack,我們可以使用同樣的套路對他們用 source-map 來反解析。

那些年,我們解析過的前端異常


相關文章