淺談前端錯誤處理
使用者反饋開啟的頁面白螢幕,怎麼定位到產生錯誤的原因呢?日常某次釋出怎麼確定釋出會沒有引入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一旦掛了,整個服務也就不可用了,所以我萌需要多個程序來保障服務的可用,某個程序只負責處理其它程序的啟動,關閉,重啟。保障某個程序掛掉後能夠立即重啟。
可以參考ofollow,noindex">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 每次呼叫都在一個沙箱環境下呼叫,用完回收調。
- 最後就是避免迴圈引用了,這樣也會導致無法回收