本文基於Koa@2.5.0
Koa
是基於Node.js
的HTTP
框架,由Express
原班人馬打造。是下一代的HTTP
框架,更簡潔,更高效。javascript
咱們來看一下下載量(2018.3.4)java
Koa
:471,451 downloads in the last month
Express
:18,471,701 downloads in the last month
說好的Koa
是下一代框架呢,爲何下載量差異有這麼大呢,Express
必定會說:你大爺仍是你大爺!。webpack
確實,好多知名項目仍是依賴Express
的,好比webpack的dev-server就是使用的Express
,因此仍是看場景啦,若是你喜歡DIY,喜歡絕對的控制一個框架,那麼這個框架就應該什麼功能都不提供,只提供一個基礎的運行環境,全部的功能由開發者本身實現。git
正是因爲Koa
的高性能和簡潔,好多知名項目都在基於Koa
,好比阿里的eggjs
,360奇舞團的thinkjs
。github
因此,雖然從使用範圍上來說,Express
對於Koa
是你大爺仍是你大爺!,可是若是Express
很好,爲何還要再造一個Koa
呢?接下來咱們來了解下Koa
到底帶給咱們了什麼,Koa
到底作了什麼。web
先來看兩段demo。數組
下面是Node
官方給的一個HTTP的示例。promise
const http = require('http'); const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World\n'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
下面是最簡單的一個Koa
的官方實例。cookie
const Koa = require('koa'); const app = new Koa(); app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000);
Koa
是一個基於Node
的框架,那麼底層必定也是用了一些Node
的API。app
jQuery
很好用,可是jQuery
也是基於DOM,逃不過也會用element.appendChild
這樣的基礎API。Koa
也是同樣,也是用一些Node
的基礎API,封裝成了更好用的HTTP框架。
那麼咱們是否是應該看看Koa
中http.createServer
的代碼在哪裏,而後順藤摸瓜,瞭解整個流程。
Koa
的源碼有四個文件
咱們主要關心application.js
中的內容,直接搜索http.createServer
,會搜到
listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); }
恰好和Koa
中的這行代碼app.listen(3000);
關聯起來了。
找到源頭,如今咱們就能夠梳理清楚主流程,你們對着源碼看我寫的這個流程
fn:listen ∨ fn:callback ∨ [fn:compose] // 組合中間件 會生成後面的 fnMiddleware ∨ fn:handleRequest // (@closure in callback) ∨ [fn(req, res):createContext] // 建立上下文 就是中間件中用的ctx ∨ fn(ctx, fnMiddleware):handleRequest // (@koa instance) ∨ code:fnMiddleware(ctx).then(handleResponse).catch(onerror); ∨ fn:handleResponse ∨ fn:respond ∨ code:res.end(body);
從上面能夠看到最開始是listen
方法,到最後HTTP的res.end
方法。
listen
能夠理解爲初始化的方法,每個請求到來的時候,都會通過從callback
到 respond
的生命週期。
在每一個請求的生命週期中,作了兩件比較核心的事情:
多箇中間件組合後,會前後處理ctx對象,ctx對象中既包含的req,也包含了res,也就是每一箇中間件的對象均可以處理請求和響應。
這樣,一次HTTP請求,接連通過各個中間件的處理,再到返回給客戶端,就完成了一次完美的請求。
app.use(async ctx => { ctx.body = 'Hello World'; });
上面的代碼是一個最簡單的中間件,每一箇中間件的第一個參數都是ctx
,下面咱們說一下這個ctx
是什麼。
建立ctx
的代碼:
createContext(req, res) { const context = Object.create(this.context); const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); context.app = request.app = response.app = this; context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; context.cookies = new Cookies(req, res, { keys: this.keys, secure: request.secure }); request.ip = request.ips[0] || req.socket.remoteAddress || ''; context.accept = request.accept = accepts(req); context.state = {}; return context; }
直接上代碼,Koa每次請求都會建立這樣一個ctx對象,以提供給每一箇中間件使用。
參數的req, res
是Node原生的對象。
下面解釋下這三個的含義:
context
:Koa封裝的帶有一些和請求與相應相關的方法和屬性request
:Koa封裝的req對象,好比提了供原生沒有的host
屬性。response
:Koa封裝的res對象,對返回的body
hook了getter和setter。其中有幾行一堆 xx = xx = xx
,這樣的代碼。
是爲了讓ctx、request、response,可以互相引用。
舉個例子,在中間件裏會有這樣的等式
ctx.request.ctx === ctx ctx.response.ctx === ctx ctx.request.app === ctx.app ctx.response.app === ctx.app ctx.req === ctx.response.req // ...
爲何會有這麼奇怪的寫法?其實只是爲了互相調用方便而已,其實最經常使用的就是ctx。
打開context.js
,會發現裏面寫了一堆的delegate
:
/** * Response delegation. */ delegate(proto, 'response') .method('attachment') .method('redirect') .method('remove') .method('vary') .method('set') .method('append') .method('flushHeaders') .access('status') .access('message') .access('body') .access('length') .access('type') .access('lastModified') .access('etag') .getter('headerSent') .getter('writable'); /** * Request delegation. */ delegate(proto, 'request') .method('acceptsLanguages') .method('acceptsEncodings') .method('acceptsCharsets') .method('accepts') .method('get') .method('is') .access('querystring') .access('idempotent') .access('socket') .access('search') .access('method') .access('query') .access('path') .access('url') .getter('origin') .getter('href') .getter('subdomains') .getter('protocol') .getter('host') .getter('hostname') .getter('URL') .getter('header') .getter('headers') .getter('secure') .getter('stale') .getter('fresh') .getter('ips') .getter('ip');
是爲了把大多數的request
、response
中的屬性也掛在ctx
下,咱們爲了拿到請求的路徑須要ctx.request.path
,可是因爲代理過path
這個屬性,ctx.path
也是能夠的,即ctx.path === ctx.request.path
。
ctx
模塊大概就是這樣,沒有講的特別細,這塊是重點不是難點,你們有興趣本身看看源碼很方便。
一個小tip: 有時候我也會把context.js
中最下面的那些delegate
當成文檔使用,會比直接看文檔快一點。
ctx
:上面講過的在請求進來的時候會建立一個給中間件處理請求和響應的對象,好比讀取請求頭和設置響應頭。next
:暫時能夠理解爲是下一個中間件,其實是被包裝過的下一個中間件。咱們來看這樣的代碼:
// 第一個中間件 app.use(async(ctx, next) => { console.log('m1.1', ctx.path); ctx.body = 'Koa m1'; ctx.set('m1', 'm1'); next(); console.log('m1.2', ctx.path); }); // 第二個中間件 app.use(async(ctx, next) => { console.log('m2.1', ctx.path); ctx.body = 'Koa m2'; ctx.set('m2', 'm2'); next(); debugger console.log('m2.2', ctx.path); }); // 第三個中間件 app.use(async(ctx, next) => { console.log('m3.1', ctx.path); ctx.body = 'Koa m3'; ctx.set('m3', 'm3'); next(); console.log('m3.2', ctx.path); });
會輸出什麼呢?來看下面的輸出:
m1.1 / m2.1 / m3.1 / m3.2 / m2.2 / m1.2 /
來解釋一下上面輸出的現象,因爲將next
理解爲是下一個中間件,在第一個中間件執行next
的時候,第一個中間件就將執行權限
給了第二個中間件,因此m1.1
後輸出的是m2.1
,在以後是m3.1
。
那麼爲何m3.1
後面輸出的是m3.2
呢?第三個中間件以後已經沒有中間件了,那麼第三個中間件裏的next
又是什麼?
我先偷偷告訴你,最後一箇中間件的next
是一個馬上resolve的Promise,即return Promise.resolve()
,一會再告訴你這是爲何。
因此第三個中間件(即最後一箇中間件)能夠理解成是這樣子的:
app.use(async (ctx, next) => { console.log('m3.1', ctx.path); ctx.body = 'Koa m3'; ctx.set('m3', 'm3'); new Promise.resolve(); // 原來是next console.log('m3.2', ctx.path); });
從代碼上看,m3.1
後面就會輸出m3.2
。
那爲何m3.2
以後又會輸出m2.2
呢?,咱們看下面的代碼。
let f1 = () => { console.log(1.1); f2(); console.log(1.2); } let f2 = () => { console.log(2.1); f3(); console.log(2.2); } let f3 = () => { console.log(3.1); Promise.resolve(); console.log(3.2); } f1(); /* outpout 1.1 2.1 3.1 3.2 2.2 1.2 */
這段代碼就是純函數調用而已,從這段代碼是否是發現,和上面一毛同樣,對一毛同樣,若是將next
理解成是下一個中間件的意思,就是這樣。
用戶使用中間件就是用app.use
這個API,咱們看看作了什麼:
// 精簡後去掉非核心邏輯的代碼 use(fn) { this.middleware.push(fn); return this; }
能夠看到,當咱們應用中間件的時候,只是把中間件放到一個數組中,而後返回this,返回this是爲了可以實現鏈式調用。
那麼Koa對這個數組作了什麼呢?看一下核心代碼
const fn = compose(this.middleware); // @callback line1 // fn 即 fnMiddleware return fnMiddleware(ctx).then(handleResponse).catch(onerror); // @handleRequest line_last
能夠看到用compose
處理了middleware
數組,獲得函數fnMiddleware
,而後在handleRequest
返回的時候運行fnMiddleware
,能夠看到fnMiddleware
是一個Promise
,resolve
的時候就會處理完請求,能猜到compose
將多箇中間件組合成了一個返回Promise
的函數,這就是奇妙之處,接下來咱們看看吧。
精簡後的compose
源碼
// 精簡後去掉非核心邏輯的代碼 00 function compose (middleware) { 01 return function (context, next) { // fnMiddleware 02 return dispatch(0) 03 function dispatch (i) { 04 let fn = middleware[i] // app.use的middleware 05 if (!fn) return Promise.resolve() 06 return fn(context, function next () { 07 return dispatch(i + 1) 08 }) 09 } 10 } 11 }
精簡後代碼只有十幾行,可是我認爲這是Koa
最難理解、最核心、最優雅、最奇妙的地方。
看着各類function,各類return有點暈是吧,不慌,不慌啊,一行一行來。
compose
返回了一個匿名函數,這個匿名函數就是fnMiddleware
。
剛纔咱們是有三個中間件,大家準備好啦,請求已通過來啦!
當請求過來的時候,fnMiddleware
就運行了,即運行了componse
返回的匿名函數,同時就會運行返回的dispatch(0)
,那咱們看看dispatch(0)
作了什麼,仔細一看其實就是
// dispatch(0)的時候,fn即middleware[0] return middleware[0](context, function next (){ return dispatch(1); }) // 上面的context和next即中間件的兩個參數 // 第一個中間件 app.use(async(ctx, next) => { console.log('m1.1', ctx.path); ctx.body = 'Koa m1'; ctx.set('m1', 'm1'); next(); // 這個next就是dispatch(1) console.log('m1.2', ctx.path); });
同理,在第二個中間件裏面的next
,就是dispatch(2)
,也就是用上面的方法被包裹一層的第三個中間件。
next
是什麼?能夠看到精簡過的compose
中05行
有個判斷,若是fn
不存在,會返回Promise.resolve()
,第三個中間件的next
是dispatch(3)
,而一共就有三個中間件,因此middleware[3]是undefined
,觸發了分支判斷條件,就返回了Promise.resolve()
。
再來複盤一下:
fnMiddleware()
,即會運行dispatch(0)
調起第一個中間件。next
是dispatch(1)
,運行next
的時候就調起第二個中間件
。next
是dispatch(2)
,運行next
的時候就調起第三個中間件
。next
是dispatch(3)
,運行next
的時候就調起Promise.resolve()
。能夠把Promise.resolve()
理解成一個空的什麼都沒有乾的中間件。到此,大概知道了多箇中間件是如何被compose
成一個大中間件的了吧。
在koa2
中,支持三種類型的中間件:
common function
:普通的函數,須要返回一個promise
。generator function
:須要被co
包裹一下,就會返回一個promise
。async function
:直接使用,會直接返回promise
。能夠看到,不管哪一種類型的中間件,只要返回一個promise
就行了,由於這行關鍵代碼return fnMiddleware(ctx).then(handleResponse).catch(onerror);
,能夠看到Koa
將fnMiddleware
的返回值認爲是promise
。若是傳入的中間件運行後沒有返回promise
,那麼會致使報錯。
Koa
的原理就解析到這裏啦,歡迎交流討論。
爲了更好地讓你們學習Koa
,我寫了一個mini版本的Koa
,你們能夠看一下 https://github.com/geeknull/t...