redux, koa, express 中間件實現對比解析

若是你有 express ,koa, redux 的使用經驗,就會發現他們都有 中間件(middlewares)的概念,中間件 是一種攔截器的思想,用於在某個特定的輸入輸出之間添加一些額外處理,同時不影響原有操做。html

最開始接觸 中間件是在服務端使用 expresskoa 的時候,後來從服務端延伸到前端,看到其在redux的設計中也獲得的極大的發揮。中間件的設計思想也爲許多框架帶來了靈活而強大的擴展性。前端

本文主要對比redux, koa, express 的中間件實現,爲了更直觀,我會抽取出三者中間件相關的核心代碼,精簡化,寫出模擬示例。示例會保持 express, koaredux 的總體結構,儘可能保持和源碼一致,因此本文也會稍帶講解下express, koa, redux 的總體結構和關鍵實現:node

示例源碼地址, 能夠一邊看源碼,一邊讀文章,歡迎star!git

本文適合對express ,koa ,redux 都有必定了解和使用經驗的開發者閱讀github

服務端的中間件

expresskoa 的中間件是用於處理 http 請求和響應的,可是兩者的設計思路確不盡相同。大部分人瞭解的expresskoa的中間件差別在於:web

  • express採用「尾遞歸」方式,中間件一個接一個的順序執行, 習慣於將response響應寫在最後一箇中間件中;
  • koa的中間件支持 generator, 執行順序是「洋蔥圈」模型。

所謂的「洋蔥圈」模型:express

不過實際上,express 的中間件也能夠造成「洋蔥圈」模型,在 next 調用後寫的代碼一樣會執行到,不過express中通常不會這麼作,由於 expressresponse通常在最後一箇中間件,那麼其它中間件 next() 後的代碼已經影響不到最終響應結果了;編程

express

首先看一下 express 的實現:redux

入口

// express.js

var proto = require('./application');
var mixin = require('merge-descriptors');

exports = module.exports = createApplication;

function createApplication() {

 
  // app 同時是一個方法,做爲http.createServer的處理函數
  var app = function(req, res, next) { 
      app.handle(req, res, next)
  }
  
  mixin(app, proto, false);
  return app
}

複製代碼

這裏其實很簡單,就是一個 createApplication 方法用於建立 express 實例,要注意返回值 app 既是實例對象,上面掛載了不少方法,同時它自己也是一個方法,做爲 http.createServer的處理函數, 具體代碼在 application.js 中:api

// application.js

var http = require('http');
var flatten = require('array-flatten');
var app = exports = module.exports = {}

app.listen = function listen() {
  var server = http.createServer(this)
  return server.listen.apply(server, arguments)
}

複製代碼

這裏 app.listen 調用 nodejshttp.createServer 建立web服務,能夠看到這裏 var server = http.createServer(this) 其中 thisapp 自己, 而後真正的處理程序即 app.handle;

中間件處理

express 本質上就是一箇中間件管理器,當進入到 app.handle 的時候就是對中間件進行執行的時候,因此,最關鍵的兩個函數就是:

全局維護一個stack數組用來存儲全部中間件,app.use 的實現就很簡單了,能夠就是一行代碼 ``

// app.use
app.use = function(fn) {
	this.stack.push(fn)
}

複製代碼

express 的真正實現固然不會這麼簡單,它內置實現了路由功能,其中有 router, route, layer 三個關鍵的類,有了 router 就要對 path 進行分流,stack 中保存的是 layer實例,app.use 方法實際調用的是 router 實例的 use 方法, 有興趣的能夠自行去閱讀。

app.handle 即對 stack 數組進行處理

app.handle = function(req, res, callback) {

	var stack = this.stack;
	var idx = 0;
	function next(err) {
		if (idx >= stack.length) {
		  callback('err') 
		  return;
		}
		var mid;
		while(idx < stack.length) {
		  mid = stack[idx++];
		  mid(req, res, next);
		}
	}
	next()
}

