koa 是什麼這裏不介紹了,這裏經過一個小例子結合源碼講一講它的實現。
<!-- more -->javascript
經過 npm 安裝 koa(v2.2.0) 後,代碼都在 lib 文件夾內,包括 4 個文件,application.js, context.js, request.js, response.js。
java
application.js 包含 app 的構造以及啓動一個服務器node
context.js app 的 context 對象, 傳入中間件的上下文對象git
request.js app 的請求對象,包含請求相關的一些屬性github
response.js app 的響應對象,包含響應相關的一些屬性npm
本文主要關於 application.js 。json
先看一個最簡單的例子數組
// app.js const Koa = require('koa') const app = new Koa() app.use(ctx => { ctx.body = 'hello world' }) app.listen(3000)
而後經過 node app.js
啓動應用,一個最簡單的 koa 服務器就搭建好了,瀏覽器訪問 http://localhost:3000
,服務器返回一個 hello world
的響應主體。promise
接下來經過源碼看看這個服務器是怎麼啓動的。瀏覽器
const app = new Koa()
, 很明顯 Koa
是一個構造函數。
module.exports = class Application extends Emitter {}
Application 類繼承了 nodejs 的 Events 類,從而能夠監聽以及觸發事件。
看一下構造函數的實現。
constructor() { super(); this.proxy = false; this.middleware = []; this.subdomainOffset = 2; this.env = process.env.NODE_ENV || 'development'; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response);
構造函數定義了一些 app 的實例屬性,包括 proxy, middleware, subdomainOffset, env, context, request, response等。
至此 咱們就生成了一個 app 的koa實例。
接下來就該用 app.use(middleware) 來使用中間件了。
use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); if (isGeneratorFunction(fn)) { deprecate('Support for generators will be removed in v3. ' + 'See the documentation for examples of how to convert old middleware ' + 'https://github.com/koajs/koa/blob/master/docs/migration.md'); fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; }
首先會驗證傳入的參數是不是一個函數。若是不是一個函數,會報錯。以後若是傳入的函數是一個generator 函數,那麼會將這個函數轉化爲一個 async 函數。使用的是 koa-convert
模塊, 這是一個很重要的模塊,能將不少 koa1 時代下的中間件轉化爲 koa2 下可用的中間件。而且注意到
Support for generators will be removed in v3.
在 koa3 中,將默認不支持 generator 函數做爲中間件。
以後將傳入的中間件函數推入 middleware 數組中,而且返回 this
以便鏈式調用。
app.use()
只是定義了一些要使用的中間件,並將它們放入 middleware 數組中,那麼怎麼使用這些中間件。來看看 app.listen
方法。
listen() { debug('listen'); const server = http.createServer(this.callback()); return server.listen.apply(server, arguments); }
app.listen 算是 node 原生 listen 方法的語法糖。經過 app.callback 方法生成一個 http.createServer 方法所須要的回調函數,而後再調用原生 http server 的 listen 方法。事實上也能夠發現,app 的 listen 方法接收 http server 的 listen 方法同樣的參數。
那麼再看看 app 的 callback 這個方法了,也是最重要的一個方法。
callback() { const fn = compose(this.middleware); if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { res.statusCode = 404; // 默認爲 404 const ctx = this.createContext(req, res); // 根據 node.js 原生的 req, res 對象生成一個 ctx 對象 const onerror = err => ctx.onerror(err); // onerror 回調函數 const handleResponse = () => respond(ctx); // 處理服務器響應 onFinished(res, onerror); return fn(ctx).then(handleResponse).catch(onerror); }; return handleRequest; }
能夠看到,callback 方法返回的 handleRequest 函數就是 http.createServer 方法所須要的回調函數。
callback 函數內,首先經過 koa-compose
模塊將全部的中間件合併成一箇中間件函數,以供 app.use
方法調用。隨後監聽一個 error
事件,onerror
做爲默認的錯誤處理函數。
onerror(err) { assert(err instanceof Error, `non-error thrown: ${err}`); if (404 == err.status || err.expose) return; if (this.silent) return; const msg = err.stack || err.toString(); console.error(); console.error(msg.replace(/^/gm, ' ')); console.error(); }
onerror
函數只是僅僅輸出 error.stack
做爲錯誤信息。
handleRequest
函數內完成了對請求的處理以及對響應結果的返回。首先 app.createContext
方法生成一個 ctx
供中間件函數 fn
調用。
createContext(req, res) { const context = Object.create(this.context); const request = context.request = Object.create(this.request); // request 屬性 const response = context.response = Object.create(this.response); // response 屬性 context.app = request.app = response.app = this; // request 和 response 上得到 app 屬性,指向這個 app 實例 context.req = request.req = response.req = req; // req 屬性,req 是原生 node 的請求對象 context.res = request.res = response.res = res; // res 屬性,res 是原生 node 的響應對象 request.ctx = response.ctx = context; // request 和 response 上得到 ctx 屬性,指向 context 對象 request.response = response; response.request = request; // request 和 response 互相指向對方 context.originalUrl = request.originalUrl = req.url; // 得到 originalUrl 屬性,爲原生 req 對象的 url 屬性 context.cookies = new Cookies(req, res, { keys: this.keys, secure: request.secure }); // cookie 屬性 request.ip = request.ips[0] || req.socket.remoteAddress || ''; // ip 屬性 context.accept = request.accept = accepts(req); // accept 屬性,是個方法,用於判斷 Content-Type context.state = {}; // context.state 屬性,用於保存一次請求中所須要的其餘信息 return context; }
因此,createContext 方法將一些經常使用的屬性,如 resquest , response, node 原生 req, res 掛載到 context 對象上。
再來看這句話 const handleResponse = () => respond(ctx)
.
function respond(ctx) { // allow bypassing koa if (false === ctx.respond) return; const res = ctx.res; if (!ctx.writable) return; let body = ctx.body; // 響應主體 const code = ctx.status; // 響應狀態碼 // ignore body if (statuses.empty[code]) { // 這裏具體是指 204 205 304 三種 // strip headers ctx.body = null; return res.end(); } if ('HEAD' == ctx.method) { // 若是是 `HEAD` 方法 if (!res.headersSent && isJSON(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body)); } return res.end(); } // status body if (null == body) { // 若是沒有設置 body , 設置ctx.message 爲 body。固然默認是 Not Found ,由於 status 默認是 404 body = ctx.message || String(code); if (!res.headersSent) { ctx.type = 'text'; ctx.length = Buffer.byteLength(body); } return res.end(body); } // 如下根據 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); }
因此 respond 函數的做用就是,根據傳入的 ctx 對象的 body ,method 屬性來決定對 request 處理的方式以及如何 response。
onFinished(res, onerror)
首先看看 onFinished(res, listener)
函數的介紹
Attach a listener to listen for the response to finish. The listener will be invoked only once when the response finished. If the response finished to an error, the first argument will contain the error. If the response has already finished, the listener will be invoked.
也就是當服務端響應完成後,執行 listener 回調函數,若是響應過程當中有錯誤發生,那麼 error 對象將做爲 listen 回調函數的第一個參數,所以 onFinished(res, onerror)
表示 當 koa 服務器發送完響應後,若是有錯誤發生,執行 onerror
這個回調函數。
return fn(ctx).then(handleResponse).catch(onerror)
。來看看這一句,fn 以前說過了,是全部的中間件函數的 「集合」, 用這一個中間件來表示整個處理過程。同時 fn 也是一個 async 函數,執行結果返回一個 promise 對象,同時 handleResponse 做爲其 resolved 函數,onerror 是 rejected 函數。
總結一下,application.js 描述了 一個 koa 服務器(實例)生成的整個過程。
new Koa() 生成了一個 koa 實例
app.use(middleware) 定義了這個 app 要使用的中間件
app.listen 方法,經過 callback 將合併後的中間件函數轉化成一個用於 http server.listen 調用的回調函數,以後調用原生的 server.listen 方法。
全文完