淺談前端錯誤處理

ixlei發表於2018-11-14

使用者反饋開啟的頁面白螢幕,怎麼定位到產生錯誤的原因呢?日常某次釋出怎麼確定釋出會沒有引入bug呢?此時捕獲到程式碼執行的bug並上報是多麼的重要。

既然捕獲錯誤並上報是日常開發中不可缺少的一環,那怎麼捕獲到錯誤呢?萬能的try...catch


try{

throw new Error()

} catch(e) {

// handle error

}
複製程式碼

看上去錯誤捕獲是多麼的簡單,然而下面的場景下就不能捕獲到了


try {

setTimeout(() => {

throw new Error('error')

})

} catch (e) {

// handle error

}
複製程式碼

你會發現上面的例子中的錯誤不能正常捕獲,看來錯誤捕獲並不是這樣簡單**try...catch**就能搞定,當然你也可以為非同步函式包裹一層**try...catch**來處理。

瀏覽器中,**window.onerror**來捕獲你的錯誤


window.onerror = function (msg, url, row, col, error) {

console.log('error');

console.log({

msg, url, row, col, error

})

};
複製程式碼

捕獲到錯誤後就可以將錯誤上報,上報方式很簡單,你可以通過建立簡單的**img**,通過**src**指定上報的地址,當然為了避免上報傳送過多的請求,可以對上報進行合併,合併上報。可以定時將資料進行上報到服務端。

但但你去看錯誤上報的資訊的時候,你會發現一些這樣的錯誤**Script error**

因為瀏覽器的同源策略,對於不同域名的錯誤,都丟擲了**Script error**,怎麼解決這個問題呢?特別是現在基本上js資源都會放在cdn上面。

解決方案

1:所有的資源都放在同一個域名下。但是這樣也會存在問題是不能利用cdn的優勢。

2:增加跨域資源支援,在cdn 上增加支援主域的跨域請求支援,在script 標籤加**crossorigin**屬性

在使用Promise過程中,如果你沒有catch,那麼可以這樣來捕獲錯誤

window.addEventListener("unhandledrejection", function(err, promise) { 
    // handle error here, for example log   
});
複製程式碼

如何在NodeJs中捕獲錯誤

NodeJs中的錯誤捕獲很重要,因為處理不當可能導致服務雪崩而不可用。當然了不僅僅知道如何捕獲錯誤,更應該知道如何避免某些錯誤。

  • 當你寫一個函式的時候,你也許曾經思考過當函式執行的時候出現錯誤的時候,我是應該直接丟擲throw,還是使用callback或者event emitter還是其它方式分發錯誤呢?

  • 我是否應該檢查引數是否是正確的型別,是不是null

  • 如果引數不符合的時候,你怎麼辦呢?丟擲錯誤還是通過callback等方式分發錯誤呢?

  • 如果儲存足夠的錯誤來複原錯誤現場呢?

  • 如果去捕獲一些異常錯誤呢?try...catch還是domain

操作錯誤VS編碼錯誤

1. 操作錯誤

操作錯誤往往發生在執行時,並非由於程式碼bug導致,可能是由於你的系統記憶體用完了或者是由於檔案控制程式碼用完了,也可能是沒有網路了等等

2.編碼錯誤

編碼錯誤那就比較容易理解了,可能是undefined卻當作函式呼叫,或者返回了不正確的資料型別,或者記憶體洩露等等

處理操作錯誤

  • 你可以記錄一下錯誤,然後什麼都不做

  • 你也可以重試,比如因為連結資料庫失敗了,但是重試需要限制次數

  • 你也可以將錯誤告訴前端,稍後再試

  • 也許你也可以直接處理,比如某個路徑不存在,則建立該路徑

處理編碼錯誤

錯誤編碼是不好處理的,因為是由於編碼錯誤導致的。好的辦法其實重啟該程式,因為

  • 你不確定某個編碼錯誤導致的錯誤會不會影響其它請求,比如建立資料庫連結錯誤由於編碼錯誤導致不能成功,那麼其它錯誤將導致其它的請求也不可用

  • 或許在錯誤丟擲之前進行IO操作,導致IO控制程式碼無法關閉,這將長期佔有記憶體,可能導致最後記憶體耗盡整個服務不可用。

  • 上面提到的兩點其實都沒有解決問題根本,應該在上線前做好測試,並在上線後做好監控,一旦發生類似的錯誤,就應該監控報警,關注並解決問題

