const Koa = require('koa'); const app = new Koa(); // response app.use(ctx => { ctx.body = 'Hello Koa'; }); app.listen(3000);
application.js
: koa
的導出文件夾,也就是定義 koa 的地方context.js
: 上下文,也就是常見的 ctx
。每個 app
都會有一份 context
,經過繼承這個 context.js
中定義的 context
。同時每個請求中,都會有一份單獨的 ctx
, 經過繼承 app
中定義的 context。說白了就是複用。request.js
: 同前面的 context.js
說明。response.js
: 同前面的 context.js
說明。說明:在 koa
中 nodejs
原生對應的是 req
和 res
node
包的功能能夠參考截圖中給出的註釋json
這個就是 koa
的定義的類api
New 會執行構造函數。
因此在實例化的時候,能夠傳入這些 options
。數組
在這裏會檢查 middleware
的類型,若是是老的 middleware
會轉換一下,最後直接放到 middleware
這個數組中。數組中的中間件,會在每個請求中去挨個執行一遍。cookie
listen
的時候,纔會去建立 server
。
對於每個請求,都會走到 callback
中去,因此 callback
是用於處理實際請求的。通常不要去重寫這個 callback
。app
接下來去看看 callback
作了什麼:
這裏涉及到幾個大的點:dom
createContext
都幹了什麼Compose
是如何實現洋蔥模型的。this.handleRequest(ctx, fn)
幹了什麼這幾個點分紅兩個大塊來說,二、3 兩點放到一塊兒講。koa
這裏作了三件重要的事情socket
app
都有其對應的 context、request、response
實例,每個請求,都會基於這些實例去建立本身的實例。在這裏就是建立了 context、request、response
node
原生的 res、req
以及 this
掛載到 context、request、response
上面。還有一些其餘爲了方便訪問作得一些掛載,不過前面三個的掛載是必須的。context
返回,傳給全部中間件的第一個 ctx
參數,做爲這個請求的上下文下面着重解釋一下第二點中,爲何要把這些屬性掛載上去。由於全部的訪問都是代理,最終都是訪問的 req、res
上面的東西,context
訪問的是 request、response
上面的東西,可是他們上面的東西又是訪問的是 req、res
上面的。
例如訪問 ctx.method
,context
會去 request
上面早,而後 request
會返回 req.method
。後面分析其餘文件時會講到這種代理結構。ide
在第三步中最後講到的 callback
中,middleware
所有經過 koa-compose
這個包包裝,返回了一個可執行的方法,在請求階段會去執行這個方法,從而執行每個中間件。先本身來手擼一個 compose
的🌰
function compose(middleware) { return function (ctx, next) { function dispatch(i) { if (i >= middleware.length) { return Promise.resolve() } let curMiddleware = middleware[i] return curMiddleware(ctx, dispatch.bind(null, i + 1)) } return dispatch(0) } } function mid1(ctx, next) { console.log('mid1 before') next() console.log('mid1 after') } function mid2(ctx, next) { console.log('mid2 before') next() console.log('mid2 after') } function mid3(ctx, next) { console.log('mid3 before') next() console.log('mid3 after') } const fn = compose([mid1, mid2, mid3]) fn({}) --------------------------------------------------------------------打印結果 mid1 before mid2 before mid3 before mid3 after mid2 after mid1 after
在 compose
中會根據 i
去挨個執行中間件,而且有一個回溯的過程。官方代碼以下。
function compose (middleware) { if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }
因此總結一下,洋蔥模型本質是經過遞歸去實現的。
講了 compose
的原理以後,回到第三步中最後的 this.handleRequest(ctx, fn);
fn
就是compose
返回的包裝過 middleware
的函數。下面進入 handleRequest
能夠看到當一個請求來到的時候,最後會去執行包裝過的中間件函數,也就是這裏的最後一行,並在中間件執行完畢以後,到 handleResponse
中去處理響應。在 handleResponse
中最終執行的是 respond
function respond(ctx) { // allow bypassing koa if (false === ctx.respond) return; if (!ctx.writable) return; const res = ctx.res; let body = ctx.body; const code = ctx.status; // ignore body if (statuses.empty[code]) { // strip headers ctx.body = null; return res.end(); } if ('HEAD' === ctx.method) { if (!res.headersSent && !ctx.response.has('Content-Length')) { const { length } = ctx.response; if (Number.isInteger(length)) ctx.length = length; } return res.end(); } // status body if (null == body) { if (ctx.req.httpVersionMajor >= 2) { body = String(code); } else { body = ctx.message || String(code); } if (!res.headersSent) { ctx.type = 'text'; ctx.length = Buffer.byteLength(body); } return res.end(body); } // responses if (Buffer.isBuffer(body)) return res.end(body); if ('string' == typeof body) return res.end(body); if (body instanceof Stream) return body.pipe(res); // body: json body = JSON.stringify(body); if (!res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body); }
主要是將 ctx
上掛載的 body
經過 res.end
返回響應。
module.exports = { /** * Return request header. * * @return {Object} * @api public */ get header() { return this.req.headers; }, /** * Set request header. * * @api public */ set header(val) { this.req.headers = val; }, .............. }
可見前面在 createContext
的時候在 request
上面去掛載 req、res
的緣由就在這裏。
module.exports = { /** * Return the request socket. * * @return {Connection} * @api public */ get socket() { return this.res.socket; }, /** * Get response status code. * * @return {Number} * @api public */ get status() { return this.res.statusCode; }, ....................... }
const proto = module.exports = { ............. get cookies() { if (!this[COOKIES]) { this[COOKIES] = new Cookies(this.req, this.res, { keys: this.app.keys, secure: this.request.secure }); } return this[COOKIES]; }, set cookies(_cookies) { this[COOKIES] = _cookies; } ............ } /** * Response delegation. */ delegate(proto, 'response') .method('attachment') .method('redirect') .method('remove') .method('vary') .method('has') .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') .access('accept') .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');
這裏的 proto
就是 context
,在自身定義了一個經常使用的方法,可經過 ctx.method
去訪問,還有後面使用 delegate
,這個函數會把自 context
上面的 request、response
上面的一些屬性定義到 proto
也就是 context
上面去,可是當使用 ctx.xxx
去訪問的時候,實際上是訪問 request、response
上面的屬性,這也是爲何須要將 request、response
掛載到 context
上面去。