深入解讀Node.js中的koa源碼
前言
Node.js也是寫了兩三年的時(shí)間了,剛開始學(xué)習(xí)Node的時(shí)候,hello world就是創(chuàng)建一個HttpServer,后來在工作中也是經(jīng)歷過Express、Koa1.x、Koa2.x以及最近還在研究的結(jié)合著TypeScript的routing-controllers(驅(qū)動依然是Express與Koa)。
用的比較多的還是Koa版本,也是對它的洋蔥模型比較感興趣,所以最近抽出時(shí)間來閱讀其源碼,正好近期可能會對一個Express項(xiàng)目進(jìn)行重構(gòu),將其重構(gòu)為koa2.x版本的,所以,閱讀其源碼對于重構(gòu)也是一種有效的幫助。
Koa是怎么來的
首先需要確定,Koa是什么。
任何一個框架的出現(xiàn)都是為了解決問題,而Koa則是為了更方便的構(gòu)建http服務(wù)而出現(xiàn)的。
可以簡單的理解為一個HTTP服務(wù)的中間件框架。
使用http模塊創(chuàng)建http服務(wù)
相信大家在學(xué)習(xí)Node時(shí),應(yīng)該都寫過類似這樣的代碼:
const http = require('http') const serverHandler = (request, response) => { response.end('Hello World') // 返回?cái)?shù)據(jù) } http .createServer(serverHandler) .listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))
一個最簡單的示例,腳本運(yùn)行后訪問http://127.0.0.1:8888即可看到一個Hello World的字符串。
但是這僅僅是一個簡單的示例,因?yàn)槲覀儾还茉L問什么地址(甚至修改請求的Method),都總是會獲取到這個字符串:
> curl http://127.0.0.1:8888 > curl http://127.0.0.1:8888/sub > curl -X POST http://127.0.0.1:8888
所以我們可能會在回調(diào)中添加邏輯,根據(jù)路徑、Method來返回給用戶對應(yīng)的數(shù)據(jù):
const serverHandler = (request, response) => { // default let responseData = '404' if (request.url === '/') { if (request.method === 'GET') { responseData = 'Hello World' } else if (request.method === 'POST') { responseData = 'Hello World With POST' } } else if (request.url === '/sub') { responseData = 'sub page' } response.end(responseData) // 返回?cái)?shù)據(jù) }
類似Express的實(shí)現(xiàn)
但是這樣的寫法還會帶來另一個問題,如果是一個很大的項(xiàng)目,存在N多的接口。
如果都寫在這一個handler里邊去,未免太過難以維護(hù)。
示例只是簡單的針對一個變量進(jìn)行賦值,但是真實(shí)的項(xiàng)目不會有這么簡單的邏輯存在的。
所以,我們針對handler進(jìn)行一次抽象,讓我們能夠方便的管理路徑:
class App { constructor() { this.handlers = {} this.get = this.route.bind(this, 'GET') this.post = this.route.bind(this, 'POST') } route(method, path, handler) { let pathInfo = (this.handlers[path] = this.handlers[path] || {}) // register handler pathInfo[method] = handler } callback() { return (request, response) => { let { url: path, method } = request this.handlers[path] && this.handlers[path][method] ? this.handlers[path][method](request, response) : response.end('404') } } }
然后通過實(shí)例化一個Router對象進(jìn)行注冊對應(yīng)的路徑,最后啟動服務(wù):
const app = new App() app.get('/', function (request, response) { response.end('Hello World') }) app.post('/', function (request, response) { response.end('Hello World With POST') }) app.get('/sub', function (request, response) { response.end('sub page') }) http .createServer(app.callback()) .listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))
Express中的中間件
這樣,就實(shí)現(xiàn)了一個代碼比較整潔的HttpServer,但功能上依舊是很簡陋的。
如果我們現(xiàn)在有一個需求,要在部分請求的前邊添加一些參數(shù)的生成,比如一個請求的唯一ID。
將代碼重復(fù)編寫在我們的handler中肯定是不可取的。
所以我們要針對route的處理進(jìn)行優(yōu)化,使其支持傳入多個handler:
route(method, path, ...handler) { let pathInfo = (this.handlers[path] = this.handlers[path] || {}) // register handler pathInfo[method] = handler } callback() { return (request, response) => { let { url: path, method } = request let handlers = this.handlers[path] && this.handlers[path][method] if (handlers) { let context = {} function next(handlers, index = 0) { handlers[index] && handlers[index].call(context, request, response, () => next(handlers, index + 1) ) } next(handlers) } else { response.end('404') } } }
然后針對上邊的路徑監(jiān)聽添加其他的handler:
function generatorId(request, response, next) { this.id = 123 next() } app.get('/', generatorId, function(request, response) { response.end(`Hello World ${this.id}`) })
這樣在訪問接口時(shí),就可以看到Hello World 123的字樣了。
這個就可以簡單的認(rèn)為是在Express中實(shí)現(xiàn)的 中間件。
中間件是Express、Koa的核心所在,一切依賴都通過中間件來進(jìn)行加載。
更靈活的中間件方案-洋蔥模型
上述方案的確可以讓人很方便的使用一些中間件,在流程控制中調(diào)用next()來進(jìn)入下一個環(huán)節(jié),整個流程變得很清晰。
但是依然存在一些局限性。
例如如果我們需要進(jìn)行一些接口的耗時(shí)統(tǒng)計(jì),在Express有這么幾種可以實(shí)現(xiàn)的方案:
function beforeRequest(request, response, next) { this.requestTime = new Date().valueOf() next() } // 方案1. 修改原h(huán)andler處理邏輯,進(jìn)行耗時(shí)的統(tǒng)計(jì),然后end發(fā)送數(shù)據(jù) app.get('/a', beforeRequest, function(request, response) { // 請求耗時(shí)的統(tǒng)計(jì) console.log( `${request.url} duration: ${new Date().valueOf() - this.requestTime}` ) response.end('XXX') }) // 方案2. 將輸出數(shù)據(jù)的邏輯挪到一個后置的中間件中 function afterRequest(request, response, next) { // 請求耗時(shí)的統(tǒng)計(jì) console.log( `${request.url} duration: ${new Date().valueOf() - this.requestTime}` ) response.end(this.body) } app.get( '/b', beforeRequest, function(request, response, next) { this.body = 'XXX' next() // 記得調(diào)用,不然中間件在這里就終止了 }, afterRequest )
無論是哪一種方案,對于原有代碼都是一種破壞性的修改,這是不可取的。
因?yàn)镋xpress采用了response.end()的方式來向接口請求方返回?cái)?shù)據(jù),調(diào)用后即會終止后續(xù)代碼的執(zhí)行。
而且因?yàn)楫?dāng)時(shí)沒有一個很好的方案去等待某個中間件中的異步函數(shù)的執(zhí)行。
function a(_, _, next) { console.log('before a') let results = next() console.log('after a') } function b(_, _, next) { console.log('before b') setTimeout(_ => { this.body = 123456 next() }, 1000) } function c(_, response) { console.log('before c') response.end(this.body) } app.get('/', a, b, c)
就像上述的示例,實(shí)際上log的輸出順序?yàn)椋?/p>
before a before b after a before c
這顯然不符合我們的預(yù)期,所以在Express中獲取next()的返回值是沒有意義的。
所以就有了Koa帶來的洋蔥模型,在Koa1.x出現(xiàn)的時(shí)間,正好趕上了Node支持了新的語法,Generator函數(shù)及Promise的定義。
所以才有了co這樣令人驚嘆的庫,而當(dāng)我們的中間件使用了Promise以后,前一個中間件就可以很輕易的在后續(xù)代碼執(zhí)行完畢后再處理自己的事情。
但是,Generator本身的作用并不是用來幫助我們更輕松的使用Promise來做異步流程的控制。
所以,隨著Node7.6版本的發(fā)出,支持了async、await語法,社區(qū)也推出了Koa2.x,使用async語法替換之前的co+Generator。
Koa也將co從依賴中移除(2.x版本使用koa-convert將Generator函數(shù)轉(zhuǎn)換為promise,在3.x版本中將直接不支持Generator)
由于在功能、使用上Koa的兩個版本之間并沒有什么區(qū)別,最多就是一些語法的調(diào)整,所以會直接跳過一些Koa1.x相關(guān)的東西,直奔主題。
在Koa中,可以使用如下的方式來定義中間件并使用:
async function log(ctx, next) { let requestTime = new Date().valueOf() await next() console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`) } router.get('/', log, ctx => { // do something... })
因?yàn)橐恍┱Z法糖的存在,遮蓋了代碼實(shí)際運(yùn)行的過程,所以,我們使用Promise來還原一下上述代碼:
function log() { return new Promise((resolve, reject) => { let requestTime = new Date().valueOf() next().then(_ => { console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`) }).then(resolve) }) }
大致代碼是這樣的,也就是說,調(diào)用next會給我們返回一個Promise對象,而Promise何時(shí)會resolve就是Koa內(nèi)部做的處理。
可以簡單的實(shí)現(xiàn)一下(關(guān)于上邊實(shí)現(xiàn)的App類,僅僅需要修改callback即可):
callback() { return (request, response) => { let { url: path, method } = request let handlers = this.handlers[path] && this.handlers[path][method] if (handlers) { let context = { url: request.url } function next(handlers, index = 0) { return new Promise((resolve, reject) => { if (!handlers[index]) return resolve() handlers[index](context, () => next(handlers, index + 1)).then( resolve, reject ) }) } next(handlers).then(_ => { // 結(jié)束請求 response.end(context.body || '404') }) } else { response.end('404') } } }
每次調(diào)用中間件時(shí)就監(jiān)聽then,并將當(dāng)前Promise的resolve與reject處理傳入Promise的回調(diào)中。
也就是說,只有當(dāng)?shù)诙€中間件的resolve被調(diào)用時(shí),第一個中間件的then回調(diào)才會執(zhí)行。
這樣就實(shí)現(xiàn)了一個洋蔥模型。
就像我們的log中間件執(zhí)行的流程:
- 獲取當(dāng)前的時(shí)間戳requestTime
- 調(diào)用next()執(zhí)行后續(xù)的中間件,并監(jiān)聽其回調(diào)
- 第二個中間件里邊可能會調(diào)用第三個、第四個、第五個,但這都不是log所關(guān)心的,log只關(guān)心第二個中間件何時(shí)resolve,而第二個中間件的resolve則依賴他后邊的中間件的resolve。
- 等到第二個中間件resolve,這就意味著后續(xù)沒有其他的中間件在執(zhí)行了(全都resolve了),此時(shí)log才會繼續(xù)后續(xù)代碼的執(zhí)行
所以就像洋蔥一樣一層一層的包裹,最外層是最大的,是最先執(zhí)行的,也是最后執(zhí)行的。(在一個完整的請求中,next之前最先執(zhí)行,next之后最后執(zhí)行)
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于node+mysql數(shù)據(jù)庫連接池連接
這篇文章主要介紹了關(guān)于node+mysql數(shù)據(jù)庫連接池連接,mysql有兩種連接方式:一種是直接連接 另一種是池化連接,我們這篇講的是池化連接,需要的朋友可以參考下2023-04-04node創(chuàng)建Vue項(xiàng)目步驟詳解
在本篇文章里小編給大家整理的是關(guān)于node創(chuàng)建Vue項(xiàng)目步驟詳解內(nèi)容,需要的朋友們可以學(xué)習(xí)下。2020-03-03Node.js Continuation Passing Style( CPS與
這篇文章主要介紹了Node.js Continuation Passing Style,將回調(diào)函數(shù)作為參數(shù)傳遞,這種書寫方式通常被稱為Continuation Passing Style(CPS),它的本質(zhì)仍然是一個高階函數(shù),CPS最初是各大語言中對排序算法的實(shí)現(xiàn)2022-06-06Express框架搭建項(xiàng)目的實(shí)現(xiàn)步驟
Express是一個基于Node.js平臺的輕量級Web應(yīng)用框架,它提供了簡潔的API和豐富的功能,本文主要介紹了Express框架搭建項(xiàng)目的實(shí)現(xiàn)步驟,感興趣的可以了解一下2024-06-06node.js 開發(fā)指南 – Node.js 連接 MySQL 并進(jìn)行數(shù)據(jù)庫操作
通常在NodeJS開發(fā)中我們經(jīng)常涉及到操作數(shù)據(jù)庫,尤其是 MySQL ,作為應(yīng)用最為廣泛的開源數(shù)據(jù)庫則成為我們的首選,本篇就來介紹下如何通過NodeJS來操作 MySQL 數(shù)據(jù)庫。2014-07-07Nodejs-cluster模塊知識點(diǎn)總結(jié)及實(shí)例用法
在本篇文章里小編給大家整理的是一篇關(guān)于Nodejs-cluster模塊知識點(diǎn)總結(jié)及實(shí)例用法,有興趣的朋友們可以跟著學(xué)習(xí)下。2021-12-12