中介軟體(middleware)

跳一跳發表於2018-05-06

介紹

在我們日常開發中,越來越多看到了中介軟體這個詞,例如Koa,redux等。這裡就大概記錄一下Koa和redux中介軟體的實現方式,可以從中看到中介軟體的實現方式都是大同小異,基本都是實現了洋蔥模型。

對於中介軟體我們需要了解的是

  • 中介軟體是如何儲存的
  • 中介軟體是如何執行的

正文

Koa

作為TJ大神的作品,真不愧是號稱基於 Node.js 平臺的下一代 web 開發框架,其中對於中介軟體的實現,generator/yield,還是await/async,對於回撥地獄的處理,都是給後來的開發者很大的影響。

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的陣列中。

執行
/**
 * 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就是我們需要去了解的內容。

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

讓我們先來看一下compose()

/**
 * 這裡使用了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);可以寫為類似下面這個程式碼結構

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;
})
複製程式碼

至此我們來看一眼洋蔥模型, 是不是和我們上面的程式碼結構很想。

中介軟體(middleware)

現在我們有了洋蔥模型式的中間節程式碼,接下來就是執行它。接下來就是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的遍歷類似)。

所以中介軟體其實都基本類似,所以好好的理解了一種中介軟體的實現方式,其他的學起來就很快了(只是表示前端這一塊哦)。

相關文章