溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

深入解讀Node.js中的koa源碼

發(fā)布時(shí)間:2020-09-14 12:33:44 來(lái)源:腳本之家 閱讀:148 作者:賈順名 欄目:web開(kāi)發(fā)

前言

Node.js也是寫(xiě)了兩三年的時(shí)間了,剛開(kāi)始學(xué)習(xí)Node的時(shí)候,hello world就是創(chuàng)建一個(gè)HttpServer,后來(lái)在工作中也是經(jīng)歷過(guò)Express、Koa1.x、Koa2.x以及最近還在研究的結(jié)合著TypeScript的routing-controllers(驅(qū)動(dòng)依然是Express與Koa)。

用的比較多的還是Koa版本,也是對(duì)它的洋蔥模型比較感興趣,所以最近抽出時(shí)間來(lái)閱讀其源碼,正好近期可能會(huì)對(duì)一個(gè)Express項(xiàng)目進(jìn)行重構(gòu),將其重構(gòu)為koa2.x版本的,所以,閱讀其源碼對(duì)于重構(gòu)也是一種有效的幫助。

Koa是怎么來(lái)的

首先需要確定,Koa是什么。

任何一個(gè)框架的出現(xiàn)都是為了解決問(wèn)題,而Koa則是為了更方便的構(gòu)建http服務(wù)而出現(xiàn)的。

可以簡(jiǎn)單的理解為一個(gè)HTTP服務(wù)的中間件框架。

使用http模塊創(chuàng)建http服務(wù)

相信大家在學(xué)習(xí)Node時(shí),應(yīng)該都寫(xiě)過(guò)類似這樣的代碼:

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'))

一個(gè)最簡(jiǎn)單的示例,腳本運(yùn)行后訪問(wèn)http://127.0.0.1:8888即可看到一個(gè)Hello World的字符串。
但是這僅僅是一個(gè)簡(jiǎn)單的示例,因?yàn)槲覀儾还茉L問(wèn)什么地址(甚至修改請(qǐng)求的Method),都總是會(huì)獲取到這個(gè)字符串:

> curl http://127.0.0.1:8888
> curl http://127.0.0.1:8888/sub
> curl -X POST http://127.0.0.1:8888

所以我們可能會(huì)在回調(diào)中添加邏輯,根據(jù)路徑、Method來(lái)返回給用戶對(duì)應(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ě)法還會(huì)帶來(lái)另一個(gè)問(wèn)題,如果是一個(gè)很大的項(xiàng)目,存在N多的接口。

如果都寫(xiě)在這一個(gè)handler里邊去,未免太過(guò)難以維護(hù)。

示例只是簡(jiǎn)單的針對(duì)一個(gè)變量進(jìn)行賦值,但是真實(shí)的項(xiàng)目不會(huì)有這么簡(jiǎn)單的邏輯存在的。

所以,我們針對(duì)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')
}
}
}

然后通過(guò)實(shí)例化一個(gè)Router對(duì)象進(jìn)行注冊(cè)對(duì)應(yīng)的路徑,最后啟動(dò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)了一個(gè)代碼比較整潔的HttpServer,但功能上依舊是很簡(jiǎn)陋的。

如果我們現(xiàn)在有一個(gè)需求,要在部分請(qǐng)求的前邊添加一些參數(shù)的生成,比如一個(gè)請(qǐng)求的唯一ID。

將代碼重復(fù)編寫(xiě)在我們的handler中肯定是不可取的。

所以我們要針對(duì)route的處理進(jìn)行優(yōu)化,使其支持傳入多個(gè)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')
}
}
}

然后針對(duì)上邊的路徑監(jiān)聽(tīng)添加其他的handler:

function generatorId(request, response, next) {
this.id = 123
next()
}
app.get('/', generatorId, function(request, response) {
response.end(`Hello World ${this.id}`)
})

這樣在訪問(wèn)接口時(shí),就可以看到Hello World 123的字樣了。

這個(gè)就可以簡(jiǎn)單的認(rèn)為是在Express中實(shí)現(xiàn)的 中間件。

中間件是Express、Koa的核心所在,一切依賴都通過(guò)中間件來(lái)進(jìn)行加載。

更靈活的中間件方案-洋蔥模型

上述方案的確可以讓人很方便的使用一些中間件,在流程控制中調(diào)用next()來(lái)進(jìn)入下一個(gè)環(huán)節(jié),整個(gè)流程變得很清晰。

