中間件(middleware)

介紹

在咱們平常開發中,愈來愈多看到了中間件這個詞,例如Koa,redux等。這裏就大概記錄一下Koa和redux中間件的實現方式,能夠從中看到中間件的實現方式都是大同小異,基本都是實現了洋蔥模型。前端

對於中間件咱們須要瞭解的是git

  • 中間件是如何存儲的
  • 中間件是如何執行的

正文

Koa

做爲TJ大神的做品,真不愧是號稱基於 Node.js 平臺的下一代 web 開發框架,其中對於中間件的實現,generator/yield,仍是await/async,對於回調地獄的處理,都是給後來的開發者很大的影響。github

Koa 1的中間件

存儲
/**
 * https://github.com/Koajs/Koa/blob/1.6.0/lib/application.js
 */

 ...
 var app = Application.prototype;

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

...

app.use = function(fn){
  if (!this.experimental) {
    // es7 async functions are not allowed,
    // so we have to make sure that `fn` is a generator function
    assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
  }
  debug('use %s', fn._name || fn.name || '-');
  this.middleware.push(fn);
  return this;
};
複製代碼

能夠在這裏看到咱們經過app.use加入的中間件,保存在一個middleware的數組中。web

執行
/**
 * https://github.com/Koajs/Koa/blob/1.6.0/lib/application.js
 */
app.listen = function(){
  debug('listen');
  var server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
};

// 刪除了一些警告代碼
app.callback = function(){
  ...
  var fn = this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));
  var self = this;
  ...
  return function handleRequest(req, res){
    var ctx = self.createContext(req, res);
    self.handleRequest(ctx, fn);
  }
};

app.handleRequest = function(ctx, fnMiddleware){
  ctx.res.statusCode = 404;
  onFinished(ctx.res, ctx.onerror);
  fnMiddleware.call(ctx).then(function handleResponse() {
    respond.call(ctx);
  }).catch(ctx.onerror);
};
複製代碼

能夠在這裏看到middleware數組通過一些處理,生成了fn,而後經過fnMiddleware.call(ctx)傳入ctx來處理,而後就將ctx傳給了respond,因此這裏的fnMiddleware就是咱們須要去了解的內容。json

這裏首先判斷是不是this.experimental來獲取是否使用了async/await,這個咱們在Koa1中不作詳細介紹。咱們主要是來看一下co.wrap(compose(this.middleware))redux

讓咱們先來看一下compose()api

/**
 * 這裏使用了Koa1@1.6.0 package.json中的Koa-compose的版本
 * https://github.com/Koajs/compose/blob/2.3.0/index.js
 */
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(){}
複製代碼

co.wrap(compose(this.middleware))就變成了以下的樣子數組

co.wrap(function *(next){
    if (!next) next = noop();

    var i = middleware.length;

    while (i--) {
      next = middleware[i].call(this, next);
    }

    return yield *next;
})
複製代碼

咱們能夠看到這裏對middleware進行了倒序遍歷。next = middleware[i].call(this, next);能夠寫爲相似下面這個代碼結構bash

function *middleware1() {
  ...
  yield function *next1() {
    ...
	yield function *next2() {
	  ...
	  ...
	  ...
	}
	...
  }
  ...
}
複製代碼

而後next = middleware[i].call(this, next);其實每個next就是一個middleware,因此也就能夠變成閉包

function *middleware1() {
  ...
  yield function *middleware2() {
    ...
    yield function *middleware() {
      ...
	  ...
	  ...
	}
	...
  }
  ...
}
複製代碼

而後咱們就得到了下面這個代碼

co.wrap(function *(next){
  next = function *middleware1() {
    ...
    yield function *middleware2() {
      ...
      yield (function *middleware3() {
        ...
        yield function *() {
          // noop
          // NO next yield !
        }
        ...
      }
      ...
    }
    ...
  }
  return yield *next;
})
複製代碼

至此咱們來看一眼洋蔥模型, 是否是和咱們上面的代碼結構很想。

如今咱們有了洋蔥模型式的中間節代碼,接下來就是執行它。接下來就是co.wrap,這裏咱們就不詳細說明了,co框架就是一個經過Promise來讓generator自執行的框架,實現了相似async/await的功能(其實應該說async/await的實現方式就是Promisegenerator)。

這裏提一個最後yield *next,是讓code能夠少執行一些,由於若是使用yield next,會返回一個迭代器,而後co來執行這個迭代器,而yield *則是至關於將generator裏面的內容寫在當前函數中,詳細能夠見yield*

關於Koa1能夠看個人早一些寫的另外一篇Koa中間件(middleware)實現探索

Koa 2的中間件

存儲
/**
 * https://github.com/Koajs/Koa/blob/1.6.0/lib/application.js
 */
 ...
 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);
  }