複製代碼

這裏就是所謂的"尾遞歸調用",next 方法不斷的取出stack中的「中間件」函數進行調用,同時把next 自己傳遞給「中間件」做爲第三個參數,每一箇中間件約定的固定形式爲 (req, res, next) => {}, 這樣每一個「中間件「函數中只要調用 next 方法便可傳遞調用下一個中間件。

之因此說是」尾遞歸「是由於遞歸函數的最後一條語句是調用函數自己,因此每個中間件的最後一條語句須要是next()才能造成」尾遞歸「,不然就是普通遞歸,」尾遞歸「相對於普通」遞歸「的好處在於節省內存空間,不會造成深度嵌套的函數調用棧。有興趣的能夠閱讀下阮老師的尾調用優化

至此,express 的中間件實現就完成了。

koa

不得不說,相比較 express 而言,koa 的總體設計和代碼實現顯得更高級,更精煉;代碼基於ES6 實現,支持generator(async await), 沒有內置的路由實現和任何內置中間件,context 的設計也非常巧妙。

總體

一共只有4個文件:

  • application.js 入口文件,koa應用實例的類
  • context.js ctx 實例,代理了不少requestresponse的屬性和方法,做爲全局對象傳遞
  • request.js koa 對原生 req 對象的封裝
  • response.js koa 對原生 res 對象的封裝

request.jsresponse.js 沒什麼可說的,任何 web 框架都會提供reqres 的封裝來簡化處理。因此主要看一下 context.jsapplication.js的實現

// context.js 

/**
 * Response delegation.
 */

delegate(proto, 'res')
  .method('setHeader')

/**
 * Request delegation.
 */

delegate(proto, 'req')
  .access('url')
  .setter('href')
  .getter('ip');

複製代碼

context 就是這類代碼,主要功能就是在作代理,使用了 delegate 庫。

簡單說一下這裏代理的含義,好比delegate(proto, 'res').method('setHeader') 這條語句的做用就是:當調用proto.setHeader時,會調用proto.res.setHeader 即,將protosetHeader方法代理到protores屬性上,其它相似。

// application.js 中部分代碼

constructor() {
	super()
	this.middleware = []
	this.context = Object.create(context)
}

use(fn) {
	this.middleware.push(fn)
}

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

callback() {
	// 這裏即中間件處理代碼
	const fn = compose(this.middleware);
	
	const handleRequest = (req, res) => {
	  // ctx 是koa的精髓之一, req, res上的不少方法代理到了ctx上, 基於 ctx 不少問題處理更加方便
	  const ctx = this.createContext(req, res);
	  return this.handleRequest(ctx, fn);
	};
	
	return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
	ctx.statusCode = 404;
	const onerror = err => ctx.onerror(err);
	const handleResponse = () => respond(ctx);
	return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
   
複製代碼

一樣的在listen方法中建立 web 服務, 沒有使用 express 那麼繞的方式,const server = http.createServer(this.callback());this.callback()生成 web 服務的處理程序

callback 函數返回handleRequest, 因此真正的處理程序是this.handleRequest(ctx, fn)

中間件處理

構造函數 constructor 中維護全局中間件數組 this.middleware和全局的this.context 實例(源碼中還有request,response對象和一些其餘輔助屬性)。和 express 不一樣,由於沒有router的實現,全部this.middleware 中就是普通的」中間件「函數而非複雜的 layer 實例,

this.handleRequest(ctx, fn);ctx 爲第一個參數,fn = compose(this.middleware) 做爲第二個參數, handleRequest 會調用 fnMiddleware(ctx).then(handleResponse).catch(onerror); 因此中間處理的關鍵在compose方法, 它是一個獨立的包koa-compose, 把它拿了出來看一下里面的內容:

// compose.js

'use strict'

module.exports = compose

function compose (middleware) {

  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      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)
      }
    }
  }
}


複製代碼

express中的next 是否是很像,只不過他是promise形式的,由於要支持異步,因此理解起來就稍微麻煩點:每一個中間件是一個async (ctx, next) => {}, 執行後返回的是一個promise, 第二個參數 next的值爲 dispatch.bind(null, i + 1) , 用於傳遞」中間件「的執行,一個個中間件向裏執行,直到最後一箇中間件執行完,resolve 掉,它前一個」中間件「接着執行 await next() 後的代碼,而後 resolve 掉,在不斷向前直到第一個」中間件「 resolve掉,最終使得最外層的promise resolve掉。

這裏和express很不一樣的一點就是koa的響應的處理並不在"中間件"中,而是在中間件執行完返回的promise resolve後:

return fnMiddleware(ctx).then(handleResponse).catch(onerror);

經過 handleResponse 最後對響應作處理,」中間件「會設置ctx.body, handleResponse也會主要處理 ctx.body ,因此 koa 的」洋蔥圈「模型纔會成立,await next()後的代碼也會影響到最後的響應。

至此,koa的中間件實現就完成了。

redux

不得不說,redux 的設計思想和源碼實現真的是漂亮,總體代碼量很少,網上已經隨處可見redux的源碼解析,我就不細說了。不過仍是要推薦一波官網對中間件部分的敘述 : redux-middleware

這是我讀過的最好的說明文檔,沒有之一,它清晰的說明了 redux middleware 的演化過程,漂亮地演繹了一場從分析問題解決問題,並不斷優化的思惟過程。

整體

本文仍是主要看一下它的中間件實現, 先簡單說一下 redux 的核心處理邏輯, createStore 是其入口程序,工廠方法,返回一個 store 實例,store實例的最關鍵的方法就是 dispatch , 而 dispatch 要作的就是一件事:

currentState = currentReducer(currentState, action)

即調用reducer, 傳入當前stateaction返回新的state

因此要模擬基本的 redux 執行只要實現 createStore , dispatch 方法便可。其它的內容如 bindActionCreators, combineReducers 以及 subscribe 監聽都是輔助使用的功能,能夠暫時不關注。

中間件處理

而後就到了核心的」中間件" 實現部分即 applyMiddleware.js

// applyMiddleware.js

import compose from './compose'

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 中間件提供的擴展是在 action 發起以後,到達 reducer 以前,它的實現思路就和expresskoa 有些不一樣了,它沒有經過封裝 store.dispatch, 在它前面添加 中間件處理程序,而是經過遞歸覆寫 dispatch ,不斷的傳遞上一個覆寫的 dispatch 來實現。

每個 redux 中間件的形式爲 store => next => action => { xxx }

這裏主要有兩層函數嵌套:

  • 最外層函數接收參數store, 對應於 applyMiddleware.js 中的處理代碼是 const chain = middlewares.map(middleware => middleware(middlewareAPI)), middlewareAPI 即爲傳入的store 。這一層是爲了把 storeapi 傳遞給中間件使用,主要就是兩個api:

    1. getState, 直接傳遞store.getState.
    2. dispatch: (...args) => dispatch(...args)這裏的實現就很巧妙了,並非store.dispatch, 而是一個外部的變量dispatch, 這個變量最終指向的是覆寫後的dispatch, 這樣作的緣由在於,對於 redux-thunk 這樣的異步中間件,內部調用store.dispatch 的時候仍而後走一遍全部「中間件」
  • 返回的chain就是第二層的數組,數組的每一個元素都是這樣一個函數next => action => { xxx }, 這個函數能夠理解爲 接受一個dispatch返回一個dispatch, 接受的dispatch 是後一箇中間件返回的dispatch.

  • 還有一個關鍵函數即 compose, 主要做用是 compose(f, g, h) 返回 () => f(g(h(..args)))

如今在來理解 dispatch = compose(...chain)(store.dispatch) 就相對容易了,原生的 store.dispatch 傳入最後一個「中間件」,返回一個新的 dispatch , 再向外傳遞到前一箇中間件,直至返回最終的 dispatch, 當覆寫後的dispatch 調用時,每一個「中間件「的執行又是從外向內的」洋蔥圈「模型。

