node.js express和koa中間件機制和錯誤處理機制
一、前言
大傢可能都知道koa是express核心原班人馬寫的,那麼他們為什麼要在express後再造一個koa的輪子呢? 今天就給大傢帶來一些分析。希望能夠起到一個拋磚引玉的作用。
其實,這個題目也可以這麼問, express有什麼缺點? koa解決瞭一些express的什麼問題? 這也在一些面試題中會這麼問。所以,為瞭實現自己的理想(money), 志同道合的同志們可以隨我分析一下瞭。
我想先從express的一個非常重要的特征開始說起,那就是 中間件
。 中間件貫穿瞭express的始終,我們在express中比較常用到應用級的中間件,比如:
const app = require('express')(); app.use((req, res, next) => { // 做一些事情。。。 next(); })
再比如我們更常用到的路由級中間件。 我為什麼要叫它是路由級的呢? 因為它的內部也同樣維護著一個next
app.get('/', (req, res, next) => { res.send('something content'); })
這裡中間件我不詳細展開。 後面有我對中間件的詳細解析,歡迎大傢圍觀。
那麼我們可以看到,其中會有個關鍵的next
, 它在express內部做的是從棧中獲取下一個中間件的關鍵。
那麼重點來瞭, 我們開始研究express這裡的實現會隱藏什麼問題。
二、中間件問題解析
通過一個例子來看:
const Express = require('express'); const app = new Express(); const sleep = () => new Promise(resolve => setTimeout(function(){resolve(1)}, 2000)); const port = 8210; function f1(req, res, next) { console.log('this is function f1....'); next(); console.log('f1 fn executed done'); } function f2(req, res, next) { console.log('this is function f2....'); next(); console.log('f2 fn executed done'); } async function f3(req, res) { console.log('f3 send to client'); res.send('Send To Client Done'); } app.use(f1); app.use(f2); app.use(f3); app.get('/', f3) app.listen(port, () => console.log(`Example app listening on port ${port}!`))
理想下的返回,和真正的返回,目前是沒有問題的。
this is function f1.... this is function f2.... f3 send to client f1 fn executed done f2 fn executed done
好的,那麼再繼續下一個例子。 在下一個例子中,其它都是沒有變化的,隻有一個地方:
const sleep = () => new Promise(resolve => setTimeout(function(){resolve()}, 1000)) async function f3(req, res) { await sleep(); console.log('f3 send to client'); res.send('Send To Client Done'); }
這時你認為的返回值順序是什麼樣的呢?
可能會認為跟上面的沒有變化,因為我們增加await瞭,照道理應該等待await執行完瞭,再去執行下面的代碼。 其實結果並不是。
返回的結果是:
this is function f1….
this is function f2….
f1 fn executed done
f2 fn executed done
f3 send to client
發生瞭什麼??
大傢可能有點吃驚。但是,如果深入到express的源碼中去一探究竟,問題原因也就顯而易見瞭。
具體源碼我在這一篇中就不詳細分析瞭,直接說出結論:
因為express中的中間件調用不是Promise
所以就算我們加瞭async await 也不管用。
那麼koa
中是怎麼使用的呢?
const Koa = require('koa'); const app = new Koa(); const sleep = () => new Promise(resolve => setTimeout(function(){resolve()}, 1000)) app.use(async (ctx, next) => { console.log('middleware 1 start'); await next(); console.log('middleware 1 end'); }); app.use(async (ctx, next) => { await sleep(); console.log('middleware 2 start'); await next(); console.log('middleware 2 end'); }); app.use(async (ctx, next) => { console.log('middleware 3 start') ctx.body = 'test middleware executed'; })
不出所料, 實現的順序是:
middleware 1 start
middleware 2 start
middleware 3 start
middleware 2 end
middleware 1 end
原因是: koa 內部使用瞭Promise
,所以能夠控制順序的執行。
綜合上面的例子,我們知道瞭express中中間件使用的時候,如果不清楚原理,是容易踩坑的。 而koa通過使用async 和 await next()
實現洋蔥模型,即:通過next,到下一個中間件,隻要下面的中間件執行完成後,才一層層的再執行上面的中間件,直到全部完成。
三、錯誤邏輯捕獲
3.1 express的錯誤捕獲邏輯
同樣,先看express在錯誤邏輯的捕獲
上有什麼特點:
app.use((req, res, next) => { // c 沒有定義 const a = c; }); // 錯誤處理中間件 app.use((err, req, res, next) => { if(error) { console.log(err.message); } next() }) process.on("uncaughtException", (err) => { console.log("uncaughtException message is::", err); })
再看一個異步的處理:
app.use((req, res, next) => { // c 沒有定義 try { setTimeout(() => { const a = c; next() }, 0) } catch(e) { console.log('異步錯誤,能catch到麼??') } }); app.use((err, req, res, next) => { if(error) { console.log('這裡會執行麼??', err.message); } next() }) process.on("uncaughtException", (err) => { console.log("uncaughtException message is::", err); })
可以先猜一下同步和異步的會不會有所區別?
答案是: 有很大的區別!!
具體分開來看:
- 同步的時候, 不會觸發
uncaughtException
, 而進入瞭錯誤處理的中間件。 - 異步的時候,
不會
觸發錯誤處理中間件,而會
觸發uncaughtException
這中間發生瞭什麼?
3.2 同步邏輯錯誤獲取的底層邏輯
邏輯是: express內部對同步發生的錯誤進行瞭攔截,所以,不會傳到負責兜底的node事件 uncaughtException
,如果發生瞭錯誤,則直接繞過其它中間件,進入錯誤處理中間件。 那麼,這裡會有一個很容易被忽略的點, 那就是,即使沒有錯誤處理中間件做兜底,也不會進入node的 uncaughtException
, 這時, 會直接報 500
錯誤。
3.3 異步邏輯錯誤獲取的底層邏輯
還是因為express的實現並沒有把Promise考慮進去, 它的中間件執行是同步順序
執行的。 所以如果有異步的,那麼錯誤處理中間件實際是兜不住的,所以,express對這種中間件中的異步處理錯誤無能為力。
從上面的異步觸發例子來看, 除瞭錯誤處理中間件沒有觸發,我們當中的try catch也沒有觸發
。這是一個大傢可能都會踩到的坑。 這裡其實是與javascript的運行機制相關瞭。具體原因見本篇 JavaScript異步隊列進行try catch時的問題解決
所以要想去catch 當前的錯誤,那麼就需要用 async await
app.use(async (req, res, next) => { try { await (() => new Promise((resolve, reject) => { http.get('http://www.example.com/testapi/123', res => { reject('假設錯誤瞭'); }).on('error', (e) => { throw new Error(e); }) }))(); } catch(e) { console.log('異步錯誤,能catch到麼??') } });
這樣,我們的catch不僅可以獲取到, uncaughtException也可以獲取到。
3.4 koa的錯誤獲取邏輯
總體上是跟express差不多,因為js的底層處理還是一致的。但還是使用上有所差異。
上面也提過洋蔥模型,特點是最開始的中間件,在最後才執行完畢,所以,在koa上,可以把錯誤處理中間件放到中間件邏輯最前面。
const http = require('http'); const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next)=>{ try { await next(); } catch (error) { // 響應用戶 ctx.status = 500; ctx.body = '進入默認錯誤中間件'; // ctx.app.emit('error', error); // 觸發應用層級錯誤事件 } }); app.use(async (ctx, next) => { await (() => new Promise((resolve, reject) => { http.get('http://www.example.com/testapi/123', res => { reject('假設錯誤瞭'); }).on('error', (e) => { throw new Error(e); }) }))(); await next(); })
上面的代碼, reject出的錯誤信息,會被最上面的錯誤處理中間件捕獲。總結來說,js的底層機制是一樣的, 隻是使用方法和細節點上不一樣,大傢在用的時候註意一下,
到此這篇關於node.js express和koa中間件機制和錯誤處理機制的文章就介紹到這瞭,更多相關node.js express和koa內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- vue中Promise的使用方法詳情
- JS異步代碼單元測試之神奇的Promise
- JavaScript詳解使用Promise處理回調地獄與async await修飾符
- Node端異常捕獲的實現方法
- JavaScript異步隊列進行try catch時的問題解決