前言
本文目的:
能讓非前端同學大致瞭解下,現代『前端異常解析』是怎麼做的,以及大部分的坑會是哪些
對於專業的前端同學,本文中也許有些坑,你還沒有踩到,也可以看下。
從 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 屬性就好了,但還有一個必要條件是服務端要支援這個屬性。這個服務端包括:
- cdn 的伺服器。
- 低端的安卓系統版本(離線包攔截請求無法設定這個屬性)。
所以在開啟這個屬性前,一定要確認好以上兩個地方是否支援,不然的話,嘿嘿,白屏!
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 來反解析。