本文咱們不講解express的源碼。可是express的實現機制對於咱們瞭解 TJ 在設計框架時的思路有必定的參考意義。express 實現了一個相似於流的請求處理過程,其源碼比 Koa 還要稍微複雜一點(主要是其內置了Router概念來實現路由)。若是對 express 的源碼感興趣的能夠參考這兩篇文章:node
exporess和koa都是用來對http請求進行 接收、處理、響應。在這個過程當中,express和koa都有提供中間件的能力來對請求和響應進行串聯。同時要提供一個封裝好的 執行上下文 來串聯中間件。github
所以,koa和express就是把這些http處理能力打包在一塊兒的一個完整的後端應用框架。涉及到了一個請求處理的完整流程,其中包含了這些知識概念:Application、Request、Response、COntext、Session、Cookie。express
express跟koa的區別是,express使用的ES5時代的語言能力(沒有使用generator和async),所以express實現的中間件機制是傳統的串行的流式運行(從第一個運行到最後一個後輸出響應);而koa使用了generator或async從而實現了一種洋蔥模型的中間件機制,所謂洋蔥模型實際上就是中間件函數在運行過程當中能夠停下來,把執行權交給後面的中間件,等到合適的時機再回到函數內繼續往下執行npm
本文咱們仍是主要分析 Koa1 的代碼(由於 Generator 比 async 要繞一些難一些),我看的代碼是基於 Koa 1.6.0。json
對於 Koa1 來講,其實現是基於 ES6 的 Generator 函數。Generator 給了咱們用同步代碼編寫異步的可能,他可讓程序執行流 流向
下方,在異步結束以後再返回以前的地方執行。Generator 就像一個迭代器,能夠經過它的 next 方法不斷去迭代來實現函數的步進式執行。對於 Generator 函數解決異步問題的學習能夠參考 阮一峯的 ES6 教程 Generator函數與異步後端
Koa 內核只有 1千 行左右的代碼。共包含 4 個文件:api
application.js request.js response.js context.js
咱們從 package.json
中能夠看到 Koa 的主入口是 lib/application.js
. 這個入口作的事情即是導出了一個 Application 的class類。(能夠看到 Koa 的實現相比express已經比較面向對象了)數組
而 Application 的 prototype 上被掛載了咱們經常使用的 application 的方法,例如 use
, listen
, callback
, onerror
。
咦?是否是少了點 API? app.env,app.proxy這些呢?
原來,這些是 Application 的實例屬性,在 Application 實例化的時候會同步初始化。來看一下 Application 構造函數的代碼:
function Application() { if (!(this instanceof Application)) return new Application; // 支持工廠函數模式建立 this.env = process.env.NODE_ENV || 'development'; // 設置當前環境 this.subdomainOffset = 2; // 應用的子域偏移(這個主要是控制request.subdomain如何返回當前域名的哪一個部分;具體可參考文檔的request.subdomains) this.middleware = []; // 存放應用中間件的數組 this.proxy = false; // 是否信任代理。爲true時會讓request.ips/hosts等字段讀取X-Forward-*頭 this.context = Object.create(context); // 在app掛載一個繼承 context 對象的對象。 this.request = Object.create(request); // 在app掛載一個繼承 request 對象的對象 this.response = Object.create(response); // 在app掛載一個繼承 response 對象的對象 }
Application 類型的實現就是如此簡單,除此以外,還繼承了 EventEmitter 從而提供事件能力:
Object.setPrototypeOf(Application.prototype, Emitter.prototype);
至此,Application 上的方法和屬性咱們都找到源頭了,暫且先不分析其方法的具體實現。咱們再來看看 request 和 response 對象。
而從 request.js
中能夠看到,該文件就僅僅導出了一個Object對象,對象中全部函數和屬性便是 Koa 中間件中 request api
的全部方法。簡要摘錄下該文件源碼結構:
// request.js module.exports = { get header() { return this.req.headers; } }
注意到這裏面的 api 基本上就是對 Node.js 原生的 http.IncomingMessage 類型 API 的封裝; response.js 也是相似的; context.js 也是相似的,並代理掛載了 request 和 response 的一些方法。那這裏問題就來了: 上面代碼中 this.req
爲何能夠拿到 IncomingMessage 對象呢?這就要從 Koa 中間件是如何運行提及了。
咱們先看下中間件是如何注入到應用中的。咱們在開發 Koa 應用時,一般是使用 app.use 來註冊中間件。
app.use(function * (next) { this.body = '123' yield next })
而 use 函數作了一件很簡單的事情: 把你的中間件置入 app.middleware 數組。
// 簡化後的 use 函數 app.use = function(fn){ this.middleware.push(fn); return this; };
因爲 use 函數同時返回了 this 指針,所以 app.use 得以能夠鏈式調用。再回到咱們的話題: 中間件是如何運行起來的。 咱們看下 Koa 的啓動代碼:
http.createServer(app.callback()).listen(3000); // 或 app.listen(3000)
因爲 listen 是一個語法糖,所以 http請求 最終都是被 app.callback() 函數返回的一個 function 來執行。 咱們看看 callback 到底返回了一個什麼函數, 下面是我去掉了一些可有可無的 error 處理代碼以後的源碼:
app.callback = function(){ var fn = co.wrap(compose(this.middleware)); // 把全部中間件包裝成一個fn函數 var self = this; // 返回一個閉包 return function handleRequest(req, res){ var ctx = self.createContext(req, res); // 把Node原生的req和res包裝成Koa的context對象 self.handleRequest(ctx, fn); // 開始執行中間件 } };
其實原理很簡單了,就是把 http 請求利用 createContext函數包裝爲 context 對象,而後調用 app.handleRequest 把應用內全部中間件執行一遍並返回結果給瀏覽器。
還記得上文提到的一個問題: 爲何 request對象內能夠用 this.req
拿到原生請求? 原理在這裏就顯而易見了,正是 self.createContext 把原生 req 設置在了 ctx 對象上(這裏就不展開源碼講解了)
如今流程基本清楚了。但這裏有個難點:
咱們若是讀過koa文檔,會發如今中間件中 this/ctx 上是能夠訪問到 ctx.request 對象上的屬性的。這個是由於 koa 在初始化context對象的過程當中,把request上相關的屬性掛載到了ctx.
這是中間件執行以前建立ctx的過程:
app.createContext = function(req, res){ var context = Object.create(this.context); var request = context.request = Object.create(this.request); // ctx能夠訪問request對象 var response = context.response = Object.create(this.response); context.app = request.app = response.app = this; // ctx能夠訪問app對象 context.req = request.req = response.req = req; //ctx能夠訪問原生req對象 context.res = request.res = response.res = res; request.ctx = response.ctx = context; // request對象能夠訪問ctx request.response = response; // request和rewspinse能夠互相訪問 response.request = request; context.onerror = context.onerror.bind(context); context.originalUrl = request.originalUrl = req.url; context.cookies = new Cookies(req, res, { keys: this.keys, secure: request.secure }); context.accept = request.accept = accepts(req); context.state = {}; return context; };
這段代碼仍是沒法解釋爲何ctx上能夠訪問request對象的上的屬性。可是這裏有一點是有做用的:ctx對象上面掛載了request對象。所以,在ctx的方法中能夠經過 this.request
訪問到request對象,這爲ctx提供了訪問request屬性的基礎。
上述的問題的答案,其實在context對象初始化的過程中。咱們看看context對象的初始化時作了個什麼事情:
delegate(proto, 'request') .method('acceptsLanguages') .method('acceptsEncodings') .method('acceptsCharsets') .access('querystring') .access('idempotent') .access('socket') .access('search') ...
能夠看到,這裏調用delegate這個庫,給context對象添加了不少方法。實際上從deleteate源碼中得知,delegate原型是這樣的:
Delegator.prototype.method = function(name){ var proto = this.proto; var target = this.target; this.methods.push(name); proto[name] = function(){ return this[target][name].apply(this[target], arguments); }; return this; };
這個很明顯就是給ctx添加方法函數,函數內調用目標對象的方法。access是經過getter,setter來訪問this[target]的屬性。
至此,ctx能夠訪問request和response屬性的謎底就解開了。
中間件的執行流程和koa2是一致的。把中間件想做一個棧,請求會從頂部的第一個中間件開始處理,遇到yield next調用,就會進入下一個中間件中,直到最後沒有yield next調用,再從棧底反彈,一個一個執行以前next以後的代碼。
上文講到了中間件執行主要靠這句代碼合併爲一個fn函數:
var fn = co.wrap(compose(this.middleware))
這裏 compose 是來自 koa-compose
這個模塊。 在前文《Koa教程-經常使用中間件》中,咱們已經瞭解了中間件的合併方式以及 koa-compose
的運做原理:總之就是經過 不斷實例化Generator並做爲參數傳遞給前一個Generator函數
的方式把多個 Generator 串聯起來,最終執行第一個中間件就至關於串聯執行全部中間件。
那麼,co.wrap 是什麼呢? 這裏看下 co 源碼 (co源碼4.6.0加註釋總共才237行):
co.wrap = function (fn) { createPromise.__generatorFunction__ = fn; return createPromise; function createPromise() { return co.call(this, fn.apply(this, arguments)); } }; function co(gen) { ... }
能夠看到,co.wrap 僅僅就是返回了一個閉包,該閉包用於利用 co 來執行原函數(關於co是如何執行Generator的,本文暫不講解)。看到這裏,會有點疑惑,wrap包裹一層這是否是有點畫蛇添足啊?實際上我上面省略了一點點代碼,這裏 Koa 是爲了兼容 ES7 可能不須要co來運行中間件的狀況。這裏fn函數賦值的原始代碼以下:
// ES7 合併後的中間件函數能夠直接執行。ES6 generator的方式須要藉助CO執行。 fn函數屏蔽了底層差別 var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware));
至此,咱們已經梳理出整個 http 請求的流程,即: Koa 收到 http 鏈接回調後,對InCompingMessage進行包裝爲 ctx, 並調用中間件合併後的函數 fn 進行業務處理。業務處理的代碼很是簡單,就是以ctx爲上下文執行中間件:
// handleRequest即是收到網絡請求後中間件運行的起點 app.handleRequest = function(ctx, fnMiddleware){ ctx.res.statusCode = 404; onFinished(ctx.res, ctx.onerror); // 注意這裏: fnMiddleware.call(ctx) 就把中間件執行上下文設置爲了 context 對象 fnMiddleware.call(ctx).then(function handleResponse() { respond.call(ctx); }).catch(ctx.onerror); };
在開發 Koa 應用時,咱們知道在中間件中使用 this
就是在訪問 context 對象. 正是由於在 Koa 執行中間件函數時將上下文設置爲了 context 對象。
這個主要是在 co 執行中間件 resolve 以後利用了上文代碼中看到的 respond 函數來實現。
fnMiddleware.call(ctx).then(function handleResponse() { respond.call(ctx); }).catch(ctx.onerror);
respond函數主要是對ctx上設置的body內容進行解析,並選擇合適的方法響應給瀏覽器。這裏限於篇幅再也不具體講解了。
yield *
另外咱們發現,在 Koa 的中間件裏,咱們一般用:
yield next
來運行下一個中間件。經過上面的原理,咱們瞭解到所謂的 next 變量 實際上就是下一個中間件 Generator函數的實例,但是咱們會疑惑右值是一個Generator對象的時候 運行 Generator實例爲何沒有使用 yield *
? 理論上,若是按照 Generator的原始執行方式,沒有使用 yield *
的話,這個語句只會返回 next 這個遍歷器對象而已,是沒法運行 next 函數的
這個問題的答案就在 co 源碼裏面,若是看過 co 的源碼,會發現它是經過 右值.then(data=>{...})
回調裏不斷遞歸調用 gen.next(data)
來實現自動執行。而 gen.next 又會返回一個新的右值 {value: xx, done:false} ,co經過 toPromise 函數對右值進行Promise化從而能夠調用 then,而 toPromise函數中若是檢測到這個右值是一個Generator遍歷器對象,則會從新用 co 來 run 這個對象。
所以,co 裏面能夠支持使用了 yield *
的方式(這種方式 Generator默認會展開下一層的遍歷器);也能夠支持 yield + 遍歷器
的方式,這種方式是 co 本身檢測到並運行這個迭代器的。
Koa2 主要是用 async await 代替了 Generator,用起來更方便了。async await 是 Generator 的語法糖,能夠這樣理解:
await --> 等於 yield async --> 等於 Generator函數聲明: function * () {} 調用 async 函數 --> 等於利用 co 來自動執行 Generator: co(function*(){})
co 運行器返回的是一個 Promise, async 函數運行後也是返回一個 Promise。
Koa2 裏面直接使用了 ES6 語法來建立 Application 類型:
module.exports = class Application extends Emitter {}
Koa2 中的 app.use、app.listen 等實現與 Koa1 基本徹底一致。區別開始出如今 app.callback 函數裏:
callback() { const fn = compose(this.middleware); // 合併中間件 if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror); }
能夠看到 handleRequest 裏面調用中間件函數 fnMiddleware 時再也不設置上下文,而是直接傳遞 ctx 到中間件中。所以 Koa 在中間件裏不是經過 this 獲取上下文,而是用 ctx 變量。
另一個主要區別就在於上面代碼中這個 compose 了。
Koa2 使用了 4.x 版本的 koa-compose, 其實現有一些變化:
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 就是專門針對 async 函數而設計的了。 它最終返回的是個 function (context, next)
這樣的閉包函數。 在上文講到的 Koa2 的請求入口裏 fnMiddleware(ctx)
的 fnMiddleware 實際上就是在調用這個函數。能夠看到這個函數只接收了 context 參數,而 next 參數是 undefined。
咱們再來看看這個函數內部作了啥。它實際上從 middleware數組的第 0 項開始觸發執行(dispath(0)),至關於主動在調用中間件async函數:
yourmid(ctx, next)
而這個 next 實際上傳入的是下一個中間件函數。由此造成了遞歸調用。直到最後中間件沒有了, fn = next 被賦值爲undefined(由於next的值是undefined),而後回溯。回溯後返回的 Promise 交給 handleResponse 響應或錯誤處理:
const onerror = err => ctx.onerror(err); return fnMiddleware(ctx).then(handleResponse).catch(onerror);
能夠看到 Koa2 的錯誤處理機制,跟 Koa1 也是同樣的,都是中間件中一旦發生 throw Error,則會觸發 fnMiddleware 的 catch,進而觸發 context 對象的 onerror,在 ctx.onerror 裏面會作出瀏覽器響應並調用 app.onerror 兜底。
以上就是 Koa2 的執行流程。跟 Koa1 差異不是很大,這裏沒有再過多展開了,若是但願瞭解更詳細的 Koa2 源碼解讀,這裏推薦一篇知乎專欄吧
咱們知道在 http 協議中, 服務器端通常使用 http 報文的 if-none-match
if-modify-since
字段來進行緩存協商。 Koa 提供了一個 request.fresh 函數來幫助你肯定是否返回 304.
這個 fresh 函數的實現基於 npm 模塊 fresh
. 它內部會檢查當前 response 響應頭的 etag 和 last-modifyed 與 請求頭裏的 對應字段進行比對判斷。
這個能夠用在 Node 響應瀏覽器的最後一環時。
咱們先看一個簡陋版的router是怎麼作的。這個庫叫作 koa-route (注意不是koa-router哦)
這個route庫只作了一件事,就是幫咱們生成簡單的 generator 中間件,中間件的內容就是判斷當前請求的路徑是不是符合咱們的配置要求,符合才執行。
其用法以下:
var _ = require('koa-route'); app.use(_.get('/pets', pets.list)); app.use(_.get('/pets/:name', pets.show));
其中pets.list假設就是咱們針對 /pets
路徑的處理函數。
實際上 _.get() 會把你傳入的path包裝成一個對其進行判斷的generator中間件。相似於:
function * (next) { if (this.path === '/pets' && this.method === 'get') { ... } else { yield next } }
看他的源碼也只有聊聊幾行,核心在於這個create函數:
這個紅圈圈出來的部分就是實際的 _.get返回值,做爲中間件給註冊進了 Koa
TODO