學習過Nodejs的朋友確定對下面這段代碼很是熟悉:javascript
const http = require('http');
let server = http.createServer((req, res) => {
// ....回調函數,輸出hello world
res.end('hello world!')
})
server.listen(3000)
複製代碼
就這樣簡單幾行代碼,就搭建了一個簡單的服務器,服務器以回調函數的形式處理HTTP請求。上面這段代碼還有一種更加清晰的等價形式,代碼以下:java
let server = new http.Server();
server.on("request", function(req, res){
// ....回調函數,輸出hello world
res.end('hello world!')
});
server.listen(3000);
複製代碼
首先建立了一個HttpServer的實例,對該實例進行request事件監聽,server在3000端口進行監聽。HttpServer繼承與net.Server,它使用http_parser對鏈接的socket對象進行解析,當解析完成http header以後,會觸發request事件,body數據繼續保存在流中,直到使用data事件接收數據。node
req是http.IncomingMessage實例(同時實現了Readable Stream接口),詳情請參看文檔json
res是http.ServerResponse實例(同時實現了Writable Stream接口),詳情請參看文檔api
Koa 應用程序是一個包含一組中間件函數的對象,它是按照相似堆棧的方式組織和執行的。服務器
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
複製代碼
Koa寫http服務器的形式與咱們直接經過node http模塊寫的方式差異很大。第一部分析可知,node的http服務器建立來自於http.createServer等方法,Koa中是如何從原生方法封裝成koa形式的服務器呢?搞懂這個原理也就搞懂了Koa框架設計的理念。app
要搞懂這個原理,最好的方法就是直接查看Koa的源代碼。Koa代碼寫的很是精簡,大約1700多行,難度並不是太大,值得一看。 咱們以上述demo爲例,進行一個分析,我把koa的執行分爲兩個階段,第一個階段:初始化階段,主要的工做爲初始化使用到的中間件(async/await形式)並在指定端口偵聽,第二個階段:請求處理階段,請求到來,進行請求的處理。框架
第一個階段主要使用的兩個函數就是app.use和app.listen。這兩個函數存在application.js中。 app.use最主要的功能將中間件推入一個叫middleware的list中。koa
use(fn) {
...
this.middleware.push(fn);
return this;
}
複製代碼
listen的主要做用就是採用咱們第一部分的方式建立一個http服務器並在指定端口進行監聽。request事件的監聽函數爲this.callback(),它返回(req, res) => {}類型的函數。socket
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
複製代碼
分析一下callback函數,代碼以下:
/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */
callback() {
const fn = compose(this.middleware); // 將中間件函數合成一個函數fn
// ...
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res); // 使用req和res建立一個上下文環境ctx
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
複製代碼
至此第一個階段完成,經過源代碼的分析,咱們能夠知道它實際執行的內容跟咱們第一部分使用node http模塊執行的大概一致。這裏有一個疑問,compose函數是怎麼實現的呢?async/await函數返回形式爲Promise,怎麼保證它的順序執行呢?一開始個人猜測是將下一個middleware放在上一個middleware執行結果的then方法中,大概思路以下:
compose(middleware) {
return () => {
let composePromise = Promise.resolve();
middleware.forEach(task => { composePromise = composePromise.then(()=>{return task&&task()}) })
return composePromise;
}
}
複製代碼
最終達到的效果爲:f1().then(f2).then(f3).. Koa在koa-compose中用了另一種方式:
function compose (middleware) {
// ...
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, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製代碼
它從第一個中間件開始,遇到next,就中斷本中間件的代碼執行,跳轉到對應的下一個中間件執行期內的代碼…一直到最後一箇中間件,而後逆序回退到倒數第二個中間件next下部分的代碼執行,完成後繼續會退…一直會退到第一個中間件next下部分的代碼執行完成,中間件所有執行結束。從而實現咱們所說的洋蔥圈模型。
當一個請求過來時,它會進入到request事件的回調函數當中,在Koa中被封裝在handleRequest中:
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
// koa默認的錯誤處理函數,它處理的是錯誤致使的異常結束
const onerror = err => ctx.onerror(err);
// respond函數裏面主要是一些收尾工做,例如判斷http code爲空如何輸出,http method是head如何輸出,body返回是流或json時如何輸出
const handleResponse = () => respond(ctx);
// 第三方函數,用於監聽 http response 的結束事件,執行回調
// 若是response有錯誤,會執行ctx.onerror中的邏輯,設置response類型,狀態碼和錯誤信息等
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製代碼
請求到來時,首先執行第一個階段封裝的compose函數,而後進入handleResponse中進行一些收尾工做。至此,完成整個請求處理階段。
Koa是一個設計很是精簡的Web框架,源代碼自己不含任何中間件,可使咱們根據自身須要去組合一些中間件使用。它結合async/await實現了洋蔥模式。