KOA 與 CO 實現淺析


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.jsresponse.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 中,定義了三個函數 onFulfilledonRejectednext,先看下 next 的定義。

next 的參數實際上就是gen每次 gen.next() 的返回值。如果 gen 已經執行結束,那么 Promise 將返回;否則,將 ret.value promise 化,並再次調用 onFulfilledonRejected 函數。

onFulfilledonRejected 幫助我們推進 gen 的執行。

nextonFulfilledonRejected 的組合,實現了 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 對象,這個對象的 resolvereject 處理函數又分別是上一個 promise 中定義的 onFulfilledonRejected 函數。至此,就完成了 compose 函數返回的 generator 鏈的推進工作。

最后還有一個問題需要明確一下,那就是 KOA 中的 context 是如何傳遞的。
通過觀察前面的代碼不難發現,每次關鍵節點的函數調用都是使用的 xxxFunc.call(ctx) 的方式,這也正是為什么我們可以在中間件中直接通過 this 訪問 context 的原因。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM