koa2中介軟體實現原始碼解析

Guohjia發表於2018-03-01

眾所周知,koa2核心的部分就是middleware和context了,本文將從結合官網demo以及原始碼對其進行解讀

官網例子使用

const Koa = require('koa');
const app = new Koa();

// x-response-time

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// logger

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);
複製程式碼

上述程式碼做的事

  • 1.require('koa')執行koa原始碼中lib/application.js的匯出部分(這是因為可以看到koa原始碼中的package.json檔案的main為application.js,預設為Index.js),new Koa對其匯出進行例項化,便於我們呼叫application.js上的相關方法
  • 2.既然已經拿到方法了,app.use就是呼叫application的use方法,這裡就是初始化中介軟體,這裡每一個use就是一箇中介軟體,接下來解讀原始碼會提到
  • 3.app.listen(3000),這裡除了大家可以想到的nodejs的http server模組監聽3000埠,並且傳入req,res物件還有更重要的事,接收到請求使用中介軟體,同樣解讀原始碼會詳細解釋

原始碼解讀-中介軟體原理

根據上述程式碼做的事,我們從2開始:

首先http.createrServer,ceateServer方法接受一個函式作為引數,這個函式的兩個引數分別為request和response; 而在koa中接受一個callback,就是下面程式碼中的this.callback

listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
複製程式碼

上述callback已經執行,所以真正的createrServer接受的函式應該是callback的返回值,所以這和ceateServer方法接受一個函式作為引數不矛盾,本質上都是一樣的,接下來看具體的callback函式

callback() {
    const fn = compose(this.middleware);

    if (!this.listeners('error').length) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }
複製程式碼

返回值是const定義的handleRequest函式,這個函式首先建立一個context(在context掛載http請求響應狀態等一些資訊),然後將建立好的ctx和一個fn作為引數傳給了this.handleReguest;ctx很好理解,就是將一些http模組的req,res掛載到ctx上去,也就是我們koa的重要組成部分之一,請求上下文

  createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }
複製程式碼

關鍵是compose(this.middleware),他的引數this.middleware是個陣列,會在使用koa的use方法時,push進去函式,也就是我們開頭所提到的初始化中介軟體

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); //型別判斷
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn); //註釋提到了,generators將被棄用,這裡做了轉換,可以看koa-convert原始碼,使用co模組進行轉換,轉換成promise物件,並且自動呼叫Generator next物件,co模組實現之類的可以自行去看原始碼,這裡不多贅述
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);  //這裡是關鍵,在middleware陣列裡面新增了各個中介軟體方法,也就是app.use傳入的函式
    return this;
  }
複製程式碼

再來看compose原始碼

function compose (middleware) {
  //一些型別判斷,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!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  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 //避免同一個中介軟體多次呼叫next
      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)
      }
    }
  }
}

複製程式碼

可以通過原始碼看出,傳遞給this.handleRequest(ctx, fn)的fn就是這個compose的返回值,一個匿名函式: 接下里我們先不看這個匿名函式,繼續看到this.handleRequest,既然已經明確了它的兩個引數ctx和fn:

 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);  //匿名函式中的next應該始終是undefined,用來控制結束
  }
複製程式碼

之前說到fn就是compose返回的匿名函式,現在又將fn傳遞給了handleRequest,

控制權轉到fnMiddleware中,所以fnMiddlewar就是剛才我們沒有看的匿名函式,這裡通過尾遞迴呼叫依次控制執行中介軟體函式,

最終返回一個promise.resolve,這個resolve中的fn其實就是app.use中的函式,此時我們才執行了app.use中的函式,並且把ctx,next通過引數的形式穿給了這個函式;

所以中介軟體的宣告是在app.use,執行是在我們app.listen接受到請求的時候,這樣我們就可以在接受到請求的時候一次執行我們的中介軟體

綜上就是所有內容了,如有問題或異議,懇請指出,不甚感激~

相關文章