標籤:

學習Koa

原生HTTP伺服器

學習過Nodejs的朋友肯定對下面這段代碼非常熟悉:

const http = require(http);let server = http.createServer((req, res) => { // ....回調函數,輸出hello world res.end(hello world!)})server.listen(3000)

就這樣簡單幾行代碼,就搭建了一個簡單的伺服器,伺服器以回調函數的形式處理HTTP請求。上面這段代碼還有一種更加清晰的等價形式,代碼如下:

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事件接收數據。

req是http.IncomingMessage實例(同時實現了Readable Stream介面),詳情請參看文檔

res是http.ServerResponse實例(同時實現了Writable Stream介面),詳情請參看文檔

Koa寫HTTP伺服器

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框架設計的理念。

Koa源代碼解析

要搞懂這個原理,最好的方法就是直接查看Koa的源代碼。Koa代碼寫的非常精簡,大約1700多行,難度並非太大,值得一看。

我們以上述demo為例,進行一個分析,我把koa的執行分為兩個階段,第一個階段:初始化階段,主要的工作為初始化使用到的中間件(async/await形式)並在指定埠偵聽,第二個階段:請求處理階段,請求到來,進行請求的處理。

初始化階段

第一個階段主要使用的兩個函數就是app.useapp.listen。這兩個函數存在application.js中。

app.use最主要的功能將中間件推入一個叫middleware的list中。

use(fn) { ... this.middleware.push(fn); return this; }

listen的主要作用就是採用我們第一部分的方式創建一個http伺服器並在指定埠進行監聽。request事件的監聽函數為this.callback(),它返回(req, res) => {}類型的函數。

listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); }

分析一下callback函數,代碼如下:

/** * Return a request handler callback * for nodes 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實現了洋蔥模式。

推薦閱讀:

a=new b()和a=b(),其本質的區別在哪?
Promise then中回調為什麼是非同步執行?
前端領域流行的js版本控制方法,以及他們之間的區別?
vuejs怎麼在伺服器部署?
node.js 入門請推薦本好的入門書籍?

TAG:koa | Nodejs |