...

  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;
  }
複製代碼

Koa2對於middleware的存儲和Koa1基本如出一轍,保存在一個數組中。

執行
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;
  }

  /**
   * 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);

複製代碼

這裏主要就是兩行代碼

const fn = compose(this.middleware);
// fnMiddleware === fn
fnMiddleware(ctx).then(handleResponse).catch(onerror);
複製代碼

Koa2的代碼彷佛比Koa1要簡介一些了,在默認使用await/async以後,少了co的使用。

fnMiddleware(ctx).then(handleResponse).catch(onerror);咱們能夠知道fnMiddleware返回了一個Promise,而後執行了這個Promise,因此咱們主要知道compose作了什麼就好。

/**
 * https://github.com/Koajs/compose/blob/4.0.0/index.js
 */
function compose (middleware) {
  ...
  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, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
複製代碼

看起來這段代碼比Koa1compose稍微複雜了些,其實差很少,主要的代碼其實也就兩個

function compose (middleware) {
  ...
  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      let fn = middleware[i]
      return Promise.resolve(fn(context, function next () {
        return dispatch(i + 1)
      }))
    }
  }
}
複製代碼

相比於Koa1遍歷middleware數組,Koa2改成了遞歸。同上面同樣,咱們能夠將函數寫爲以下結構

async function middleware1() {
  ...
  await (async function middleware2() {
    ...
    await (async function middleware3() {
      ...
    });
    ...
  });
  ...
}
複製代碼

由於async函數的自執行,因此直接運行該函數就能夠了。

能夠看到Koa1Koa2的中間件的實現方式基本是同樣的,只是一個是基於generator/yield, 一個是基於async/await

Redux

相比於Koa的中間件的具體實現,Redux相對稍複雜一些。

本人對於Redux基本沒有使用,只是寫過一些簡單的demo,看過一部分的源碼,若有錯誤,請指正

存儲

咱們在使用Redux的時候可能會這麼寫

// 好高階的函數啊
const logger = store => next => action => {
  console.group(action.type)
  console.info('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  console.groupEnd(action.type)
  return result
}

let store = createStore(
  todoApp,
  applyMiddleware(
    logger
  )
)
複製代碼

咱們能夠很方便的找到applyMiddleware的源碼。

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
複製代碼

Redux沒有單獨保存middleware的地方,可是經過展開符的...middlewares,咱們也能夠知道至少一開始的middlewares是一個數組的形式。

執行

執行的代碼,仍是上面那段代碼片斷。

咱們能夠看到applyMiddleware()中,對傳入的middlewares作了簡單的封裝,目的是爲了讓每一個middleware在執行的時候能夠拿到當前的一些環境和一些必要的接口函數。也就是上面那個高階函數logger所須要的三個參數store,next,action

一開始是middlewares.map(middleware => middleware(middlewareAPI)),而middlewareAPI傳入了getStatedispatch接口(dispatch接口暫時沒有用)。這一步就實現了上面高階函數logger所須要的參數store

而後是咱們看到好屢次的compose函數,咱們找到compose函數的實現。

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製代碼

咱們看到compose對傳入的中間件函數,經過Array.reduce函數處理了一下。最終的函數應該大概相似下面這個格式

// 加入函數名next方便後面理解
function chain(...args) {
  return () => {
    return a(function next(...args) {
      return b(function next(...args) {
        return c(...args);
      })
    })
  }
}
複製代碼

這裏已經再次出現了咱們熟悉的洋蔥模型。同時將下一個組件已參數(next)的形式傳入當前的中間件,這裏就完成了上面的高階函數logger所須要的第二個參數next,在中間件內部調用next函數就能夠繼續中間節的流程。

最後傳入了store.dispatch也就是高階函數logger所須要的第二個參數action,這個就不用多數了,就是將咱們剛剛獲得的洋蔥格式的函數調用一下,經過閉包使得每一箇中間節均可以拿到store.dispatch

總結

至此,ReduxKoa的中間件的介紹就差很少了,二者都是以數組的形式保存了中間件,執行的時候都是建立了一個相似洋蔥模型的函數結構,也都是將一個包裹下一個中間件的函數當作next,傳入當前中間件,使得當前中間件能夠經過調用next來執行洋蔥模型,同時在next執行的先後均可以寫邏輯代碼。不一樣的是Koa1是經過遍歷生成的,Koa2是經過遞歸來生成的,redux是經過reduce來生成的(和Koa1的遍歷相似)。

因此中間件其實都基本相似,因此好好的理解了一種中間件的實現方式,其餘的學起來就很快了(只是表示前端這一塊哦)。

相關文章
相關標籤/搜索