欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時(shí)間:2019年06月17日 09:36:46   作者:賈順名  
這篇文章主要介紹了深入解讀Node.js中的koa源碼,任何一個框架的出現(xiàn)都是為了解決問題,而Koa則是為了更方便的構(gòu)建http服務(wù)而出現(xiàn)的。 可以簡單的理解為一個HTTP服務(wù)的中間件框架。,需要的朋友可以參考下

前言

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í)行的流程:

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

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

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

您可能感興趣的文章:

相關(guān)文章

  • Node.js如何在項(xiàng)目中操作MySQL

    Node.js如何在項(xiàng)目中操作MySQL

    這篇文章主要介紹了Node.js如何在項(xiàng)目中操作MySQL,從而實(shí)現(xiàn)數(shù)據(jù)的查詢、插入、更新和刪除等操作,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2022-01-01
  • 關(guān)于node+mysql數(shù)據(jù)庫連接池連接

    關(guān)于node+mysql數(shù)據(jù)庫連接池連接

    這篇文章主要介紹了關(guān)于node+mysql數(shù)據(jù)庫連接池連接,mysql有兩種連接方式:一種是直接連接 另一種是池化連接,我們這篇講的是池化連接,需要的朋友可以參考下
    2023-04-04
  • node創(chuàng)建Vue項(xiàng)目步驟詳解

    node創(chuàng)建Vue項(xiàng)目步驟詳解

    在本篇文章里小編給大家整理的是關(guān)于node創(chuàng)建Vue項(xiàng)目步驟詳解內(nèi)容,需要的朋友們可以學(xué)習(xí)下。
    2020-03-03
  • Node.js Continuation Passing Style( CPS與回調(diào))

    Node.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-06
  • NodeJS中的MongoDB快速入門詳細(xì)教程

    NodeJS中的MongoDB快速入門詳細(xì)教程

    MongoDB 是一個基于分布式文件存儲的數(shù)據(jù)庫,由 C++ 語言編寫。這篇文章主要介紹了NodeJS中的MongoDB快速入門詳細(xì)教程的相關(guān)資料,需要的朋友可以參考下
    2016-11-11
  • Express框架搭建項(xiàng)目的實(shí)現(xiàn)步驟

    Express框架搭建項(xiàng)目的實(shí)現(xiàn)步驟

    Express是一個基于Node.js平臺的輕量級Web應(yīng)用框架,它提供了簡潔的API和豐富的功能,本文主要介紹了Express框架搭建項(xiàng)目的實(shí)現(xiàn)步驟,感興趣的可以了解一下
    2024-06-06
  • node.js 開發(fā)指南 – Node.js 連接 MySQL 并進(jìn)行數(shù)據(jù)庫操作

    node.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-07
  • 詳解Puppeteer 入門教程

    詳解Puppeteer 入門教程

    本篇文章主要介紹了詳解Puppeteer 入門教程,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-05-05
  • Nodejs-cluster模塊知識點(diǎn)總結(jié)及實(shí)例用法

    Nodejs-cluster模塊知識點(diǎn)總結(jié)及實(shí)例用法

    在本篇文章里小編給大家整理的是一篇關(guān)于Nodejs-cluster模塊知識點(diǎn)總結(jié)及實(shí)例用法,有興趣的朋友們可以跟著學(xué)習(xí)下。
    2021-12-12
  • Node.js 的模塊知識匯總

    Node.js 的模塊知識匯總

    node.js中是通過模塊來劃分為單位來劃分所有功能的。每個模塊為一個js文件。每個模塊中定義的全局變量或函數(shù)的作用范圍也被限制在這個模塊中,只能用exports對象將其傳遞到外部。
    2017-08-08

最新評論