koa2中間件koa和koa-compose源碼分析原理(一)

koa是基於nodejs平臺的下一代web開發框架,它是使用generator和promise,koa的中間件是一系列generator函數的對象。
當對象被請求過來的時候,會依次通過各個中間件進行處理,當有yield next就跳到下一個中間件,當中間件沒有 yield next執行的時候,而後就會逆序執行前面那些中間件剩下的邏輯代碼,好比看以下的demo:html

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

app.use(async (ctx, next) => {
  console.log(11111);
  await next();
  console.log(22222);
});

app.use(async (ctx, next) => {
  console.log(33333);
  await next();
  console.log(44444);
});

app.use(async (ctx, next) => {
  console.log(55555);
  await next();
  console.log(66666);
});
app.listen(3001);
console.log('app started at port 3000...');

// 執行結果爲 11111  33333 55555 66666 44444 22222

當咱們在瀏覽器訪問 http://localhost:3001 的時候,會分別輸出 11111 33333 55555 66666 44444 22222,如上代碼是以下執行的:請求的時候,會執行第一個use裏面的異步函數代碼,先打印出 11111,而後碰到 await next() 函數,就執行第二個中間件,就會打印 33333, 而後又碰到 await next()後,就會跳轉到下一個中間件,所以會打印 55555, 而後再碰到 awaitnext() 方法後,因爲下面沒有中間件了,所以先會打印 666666, 而後依次逆序返回上面未執行完的代碼邏輯,而後咱們就會打印44444,再依次就會打印 22222 了。node

它的結構網上都叫洋蔥結構,當初爲何要這樣設計呢?由於是爲了解決複雜應用中頻繁的回調而設計的級聯代碼,它並不會把控制權徹底交給一箇中間件的代碼,而是碰到next就會去下一個中間件,等下面全部中間件執行完成後,就會再回來執行中間件未完成的代碼,咱們上面說過koa是由一系列generator函數對象的,若是咱們不使用koa的async語法的話,咱們能夠再來看下使用generator函數來實現以下:git

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

app.use(function *(next) {
  // 第一步進入
  const start = new Date;
  console.log('我是第一步');
  yield next;

  // 這是第五步進入的
  const ms = new Date - start;
  console.log(ms + 'ms');
});

app.use(function *(next) {
  // 這是第二步進入的
  const start = new Date;
  console.log('我是第二步');
  yield next;
  // 這是第四步進入的
  const ms = new Date - start;
  console.log('我是第四步' + ms);
  console.log(this.url);
});

// response
app.use(function *() {
  console.log('我是第三步');
  this.body = 'hello world';
});
app.listen(3001);
console.log('app started at port 3000...');

執行的結果以下所示:github

koa-compose 源碼分析web

源碼代碼以下:api

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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!')
  }

  /**
   * @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
      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)
      }
    }
  }
}

koa部分源碼以下:數組

module.exports = class Application extends Emitter {
  /**
   * Initialize a new `Application`.
   *
   * @api public
   */

  constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
  /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */
  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);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */
  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;
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

  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);
  }

  /**
   * Initialize a new context.
   *
   * @api private
   */

  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.state = {};
    return context;
  }

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

如上就是koa部分主要代碼,和 koa-compose 源碼;首先先看 koa的源碼中 use 方法,use方法的做用是把全部的方法存入到一個全局數組middleware裏面去,而後返回 this,目的使函數能鏈式調用。咱們以前作的demo以下這樣的:promise

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

app.use(function *(next) {
  // 第一步進入
  const start = new Date;
  console.log('我是第一步');
  yield next;

  // 這是第五步進入的
  const ms = new Date - start;
  console.log(ms + 'ms');
});

app.use(function *(next) {
  // 這是第二步進入的
  const start = new Date;
  console.log('我是第二步');
  yield next;
  // 這是第四步進入的
  const ms = new Date - start;
  console.log('我是第四步' + ms);
  console.log(this.url);
});

// response
app.use(function *() {
  console.log('我是第三步');
  this.body = 'hello world';
});
app.listen(3001);
console.log('app started at port 3000...');

