KOA 與 CO 的實現都很是的短小精悍,只須要花費很短的時間就能夠將源代碼通讀一遍。如下是一些淺要的分析。node
既然 KOA 實現了 web 服務器,那咱們就先從最原始的 web 服務器的實現方式着手。
下面的代碼中咱們建立了一個始終返回請求路徑的 web 服務器。web
const http = require('http'); const server = http.createServer((req, res) => { res.end(req.url); }); server.listen(8001);
當你請求 http://localhost:8001/some/url
的時候,獲得的響應就是 /some/url
。數組
簡單的說,KOA 就是對上面這段代碼的封裝。promise
首先看下 KOA 的大概目錄結構:服務器
lib
目錄下只有四個文件,其中 request.js
和 response.js
是對 node 原生的 request(req)
和 response(res)
的加強,提供了不少便利的方法,context.js
就是著名的上下文。咱們暫時拋開這三個文件的細節,先看下主文件 application.js
的實現。app
先關注兩個函數:dom
// 構造函數 function Application() { if (!(this instanceof Application)) return new Application; this.env = process.env.NODE_ENV || 'development'; this.subdomainOffset = 2; this.middleware = []; this.proxy = false; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } // listen 方法 app.listen = function(){ debug('listen'); var server = http.createServer(this.callback()); return server.listen.apply(server, arguments); };
上面的這兩個函數,正是完成了一個 web 服務器的創建過程:koa
const server = new KOA(); // new Application() server.listen(8001);
而先前 http.createServer()
的那個回調函數則被替換成了 app.callback
的返回值。函數
咱們細看下 app.callback
的具體實現:oop
app.callback = function(){ if (this.experimental) { console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.') } var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware)); var self = this; if (!this.listeners('error').length) this.on('error', this.onerror); return function handleRequest(req, res){ res.statusCode = 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror); fn.call(ctx).then(function handleResponse() { respond.call(ctx); }).catch(ctx.onerror); } };
先跳過 ES7 的實驗功能以及錯誤處理,app.callback
中主要作了以下幾件事情:
每當服務器接收到請求時,作以下處理:
co.wrap
返回的函數,並作必要的錯誤處理如今咱們把目光集中到這三行代碼中:
// 中間件重組與 co 包裝 var fn = co.wrap(compose(this.middleware)); // ------------------------------------------ // 在處理 request 的回調函數中 // 建立每次請求的上下文 var ctx = self.createContext(req, res); // 調用 co 包裝的函數,執行中間件 fn.call(ctx).then(function handleResponse() { respond.call(ctx); }).catch(ctx.onerror);
先看第一行代碼,compose
實際上就是 koa-compose
,實現以下:
function compose(middleware){ return function *(next){ if (!next) next = noop(); var i = middleware.length; while (i--) { next = middleware[i].call(this, next); } return yield *next; } } function *noop(){}
compose
返回一個 generator函數
,這個 generator函數
中倒序依次以 next
爲參數調用每一箇中間件,並將返回的generator實例
從新賦值給 next
,最終將 next
返回。
這裏比較有趣也比較關鍵的一點是:
next = middleware[i].call(this, next);
咱們知道,調用 generator函數
返回 generator實例
,當 generator函數
中調用其餘的 generator函數
的時候,須要經過 yield *genFunc()
顯式調用另外一個 generator函數
。
舉個例子:
const genFunc1 = function* () { yield 1; yield *genFunc2(); yield 4; } const genFunc2 = function* () { yield 2; yield 3; } for (let d of genFunc1()) { console.log(d); }
執行的結果是在控制檯依次打印 1,2,3,4。
回到上面的 compose
函數,其實它就是完成上面例子中的 genFunc1
調用 genFunc2
的事情。而 next
的做用就是保存並傳遞下一個中間件函數返回的 generator實例
。
參考一下 KOA 中間件的寫法以幫助理解:
function* (next) { // do sth. yield next; // do sth. }
經過 compose
函數,KOA 把中間件所有級聯了起來,造成了一個 generator
鏈。下一步就是完成上面例子中的 for-of
循環的事情了,而這正是 co 的工做。
仍是先看下 co.wrap
co.wrap = function (fn) { createPromise.__generatorFunction__ = fn; return createPromise; function createPromise() { return co.call(this, fn.apply(this, arguments)); } };
該函數返回一個函數 createPromise
,也就是 KOA 源碼裏面的 fn
。
當調用這個函數的時候,實際上調用的是 co
,只是將上下文 ctx
做爲 this
傳遞了進來。
如今分析下 co
的代碼:
function co(gen) { var ctx = this; var args = slice.call(arguments, 1) // 返回一個 promise return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.apply(ctx, args); if (!gen || typeof gen.next !== 'function') return resolve(gen); onFulfilled(); function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); } }); }
co
函數的參數是 gen
,就是以前 compose
函數返回的 generator實例
。
在 co
返回的 Promise 中,定義了三個函數 onFulfilled
、 onRejected
和 next
,先看下 next
的定義。
next
的參數實際上就是gen
每次 gen.next()
的返回值。若是 gen
已經執行結束,那麼 Promise 將返回;不然,將 ret.value
promise 化,並再次調用 onFulfilled
和 onRejected
函數。
onFulfilled
和 onRejected
幫助咱們推動 gen
的執行。
next
和 onFulfilled
、onRejected
的組合,實現了 generator
的遞歸調用。那麼到底是如何實現的呢?關鍵還要看 toPromise
的實現。
function toPromise(obj) { if (!obj) return obj; if (isPromise(obj)) return obj; if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); if ('function' == typeof obj) return thunkToPromise.call(this, obj); if (Array.isArray(obj)) return arrayToPromise.call(this, obj); if (isObject(obj)) return objectToPromise.call(this, obj); return obj; }
在 toPromise
函數中,後三個分支處理分別對 thunk 函數、數組和對象進行了處理,此處略去細節,只須要知道最終都調回了 toPromise
的前三個分支處理中。這個函數最終返回一個 promise 對象,這個對象的 resolve
和 reject
處理函數又分別是上一個 promise 中定義的 onFulfilled
和 onRejected
函數。至此,就完成了 compose
函數返回的 generator
鏈的推動工做。
最後還有一個問題須要明確一下,那就是 KOA 中的 context
是如何傳遞的。
經過觀察前面的代碼不難發現,每次關鍵節點的函數調用都是使用的 xxxFunc.call(ctx)
的方式,這也正是爲何咱們能夠在中間件中直接經過 this
訪問 context
的緣由。