js 弹出层的点击事件影响到底层的点击事件_聊一聊 Node.js 错误处理
錯誤分類
軟件程序中,我們可以將錯誤大致分為外部錯誤和內(nèi)部錯誤兩大類。
外部錯誤是正確編寫的程序在運行時產(chǎn)生的錯誤。它并不是程序本身的 bug,更多是一些外部原因?qū)е碌膯栴},比如請求超時、服務器返回500、內(nèi)存不足等。
而內(nèi)部錯誤是程序里的 bug。比如傳參類型錯誤、讀取 undefined 的一個屬性等。這類問題跟你選擇的開發(fā)語言、開發(fā)者的編程經(jīng)驗、系統(tǒng)復雜度等因素息息相關,雖然無法避免,但可以通過修改代碼來修復它。
對應到 Node.js 程序上,一般遇到以下四類錯誤: 1. 標準的 JavaScript 錯誤。例如 SyntaxError、RangeError、ReferenceError、TypeError等。 2. 由底層操作系觸發(fā)的系統(tǒng)錯誤,例如試圖打開不存在的文件。 3. 用戶自定義錯誤。 4. 斷言錯誤。這類錯誤通常來自 assert 模塊。
注:本文中不區(qū)分錯誤和異常,都將其統(tǒng)稱為錯誤。
錯誤處理
當錯誤發(fā)生后,我們需要第一時間去處理它。針對不同類型的錯誤,有不同的措施。處理錯誤的總體原則:
外部錯誤
程序運行過程中,可能會遇到各種外部因素導致的問題,這些問題需要具體問題具體分析。我們沒辦法保證外部服務提供方的穩(wěn)定性,但是遇到此類問題時,可以做一些事情,來保證我們的程序不至于直接崩潰。
舉個例子,秒殺場景的業(yè)務經(jīng)常會承受非常大的 QPS,在一波瞬間大流量的沖擊,后端服務扛不住的話會報 5XX 錯誤。在后端服務掛掉后,我們可能會去讀 redis 等緩存中的數(shù)據(jù),用舊數(shù)據(jù)來兜底。而當 Node.js 應用也掛掉了,還可以在 Nginx 層進行 CDN 降級,給用戶輸出一個兜底的靜態(tài)頁。
還有些來自后端服務的錯誤,只需要進行簡單的重試就能解決。如果要重試的話,要確定重試的次數(shù),以及重試的間隔。
有人建議在發(fā)生錯誤后直接崩潰掉,防止錯誤擴散。個人認為其實是不合理的,會降低服務的可用性。我們可以在出現(xiàn)一些嚴重的錯誤后,先記錄下錯誤,然后重啟進程。在 Node.js 中,未捕獲的 JavaScript 異常一直冒泡回到事件循環(huán)時,會觸發(fā) process.uncaughtException 事件。我們可以在事件回調(diào)中做錯誤上報,然后重啟 Node.js 進程。這時,還需要借助 Cluster 來啟動多個 Node 進程,保證單進程崩潰重啟不會影響整體服務的可用性。實際的生產(chǎn)環(huán)境中,使用 PM2 來管理 Node.js 進程是一個更好的選項。
我們永遠也無法阻止外部錯誤,它跟你的業(yè)務場景、用戶終端等各種不可控的因素相關。但是我們?nèi)绻龊帽O(jiān)控、告警、日志、緩存等工作,可以方便程序員迅速定位/解決問題,從而將損失降至最低。
內(nèi)部錯誤
同步場景
對于 JavaScript 錯誤,我們可以使用 throw 拋出,并用 try catch 來捕獲住。
try {throw new Error('some error') } catch(e) {console.error(e) }而且對于 throw 拋出的異常必須要 try catch 包裹,否則 Node.js 進程會直接退出。這種寫法可以獲取到完整的錯誤調(diào)用堆棧。比如:
fs.js:115throw err;^Error: ENOENT: no such file or directory, scandir '/Users/frank/code/work/wxapp/src/componentsa'at Object.readdirSync (fs.js:783:3)at getDirFilePaths (/Users/frank/code/m/demo/readdir.js:8:22)at Object.<anonymous> (/Users/frank/code/m/demo/readdir.js:27:15)at Module._compile (internal/modules/cjs/loader.js:688:30)at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10)at Module.load (internal/modules/cjs/loader.js:598:32)at tryModuleLoad (internal/modules/cjs/loader.js:537:12)at Function.Module._load (internal/modules/cjs/loader.js:529:3)at Function.Module.runMain (internal/modules/cjs/loader.js:741:12)at startup (internal/bootstrap/node.js:285:19)眾所周知,JS 函數(shù)調(diào)用會形成一系列的棧幀,為了盡可能的恢復錯誤發(fā)生現(xiàn)場,最好在錯誤上報時帶上堆棧信息。Node.js 中,Error.captureStackTrace() 方法是 v8 引擎暴露出來的,處理錯誤堆棧信息的 API。
Error.captureStackTrace(targetObject[, constructorOpt]) 在 targetObject 中添加一個 .stack 屬性。對該屬性進行訪問時,將以字符串的形式返回 Error.captureStackTrace() 語句被調(diào)用時的代碼位置信息(即:調(diào)用棧歷史)。值得注意的是,它的第二個參數(shù)可以用來控制棧幀的終點。在一些底層庫中,這個參數(shù)可以用來向開發(fā)者隱藏內(nèi)部實現(xiàn)細節(jié)。
實際的生產(chǎn)環(huán)境中,我們可以使用 nested-error-stacks 這類 npm 包來采集堆棧信息,原理其實也是基于 Error.captureStackTrace()。
這里有個問題是:try catch 代碼塊是同步的,對于異步 API 發(fā)生的錯誤,它不能捕獲到。
比如下列代碼:
try {setTimeout(() => {throw new Error('some error')}, 1000) } catch(e) {console.log('some error...') }錯誤并不能被捕獲住。這個跟 Node.js 的事件循環(huán)機制有關,因為異步任務是通過事件隊列來實現(xiàn)的,每次從事件隊列中取出一個函數(shù)來執(zhí)行時,實際上這個函數(shù)是在調(diào)用棧的最頂層執(zhí)行的,如果它拋出了一個異常,也是無法沿著調(diào)用棧回溯到這個異步任務的創(chuàng)建者的。
下面介紹下在異步流程中,我們應該怎么處理錯誤。
異步場景
Node.js 中常見異步場景包括三類: - Node.js style callback - Promise - EventEmitter
大部分異步 API 都遵循錯誤回調(diào)優(yōu)先的約定,將 Error 作為 callback 的第一個參數(shù)來傳遞,這種風格比較類似函數(shù)式編程中的 Continuation-passing style。
fs.readFile(path, 'r', (err, data) => {if (err) {throw err} else {try {// handle data} catch(e) {}} })這種寫法很容易造成回調(diào)地獄。另一方面,對于回調(diào)函數(shù)中的同步邏輯,我們還需要用 try catch 去單獨處理,這導致錯誤邏輯的處理被分散了兩處。Promise 被正式 ES6 標準化后,我們可以用 Promise 的鏈式調(diào)用來處理錯誤。
new Promise((resolve, reject) => {reject(new Error('some error'));}).then(() => {...}).then(() => {...}).catch(err => {});這樣,Promise 鏈上的錯誤都會在 catch 方法上捕獲住。對于沒有 catch 的 Promise 異常,會一直冒泡到頂層,在 process.unhandledRejection 事件上被捕獲住。
還有一類是 EventEmitter 對象上的錯誤。它們會被分發(fā)到 error 事件上進行處理,比如 Stream 等。我們需要去為每一個流去監(jiān)聽 error 事件,否則會冒泡到process.uncaughtException 事件上去。
異步場景中,還有個問題就是,會丟失異步回調(diào)前的錯誤堆棧。原因還是上文提到的 Node.js 事件循環(huán)機制。
const foo = function () {throw new Error('some error') } const bar = function () {setTimeout(foo) } bar()輸出結(jié)果:
Error: some errorat Timeout.foo [as _onTimeout] (/Users/frank/code/m/demo/readdir.js:47:9)at ontimeout (timers.js:436:11)at tryOnTimeout (timers.js:300:5)at listOnTimeout (timers.js:263:5)at Timer.processTimers (timers.js:223:10)可以看到丟失了 bar 的調(diào)用棧。然而在 Node.js 中,異步調(diào)用場景還挺多的,有什么辦法可以將多個異步調(diào)用給串起來,獲取到完整的調(diào)用鏈信息呢?答案是有的。Node.js v8+ 上提供了 async_hooks 模塊,用來完善異步場景的監(jiān)控。
async_hooks
async_hooks 提供了一些 API 用于跟蹤 Node.js 中的異步資源的生命周期。有幾個概念: - 每個異步函數(shù)的作用域,我們稱之為 async scope。 - 每一個 async scope 中都有一個 asyncId, 用來標記當前作用域。相同 async scope 的 asyncId 也相同。每個異步資源在創(chuàng)建時 asyncId 全量遞增的。 - 每一個 async scope 中都有一個 triggerAsyncId 表示當前函數(shù)是由哪個 async scope 觸發(fā)生成的。 - 通過 asyncId 和 triggerAsyncId,我們可以獲取到異步資源的調(diào)用鏈。 - async_hooks.createHooks 函數(shù)可以用來給每個異步資源添加 init/before/after/destory 等生命周期鉤子函數(shù)。
console.log('global.asyncId:', async_hooks.executionAsyncId()); // global.asyncId: 1 console.log('global.triggerAsyncId:', async_hooks.triggerAsyncId()); // global.triggerAsyncId: 0 fs.open('./app.js', 'r', (err, fd) => {console.log('fs.open.asyncId:', async_hooks.executionAsyncId()); // fs.open.asyncId: 7console.log('fs.open.triggerAsyncId:', async_hooks.triggerAsyncId()); // fs.open.triggerAsyncId: 1 });回調(diào)函數(shù)中的 triggerAsyncId 為 1,它等于 global scope 上的 asyncId。這樣就可以拿到多個異步調(diào)用的調(diào)用鏈。
國內(nèi)的趙坤大神寫過一個 koa 日志中間件 koa-await-breakpoint,用于實現(xiàn)在每個 await 執(zhí)行的語句前后進行自動打點工作。
// On top of the main file const koaAwaitBreakpoint = require('koa-await-breakpoint')({name: 'api',files: ['./routes/*.js'] }) const Koa = require('koa') const app = new Koa() // Generally, above other middlewares app.use(koaAwaitBreakpoint) ... app.listen(3000)每個請求到來時,生成一個 requestId 掛載到 ctx 上,通過 requestId 將日志串起來。核心原理是 hack 了模塊的 require 方法(重載 Module.prototype._compile),用 esprima 將模塊代碼轉(zhuǎn)成 AST,找到其中的 awaitExpression 節(jié)點,對其用日志函數(shù)包裹后重新插入到 AST,最后用 escodegen 將 AST 生成代碼。其中還用到了 async_hooks,在日志函數(shù)中,基于 async_hooks 的 init 鉤子中將異步調(diào)用關系存儲到一個 Map 中,最終實現(xiàn)函數(shù)調(diào)用鏈的自動日志打點。
不過,使用 async_hooks 在目前有較嚴重的性能損耗。建議生產(chǎn)環(huán)境慎用。
總結(jié)
錯誤可分為外部錯誤和內(nèi)部錯誤兩類。對外部錯誤的處理主要考驗系統(tǒng)架構(gòu)的設計,只有系統(tǒng)設計的足夠健壯,才能夠抵御各種外部挑戰(zhàn),并損失降到最低。對于內(nèi)部錯誤,本文分別討論了同步和異步兩種場景,介紹了 Error.captureStackTrace()、async_hooks 等 API 在收集錯誤堆棧、異步調(diào)用鏈上的用途,并結(jié)合 koa-await-breakpoint 源碼,解釋了 Node.js 自動化打點的核心原理。
參考鏈接:
總結(jié)
以上是生活随笔為你收集整理的js 弹出层的点击事件影响到底层的点击事件_聊一聊 Node.js 错误处理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL运算符,函数,索引,图形化管理
- 下一篇: gdb x命令_gdb基本命令