您好,登錄后才能下訂單哦!
前言
Node.js也是寫了兩三年的時間了,剛開始學習Node的時候,hello world就是創建一個HttpServer,后來在工作中也是經歷過Express、Koa1.x、Koa2.x以及最近還在研究的結合著TypeScript的routing-controllers(驅動依然是Express與Koa)。
用的比較多的還是Koa版本,也是對它的洋蔥模型比較感興趣,所以最近抽出時間來閱讀其源碼,正好近期可能會對一個Express項目進行重構,將其重構為koa2.x版本的,所以,閱讀其源碼對于重構也是一種有效的幫助。
Koa是怎么來的
首先需要確定,Koa是什么。
任何一個框架的出現都是為了解決問題,而Koa則是為了更方便的構建http服務而出現的。
可以簡單的理解為一個HTTP服務的中間件框架。
使用http模塊創建http服務
相信大家在學習Node時,應該都寫過類似這樣的代碼:
const http = require('http') const serverHandler = (request, response) => { response.end('Hello World') // 返回數據 } http .createServer(serverHandler) .listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))
一個最簡單的示例,腳本運行后訪問http://127.0.0.1:8888即可看到一個Hello World的字符串。
但是這僅僅是一個簡單的示例,因為我們不管訪問什么地址(甚至修改請求的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
所以我們可能會在回調中添加邏輯,根據路徑、Method來返回給用戶對應的數據:
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) // 返回數據 }
類似Express的實現
但是這樣的寫法還會帶來另一個問題,如果是一個很大的項目,存在N多的接口。
如果都寫在這一個handler里邊去,未免太過難以維護。
示例只是簡單的針對一個變量進行賦值,但是真實的項目不會有這么簡單的邏輯存在的。
所以,我們針對handler進行一次抽象,讓我們能夠方便的管理路徑:
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') } } }
然后通過實例化一個Router對象進行注冊對應的路徑,最后啟動服務:
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中的中間件
這樣,就實現了一個代碼比較整潔的HttpServer,但功能上依舊是很簡陋的。
如果我們現在有一個需求,要在部分請求的前邊添加一些參數的生成,比如一個請求的唯一ID。
將代碼重復編寫在我們的handler中肯定是不可取的。
所以我們要針對route的處理進行優化,使其支持傳入多個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') } } }
然后針對上邊的路徑監聽添加其他的handler:
function generatorId(request, response, next) { this.id = 123 next() } app.get('/', generatorId, function(request, response) { response.end(`Hello World ${this.id}`) })
這樣在訪問接口時,就可以看到Hello World 123的字樣了。
這個就可以簡單的認為是在Express中實現的 中間件。
中間件是Express、Koa的核心所在,一切依賴都通過中間件來進行加載。
更靈活的中間件方案-洋蔥模型
上述方案的確可以讓人很方便的使用一些中間件,在流程控制中調用next()來進入下一個環節,整個流程變得很清晰。
但是依然存在一些局限性。
例如如果我們需要進行一些接口的耗時統計,在Express有這么幾種可以實現的方案:
function beforeRequest(request, response, next) { this.requestTime = new Date().valueOf() next() } // 方案1. 修改原handler處理邏輯,進行耗時的統計,然后end發送數據 app.get('/a', beforeRequest, function(request, response) { // 請求耗時的統計 console.log( `${request.url} duration: ${new Date().valueOf() - this.requestTime}` ) response.end('XXX') }) // 方案2. 將輸出數據的邏輯挪到一個后置的中間件中 function afterRequest(request, response, next) { // 請求耗時的統計 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() // 記得調用,不然中間件在這里就終止了 }, afterRequest )
無論是哪一種方案,對于原有代碼都是一種破壞性的修改,這是不可取的。
因為Express采用了response.end()的方式來向接口請求方返回數據,調用后即會終止后續代碼的執行。
而且因為當時沒有一個很好的方案去等待某個中間件中的異步函數的執行。
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)
就像上述的示例,實際上log的輸出順序為:
before a before b after a before c
這顯然不符合我們的預期,所以在Express中獲取next()的返回值是沒有意義的。
所以就有了Koa帶來的洋蔥模型,在Koa1.x出現的時間,正好趕上了Node支持了新的語法,Generator函數及Promise的定義。
所以才有了co這樣令人驚嘆的庫,而當我們的中間件使用了Promise以后,前一個中間件就可以很輕易的在后續代碼執行完畢后再處理自己的事情。
但是,Generator本身的作用并不是用來幫助我們更輕松的使用Promise來做異步流程的控制。
所以,隨著Node7.6版本的發出,支持了async、await語法,社區也推出了Koa2.x,使用async語法替換之前的co+Generator。
Koa也將co從依賴中移除(2.x版本使用koa-convert將Generator函數轉換為promise,在3.x版本中將直接不支持Generator)
由于在功能、使用上Koa的兩個版本之間并沒有什么區別,最多就是一些語法的調整,所以會直接跳過一些Koa1.x相關的東西,直奔主題。
在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... })
因為一些語法糖的存在,遮蓋了代碼實際運行的過程,所以,我們使用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) }) }
大致代碼是這樣的,也就是說,調用next會給我們返回一個Promise對象,而Promise何時會resolve就是Koa內部做的處理。
可以簡單的實現一下(關于上邊實現的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(_ => { // 結束請求 response.end(context.body || '404') }) } else { response.end('404') } } }
每次調用中間件時就監聽then,并將當前Promise的resolve與reject處理傳入Promise的回調中。
也就是說,只有當第二個中間件的resolve被調用時,第一個中間件的then回調才會執行。
這樣就實現了一個洋蔥模型。
就像我們的log中間件執行的流程:
所以就像洋蔥一樣一層一層的包裹,最外層是最大的,是最先執行的,也是最后執行的。(在一個完整的請求中,next之前最先執行,next之后最后執行)
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。