KOA 與 CO 的實現都非常的短小精悍,只需要花費很短的時間就可以將源代碼通讀一遍。以下是一些淺要的分析。
如何用 node 實現一個 web 服務器
既然 KOA 實現了 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 的實現
簡單的說,KOA 就是對上面這段代碼的封裝。
首先看下 KOA 的大概目錄結構:
lib
目錄下只有四個文件,其中 request.js
和 response.js
是對 node 原生的 request(req)
和 response(res)
的增強,提供了很多便利的方法,context.js
就是著名的上下文。我們暫時拋開這三個文件的細節,先看下主文件 application.js
的實現。
先關注兩個函數:
// 構造函數
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 服務器的建立過程:
const server = new KOA(); // new Application()
server.listen(8001);
而先前 http.createServer()
的那個回調函數則被替換成了 app.callback
的返回值。
我們細看下 app.callback
的具體實現:
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 包裝
- 返回處理request的回調函數
每當服務器接收到請求時,做如下處理:
- 初始化上下文
- 調用之前
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 的原理分析
還是先看下 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
的原因。