但是依然存在一些局限性。

例如如果我們需要進(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) {
// 請(qǐng)求耗時(shí)的統(tǒng)計(jì)
console.log(
`${request.url} duration: ${new Date().valueOf() - this.requestTime}`
)
response.end('XXX')
})
// 方案2. 將輸出數(shù)據(jù)的邏輯挪到一個(gè)后置的中間件中
function afterRequest(request, response, next) {
// 請(qǐng)求耗時(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
)

無(wú)論是哪一種方案,對(duì)于原有代碼都是一種破壞性的修改,這是不可取的。

因?yàn)镋xpress采用了response.end()的方式來(lái)向接口請(qǐng)求方返回?cái)?shù)據(jù),調(diào)用后即會(huì)終止后續(xù)代碼的執(zhí)行。

而且因?yàn)楫?dāng)時(shí)沒(méi)有一個(gè)很好的方案去等待某個(gè)中間件中的異步函數(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()的返回值是沒(méi)有意義的。

所以就有了Koa帶來(lái)的洋蔥模型,在Koa1.x出現(xiàn)的時(shí)間,正好趕上了Node支持了新的語(yǔ)法,Generator函數(shù)及Promise的定義。
所以才有了co這樣令人驚嘆的庫(kù),而當(dāng)我們的中間件使用了Promise以后,前一個(gè)中間件就可以很輕易的在后續(xù)代碼執(zhí)行完畢后再處理自己的事情。

但是,Generator本身的作用并不是用來(lái)幫助我們更輕松的使用Promise來(lái)做異步流程的控制。

所以,隨著Node7.6版本的發(fā)出,支持了async、await語(yǔ)法,社區(qū)也推出了Koa2.x,使用async語(yǔ)法替換之前的co+Generator。

Koa也將co從依賴中移除(2.x版本使用koa-convert將Generator函數(shù)轉(zhuǎn)換為promise,在3.x版本中將直接不支持Generator)

由于在功能、使用上Koa的兩個(gè)版本之間并沒(méi)有什么區(qū)別,最多就是一些語(yǔ)法的調(diào)整,所以會(huì)直接跳過(guò)一些Koa1.x相關(guān)的東西,直奔主題。

在Koa中,可以使用如下的方式來(lái)定義中間件并使用:

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(yǔ)法糖的存在,遮蓋了代碼實(shí)際運(yùn)行的過(guò)程,所以,我們使用Promise來(lái)還原一下上述代碼:

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)
})
}

大致代碼是這樣的,也就是說(shuō),調(diào)用next會(huì)給我們返回一個(gè)Promise對(duì)象,而Promise何時(shí)會(huì)resolve就是Koa內(nèi)部做的處理。
可以簡(jiǎn)單的實(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é)束請(qǐng)求
response.end(context.body || '404')
})
} else {
response.end('404')
}
}
}

每次調(diào)用中間件時(shí)就監(jiān)聽(tīng)then,并將當(dāng)前Promise的resolve與reject處理傳入Promise的回調(diào)中。

也就是說(shuō),只有當(dāng)?shù)诙€(gè)中間件的resolve被調(diào)用時(shí),第一個(gè)中間件的then回調(diào)才會(huì)執(zhí)行。

這樣就實(shí)現(xiàn)了一個(gè)洋蔥模型。

就像我們的log中間件執(zhí)行的流程:

  1. 獲取當(dāng)前的時(shí)間戳requestTime
  2. 調(diào)用next()執(zhí)行后續(xù)的中間件,并監(jiān)聽(tīng)其回調(diào)
  3. 第二個(gè)中間件里邊可能會(huì)調(diào)用第三個(gè)、第四個(gè)、第五個(gè),但這都不是log所關(guān)心的,log只關(guān)心第二個(gè)中間件何時(shí)resolve,而第二個(gè)中間件的resolve則依賴他后邊的中間件的resolve。
  4. 等到第二個(gè)中間件resolve,這就意味著后續(xù)沒(méi)有其他的中間件在執(zhí)行了(全都resolve了),此時(shí)log才會(huì)繼續(xù)后續(xù)代碼的執(zhí)行

所以就像洋蔥一樣一層一層的包裹,最外層是最大的,是最先執(zhí)行的,也是最后執(zhí)行的。(在一個(gè)完整的請(qǐng)求中,next之前最先執(zhí)行,next之后最后執(zhí)行)

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI