1. 程式人生 > >一篇文章教你如何捕獲前端錯誤

一篇文章教你如何捕獲前端錯誤

本文首發於 vivo網際網路技術 微信公眾號 https://mp.weixin.qq.com/s/E51lKQOojsvhHvACIyXwhw
作者:黃文佳

常見錯誤的分類

對於使用者在訪問頁面時發生的錯誤,主要包括以下幾個型別:

1、js執行時錯誤

JavaScript程式碼在使用者瀏覽器中執行時,由於一些邊界情況、本地環境的不可控等因素,可能會存在js執行時錯誤。

而依賴客戶端的某些方法,由於相容性或者網路等問題,也有概率會出現執行時錯誤。

e.g: 下圖是當使用了未定義的變數"foo",導致產生js執行時錯誤時的上報資料:

2、資源載入錯誤

這裡的靜態資源包括js、css以及image等。現在的web專案,往往依賴了大量的靜態資源,而且一般也會有cdn存在。

如果某個節點出現問題導致某個靜態資源無法訪問,就需要能夠捕獲這種異常並進行上報,方便第一時間解決問題。

e.g: 下圖是圖片資源不存在時的上報資料:

3、未處理的promise錯誤

未使用catch捕獲的promise錯誤,往往都會存在比較大的風險。而編碼時有可能覆蓋的不夠全面,因此有必要監控未處理的promise錯誤並進行上報。

e.g: 下圖是promise請求介面發生錯誤後,未進行catch時的上報資料:

4、非同步請求錯誤(fetch與xhr)

非同步錯誤的捕獲分為兩個部分:一個是傳統的XMLHttpRequest,另一個是使用fetch api。

像axios和jQuery等庫就是在xhr上的封裝,而有些情況也可能會使用原生的fetch,因此對這兩種情況都要進行捕獲。

e.g: 下圖是xhr請求介面返回400時捕獲後的上報資料:

各個型別錯誤的捕獲方式

1、window.onerror與window.addEventListener('error')捕獲js執行時錯誤

使用window.onerror和window.addEventListener('error')都能捕獲,但是window.onerror含有詳細的error堆疊資訊,存在error.stack中,所以我們選擇使用onerror的方式對js執行時錯誤進行捕獲。

window.onerror = function (msg, url, lineNo, columnNo, error) {
    // 處理錯誤資訊
}
// demo
msg: Uncaught TypeError: Uncaught ReferenceError: a is not defined
error.statck: TypeError: ReferenceError: a is not defined at http://xxxx.js:1:13
window.addEventListener('error', event => (){ 
  // 處理錯誤資訊
}, false);
// true代表在捕獲階段呼叫,false代表在冒泡階段捕獲。使用true或false都可以,預設為false

2、資源載入錯誤使用addEventListener去監聽error事件捕獲

實現原理:當一項資源(如<img>或<script>)載入失敗,載入資源的元素會觸發一個Event介面的error事件,並執行該元素上的onerror()處理函式。

這些error事件不會向上冒泡到window,不過能被window.addEventListener在捕獲階段捕獲。

但這裡需要注意,由於上面提到了addEventListener也能夠捕獲js錯誤,因此需要過濾避免重複上報,判斷為資源錯誤的時候才進行上報。

window.addEventListener('error', event => (){ 
  // 過濾js error
  let target = event.target || event.srcElement;
  let isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
  if (!isElementTarget) return false;
  // 上報資源地址
  let url = target.src || target.href;
  console.log(url);
}, true);

3、未處理的promise錯誤處理方式

實現原理:當promise被reject並且錯誤資訊沒有被處理的時候,會丟擲一個unhandledrejection。

這個錯誤不會被window.onerror以及window.addEventListener('error')捕獲,但是有專門的window.addEventListener('unhandledrejection')方法進行捕獲處理。

window.addEventListener('rejectionhandled', event => {
  // 錯誤的詳細資訊在reason欄位
  // demo:settimeout error
  console.log(event.reason);
});

4、fetch與xhr錯誤的捕獲

對於fetch和xhr,我們需要通過改寫它們的原生方法,在觸發錯誤時進行自動化的捕獲和上報。

改寫fetch方法:

// fetch的處理
function _errorFetchInit () {
    if(!window.fetch) return;
    let _oldFetch = window.fetch;
    window.fetch = function () {
        return _oldFetch.apply(this, arguments)
        .then(res => {
            if (!res.ok) { // 當status不為2XX的時候,上報錯誤
            }
            return res;
        })
        // 當fetch方法錯誤時上報
        .catch(error => {
            // error.message,
            // error.stack
            // 丟擲錯誤並且上報
            throw error; 
        })
    }
}

對於XMLHttpRequest的重寫:

xhr改寫


// xhr的處理
function _errorAjaxInit () {
    let protocol = window.location.protocol;
    if (protocol === 'file:') return;
    // 處理XMLHttpRequest
    if (!window.XMLHttpRequest) {
        return;  
    }
    let xmlhttp = window.XMLHttpRequest;    
    // 儲存原生send方法
    let _oldSend = xmlhttp.prototype.send;
    let _handleEvent = function (event) {
        try {
            if (event && event.currentTarget && event.currentTarget.status !== 200) {
                    // event.currentTarget 即為構建的xhr例項
                    // event.currentTarget.response
                    // event.currentTarget.responseURL || event.currentTarget.ajaxUrl
                    // event.currentTarget.status
                    // event.currentTarget.statusText
                });
            }
        } catch (e) {va
            console.log('Tool\'s error: ' + e);
        }
    }
    xmlhttp.prototype.send = function () {
        this.addEventListener('error', _handleEvent); // 失敗
        this.addEventListener('load', _handleEvent);  // 完成
        this.addEventListener('abort', _handleEvent); // 取消
        return _oldSend.apply(this, arguments);
    }
}