咱們來理解下,咱們會把 app.use(function *(){}) 這樣的函數會調用use方法,而後use函數內部會進行判斷是否是函數,若是不是函數會報錯,若是是函數的話,就轉換成 async 這樣的函數,而後纔會依次存入 middleware這個全局數組裏面去,存入之後,咱們須要怎麼調用呢?咱們下面會 使用 app.listen(3001), 這樣啓動一個服務器,而後咱們就會調用 koa中的listen這個方法,端口號是3001,listen方法上面有代碼,咱們複製下來一步步來理解下;以下基本代碼:瀏覽器

listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

如上代碼,它是經過node基本語法建立一個服務器,const server = http.createServer(this.callback()); 這句代碼就會執行 callback這個方法,來調用,可能看這個方法,咱們很差理解,這個方法和下面的基本方法是相似的;以下node基本代碼:服務器

var http = require("http")
http.createServer(function(req,res){
  res.writeHead(200,{'Content-Type':'text/html'});
  res.write("holloe  world")    
  res.end("fdsa");
}).listen(8000);

如上代碼我是建立一個8000服務器,當咱們訪問 http://localhost:8000/ 的時候,咱們會調用 http.createServer 中的function函數代碼,而後會打印數據,所以該方法是自動執行的。所以上面的listen方法也是這個道理的,會自動調用callback()方法內部代碼執行的,所以koa中的callback代碼以下:

/**
 * Return a request handler callback
 * for node's native http server.
 *
 * @return {Function}
 * @api public
 */
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;
}

而後會調用 const fn = compose(this.middleware); 這句代碼,該compose 代碼會返回一個函數,compose函數代碼(也就是koa-compose源碼)以下:

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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!')
  }

  /**
   * @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
      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 函數代碼 傳入一個數組的中間件 middleware, 首先判斷是否是數組,而後判斷是否是函數,該參數 middleware 就是咱們把use裏面的全部函數存入該數組中的。該函數會返回一個函數。

而後繼續往下執行 callback中的代碼,以下代碼執行:

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

return handleRequest;

首先會建立一個上下文對象 ctx,具體怎麼建立能夠看 koa源碼中的 createContext 這個方法,而後會調用 koa中的handleRequest(ctx, fn)這個方法, 該方法傳遞二個參數,第一個是ctx,指上下文對象,第二個是 compose 函數中返回的函數,koa中的 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);
}

最後一句代碼 fnMiddleware(ctx).then(handleResponse).catch(onerror); 中的 fnMiddleware(ctx) 就會調用koa-compose 中返回的函數的代碼,compose 函數返回的代碼以下函數:

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)
    }
  }
}

依次執行 app.use(function(req, res) {}) 中內這樣的函數,會執行如上 dispatch方法,從0開始,也就是說從第一個函數開始執行,而後就會執行完成後,會返回一個promise對象,Promise.resolve(fn(context, dispatch.bind(null, i + 1))); dispatch.bind(null, i + 1)) 該函數的做用是循環調用dispatch方法,返回promise對象後,執行then方法就會把值返回回來,所以執行全部的 app.use(function(req, res) {}); 裏面這樣的function方法,dispatch(i + 1) 就是將數組指針移向下一個,執行下一個中間件的代碼,而後一直這樣到最後一箇中間件,這就是一直use,而後next方法執行到最後的基本原理,可是咱們從上面知道,咱們執行完全部的use方法後,並無像洋蔥的結構那樣?那怎麼回去的呢?其實回去的代碼其實就是函數壓棧和出棧,好比咱們能夠看以下代碼就能夠理解其函數的壓棧和出棧的基本原理了。

以下函數代碼:

function test1() {
  console.log(1)
  test2();
  console.log(5)
  return Promise.resolve();
}
function test2() {
  console.log(2)
  test3();
  console.log(4)
}

function test3() {
  console.log(3)
  return;
}
test1();

打印的順序分別爲 1, 2, 3, 4, 5;

如上代碼就是koa的執行分析的基本原理了。

相關文章
相關標籤/搜索