至此,redux中間件就完成了。

其它關鍵點

redux 中間件的實現中還有一點實現也值得學習,爲了讓」中間件「只能應用一次,applyMiddleware 並非做用在 store 實例上,而是做用在 createStore 工廠方法上。怎麼理解呢?若是applyMiddleware 是這樣的

(store, middlewares) => {}

那麼當屢次調用 applyMiddleware(store, middlewares) 的時候會給同一個實例重複添加一樣的中間件。因此 applyMiddleware 的形式是

(...middlewares) => (createStore) => createStore,

這樣,每一次應用中間件時都是建立一個新的實例,避免了中間件重複應用問題。

這種形式會接收 middlewares 返回一個 createStore 的高階方法,這個方法通常被稱爲 createStoreenhance 方法,內部即增長了對中間件的應用,你會發現這個方法和中間件第二層 (dispatch) => dispatch 的形式一致,因此它也能夠用於compose 進行屢次加強。同時createStore 也有第三個參數enhance 用於內部判斷,自加強。因此 redux 的中間件使用能夠有兩種寫法:

第一種:用 applyMiddleware 返回 enhance 加強 createStore

store = applyMiddleware(middleware1, middleware2)(createStore)(reducer, initState)

複製代碼
第二種: createStore 接收一個 enhancer 參數用於自加強

store = createStore(reducer, initState, applyMiddleware(middleware1, middleware2))

複製代碼

第二種使用會顯得直觀點,可讀性更好。

縱觀 redux 的實現,函數式編程體現的淋漓盡致,中間件形式 store => next => action => { xx } 是函數柯里化做用的靈活體現,將多參數化爲單參數,能夠用於提早固定 store 參數,獲得形式更加明確的 dispatch => dispatch,使得 compose得以發揮做用。

總結

整體而言,expresskoa 的實現很相似,都是next 方法傳遞進行遞歸調用,只不過 koapromise 形式。redux 相較前二者有些許不一樣,先經過遞歸向外覆寫,造成執行時遞歸向裏調用。

總結一下三者關鍵異同點(不只限於中間件):

  1. 實例建立: express 使用工廠方法, koa是類
  2. koa 實現的語法更高級,使用ES6,支持generator(async await)
  3. koa 沒有內置router, 增長了 ctx 全局對象,總體代碼更簡潔,使用更方便。
  4. koa 中間件的遞歸爲 promise形式,express 使用while 循環加 next 尾遞歸
  5. 我更喜歡 redux 的實現,柯里化中間件形式,更簡潔靈活,函數式編程體現的更明顯
  6. reduxdispatch 覆寫的方式進行中間件加強

最後再次附上 模擬示例源碼 以供學習參考,喜歡的歡迎star, fork!

回答一個問題

有人說,express 中也能夠用 async function 做爲中間件用於異步處理? 實際上是不能夠的,由於 express 的中間件執行是同步的 while 循環,當中間件中同時包含 普通函數async 函數 時,執行順序會打亂,先看這樣一個例子:

function a() {
  console.log('a')
}

async function b() {
  console.log('b')
  await 1
  console.log('c')
  await 2
  console.log('d')
}

function f() {
	a()
	b()
	console.log('f')
}

複製代碼

這裏的輸出是 'a' > 'b' > 'f' > 'c'

在普通函數中直接調用async函數, async 函數會同步執行到第一個 await 後的代碼,而後就當即返回一個promise, 等到內部全部 await 的異步完成,整個async函數執行完,promise 纔會resolve掉.

因此,經過上述分析 express中間件實現, 若是用async函數作中間件,內部用await作異步處理,那麼後面的中間件會先執行,等到 await 後再次調用 next 索引就會超出!,你們能夠本身在這裏 express async 打開註釋,本身嘗試一下。

相關文章
相關標籤/搜索