關於responseURL 的說明

需要特別注意的是,當請求完全無法執行的時候,XMLHttpRequest會收到status=0 和 statusText=null的返回,此時responseURL也為空string。

另外在安卓4.4及以下版本的webview中,xhr物件也不存在responseURL屬性。

因此我們需要額外的改寫xhr的open方法,將傳入的url記錄下來,方便上報時帶上。

var _oldOpen = xmlhttp.prototype.open;
// 重寫open方法,記錄請求的url
xmlhttp.prototype.open = function (method, url) {
    _oldOpen.apply(this, arguments);
    this.ajaxUrl = url;
};

其他問題

1、其他框架,例如vue專案的錯誤捕獲

vue內部發生的錯誤會被Vue攔截,因此vue提供方法給我們處理vue元件內部發生的錯誤。

Vue.config.errorHandler = function (err, vm, info) {

2、script error的解決方式

"script error.”有時也被稱為跨域錯誤。當網站請求並執行一個託管在第三方域名下的指令碼時,就可能遇到該錯誤。最常見的情形是使用 CDN 託管 JS 資源。

其實這並不是一個 JavaScript Bug。出於安全考慮,瀏覽器會刻意隱藏其他域的 JS 檔案丟擲的具體錯誤資訊,這樣做可以有效避免敏感資訊無意中被不受控制的第三方指令碼捕獲。

因此,瀏覽器只允許同域下的指令碼捕獲具體錯誤資訊,而其他指令碼只知道發生了一個錯誤,但無法獲知錯誤的具體內容。

解決方案1:(推薦)

新增 crossorigin="anonymous" 屬性。

<script src="http://another-domain.com/app.js" crossorigin="anonymous"></script>

此步驟的作用是告知瀏覽器以匿名方式獲取目標指令碼。這意味著請求指令碼時不會向服務端傳送潛在的使用者身份資訊(例如 Cookies、HTTP 證書等)。

新增跨域 HTTP 響應頭:

Access-Control-Allow-Origin: *

或者

 Access-Control-Allow-Origin: http://test.com

**注意:**大部分主流 CDN 預設添加了 Access-Control-Allow-Origin 屬性。

完成上述兩步之後,即可通過 window.onerror 捕獲跨域指令碼的報錯資訊。

解決方案2

難以在 HTTP 請求響應頭中新增跨域屬性時,還可以考慮 try catch 這個備選方案。

在如下示例 HTML 頁面中加入 try catch:

<!doctype html>
<html>
<head>
    <title>Test page in http://test.com</title>
</head>
<body>
    <script src="http://another-domain.com/app.js"></script>
    // app.js裡面有一個foo方法,呼叫了不存在的bar方法
    <script>
    window.onerror = function (message, url, line, column, error) {
        console.log(message, url, line, column, error);
    }
    try {
        foo();
    } catch (e) {
        console.log(e);

        throw e;
    }
</script>
</body>
</html>

// 執行輸出結果如下:

=> ReferenceError: bar is not defined
at foo (http://another-domain.com/app.js:2:3)
at http://test.com/:15:3
=> "Script error.", "", 0, 0, undefined

可見 try catch 中的 Console 語句輸出了完整的資訊,但 window.onerror 中只能捕獲“Script error”。根據這個特點,可以在 catch 語句中手動上報捕獲的異常。

總結

上述的錯誤捕獲基本覆蓋了前端監控所需的錯誤場景,但是第三部分指出的兩個其他問題,目前解決的方式都不太完美。

對於有使用框架的專案:一是需要有額外的處理流程,比如示例中就需要單獨為vue專案進行初始化;二是對於其他框架,都需要單獨處理,例如react專案的話,則需要使用官方提供的componentDidCatch方法來做錯誤捕獲。

而對於跨域js捕獲的問題:我們並不能保證所有的跨域靜態資源都新增跨域 HTTP 響應頭;而通過第二種包裹try-catch的方式進行上報,則需要考慮的場景繁多並且無法保證沒有遺漏。

雖然存在這兩點不足,但前端錯誤捕獲這部分還是和專案的使用場景密切相關的。我們可以在瞭解這些方式以後,選擇最適合自己專案的方案,為自己的監控工具服務。

—— —— 參考文件 —— ——

1.Using XMLHttpRequest:

https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest

2.script error 產生的原因和解決辦法:

https://www.alibabacloud.com/help/zh/faq-detail/88579.htm

3.JavaScript執行錯誤:

https://docs.fundebug.com/notifier/javascript/type/javascript.html

4.betterjs的script error:

https://github.com/BetterJS/badjs-report/issues/3

5.Vuejs的errorHandler:

https://cn.vuejs.org/v2/api/index.html#errorHandler

6.React的componentDidCatch:

https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html

 

更多內容敬請關注 vivo 網際網路技術 微信公眾號

注:轉載文章請先與微訊號:labs2020 聯絡。