如何分發錯誤

  • 在同步函式中,直接throw出錯誤

  • 對於一些非同步函式,可以將錯誤通過callback丟擲

  • async/await可以直接使用try..catch捕獲錯誤

  • EventEmitter丟擲error事件

NodeJs的運維

一個NodeJs運用,僅僅從碼層面是很難保證穩定執行的,還要從運維層面去保障。

多程式來管理你的應用

單程式的nodejs一旦掛了,整個服務也就不可用了,所以我萌需要多個程式來保障服務的可用,某個程式只負責處理其它程式的啟動,關閉,重啟。保障某個程式掛掉後能夠立即重啟。

可以參考TSW中多程式的設計。master負責對worker的管理,worker和master保持這心跳監測,一旦失去,就立即重啟之。

domain
process.on('uncaughtException', function(err) {
    console.error('Error caught in uncaughtException event:', err);
});
process.on('unhandleRejection', function(err) {
  // TODO
})
複製程式碼

上面捕獲nodejs中異常的時候,可以說是很暴力。但是此時捕獲到異常的時候,你已經失去了此時的上下文,這裡的上下文可以說是某個請求。假如某個web服務發生了一些異常的時候,還是希望能夠返回一些兜底的內容,提升使用者使用體驗。比如服務端渲染或者同構,即使失敗了,也可以返回個靜態的html,走降級方案,但是此時的上下文已經丟失了。沒有辦法了。

function domainMiddleware(options) {
    return async function (ctx, next) {
        const request = ctx.request;
        const d = process.domain || domain.create();
        d.request = request;
        let errHandler = (err) => {
            ctx.set('Content-Type', 'text/html; charset=UTF-8');
            ctx.body = options.staticHtml;
        };
        d.on('error', errHandler);
        d.add(ctx.request);
        d.add(ctx.response);
        try {
            await next();
        } catch(e) {
            errHandler(e)
        }
    }
複製程式碼

上面是一個簡單的koa2的domain的中介軟體,利用domain監聽error事件,每個請求的Request, Response物件在發生錯誤的時候,均會觸發error 事件,當發生錯誤的時候,能夠在有上下文的基礎上,可以走降級方案。

如何避免記憶體洩露

記憶體洩漏很常見,特別是前端去寫後端程式,閉包運用不當,迴圈引用等都會導致記憶體洩漏。

  • 不要阻塞Event Loop的執行,特別是大迴圈或者IO同步操作

    for ( var i = 0; i < 10000000; i++ ) {
        var user       = {};
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem[@outmem](/user/outmem).com';
    }
    複製程式碼

    上面的很長的迴圈會導致記憶體洩漏,因為它是一個同步執行的程式碼,將在程式中執行,V8在迴圈結束的時候,是沒辦法回收迴圈產生的記憶體的,這會導致記憶體一直增長。還有可能原因是,這個很長的執行,阻塞了node進入下一個Event loop, 導致佇列中堆積了太多等待處理已經準備好的回撥,進一步加劇記憶體的佔用。那怎麼解決呢?

    可以利用setTimeout將操作放在下一個loop中執行,減少長迴圈,同步IO對程式的阻.阻塞下一個loop 的執行,也會導致應用的效能下降

  • 模組的私有變數和方法都會常駐在記憶體中

var leakArray = [];   
exports.leak = function () {  
  leakArray.push("leak" + Math.random());  
};
複製程式碼

在node中require一個模組的時候,最後都是形成一個單例,也就是隻要呼叫該函式一下,函式記憶體就會增長,閉包不會被回收,第二是leak方法是一個私有方法,這個方法也會一直存在記憶體。加入每個請求都會呼叫一下這個方法,那麼記憶體一會就炸了。

這樣的場景其實很常見

// main.js
function Main() {
  this.greeting = 'hello world';
}
module.exports = Main;
複製程式碼
var a = require('./main.js')();
var b = require('./main.js')();
a.greeting = 'hello a';
console.log(a.greeting); // hello a
console.log(b.greeting); // hello a
複製程式碼

require得到是一個單例,在一個服務端中每一個請求執行的時候,操作的都是一個單例,這樣每一次執行產生的變數或者屬性都會一直掛在這個物件上,無法回收,佔用大量記憶體。

其實上面可以按照下面的呼叫方式來呼叫,每次都產生一個例項,用完回收。

var a = new require('./main.js');
// TODO
複製程式碼

有的時候很難避免一些可能產生記憶體洩漏的問題,可以利用vm每次呼叫都在一個沙箱環境下呼叫,用完回收調。

  • 最後就是避免迴圈引用了,這樣也會導致無法回收

相關文章