Redux,Koa,Express之middleware機制對比

看到標題,也許您會覺得奇怪,redux跟Koa以及Express并不是同一類別的框架,干嘛要拿來做類比。盡管,例如Express以及koa等這類的middleware是指可以被嵌入在框架接收請求到產生響應過程之中的代碼。而redux的middleware是提供的是位于 action 被發起之后,到達 reducer 之前的擴展點。我覺得,不管是什么框架,一種思想才是最重要的。相同的概念,可能實現的思路與方法不同,同樣值得我們去深究,去學習。

面對多樣的業務場景,前后端都需要一種插件機制,可以隨意組合。middleware即是這種可以自由組合,自由插拔的插件機制。所以我們就來橫向比較一下middleware在redux,koa以及express的運行機制,本文主要通過以下三點來進行比較:

  1. 異步編程模式
  2. middleware的使用
  3. middleware的執行原理及實現

異步編程模式

理解異步編程方式式理解幾種middleware執行原理的前提。

框架 異步方式
Express callback
Koa1 generator/yield+co
Koa2 Async/Await
Redux redux-thunk,redux-saga,redux-promise等
  • express: 由于是在ES6之前出現的,所以中間件的基礎原理還是callback方式
  • koa: koa1得益于generator特性并通過co框架加入了自動流程管理。(co會把所有generator的返回封裝成為Promise對象)koa2則使用了Async/Await的形式(僅僅知識genertator函數的語法糖)
  • redux: 論redux的異步變成,更準確的應該是異步的action的方式。解決異步action的方式有多種,比如:redux-thunk,redux-saga,redux-promise等

本文并不會對js里的異步編程作詳細,推薦阮一峰老師寫的《深入掌握 ECMAScript 6 異步編程》系列文章。

middleware的使用

眾所周知,Koa是Express框架原班人馬基于ES6新特性重新開發的敏捷開發框架。Express主要是基于Connect中間件框架,其自身封裝了大量的功能,比如路由、請求等。而Koa是基于co(koa2基于async/await)中間件框架,框架自身并沒集成太多功能,大部分功能需要用戶自行require中間件去解決。我們首先來比較一下Koa與Express的寫法:

//Express
var express = require('express')
var app = express()

app.get('/',(req,res)=>{
    res.send('Hello Express!')
})
app.listen(3000)

//Koa
var koa = require('koa')
var app = koa()
var route = require('koa-route')

app.use(route.get('/',async (ctx) => {
    ctx.body = 'Hello Koa'
}))

app.listen(3000)

redux使用包含自定義功能的middleware來擴展。Middleware可以讓你包裝store的dispatch方法來達到你想要的目的。同時,middleware還擁有"compose"這一關鍵特性。(這個下文會講)多個middleware可以被組合到一起使用,形成middleware鏈。其中,每個middleware都不需要關心鏈中它前后的middleware 的任何信息。

例如:

const logger = ()=>{
    // ...
}
const crashReporter = ()=>{
    // ...
}
const thunk = () =>{
    // ...
}

let store = createStore(
  App,
  applyMiddleware(
    crashReporter,
    thunk,
    logger
  )
)

middleware的執行原理及實現

express的middleware執行原理及實現

Express更像是中間件順序執行,稱之為線性模型

↓
---------------
| middleware1 |
---------------
       ↓
---------------
| ... ... ... |
---------------
       ↓
---------------
| middlewareN |
---------------
       ↓

其實express middleware的原理很簡單,express內部維護一個函數數組,這個函數數組表示在發出響應之前要執行的所有函數,也就是中間件數組,每一次use以后,傳進來的中間件就會推入到數組中,執行完畢后調用next方法執行函數的下一個函數,如果沒用調用,調用就會終止。
下面我們實現一個簡單的Express中間件功能

function express() {
    var funcs = [] // 中間件存儲的數組
    var app = function (req, res) {
        var i = 0  
        // 定義next()
        function next() {
            var task = funcs[i++]  // 取出中間件數組里的下一個中間件函數
            if (!task) {    // 如果中間件不存在,return
                return
            }
            task(req, res, next);   // 否則,執行下一個中間件
        }
        next()
    }
    // use方法則是將中間件函數推入到中間件數組中
    app.use = function (task) {
        funcs.push(task);
    }
    return app    // 返回實例
}
koa的middleware執行原理及實現

Koa會把多個中間件推入棧中,與express不同,koa的中間件是所謂的洋蔥型模型。


image

koa的中間件的實現主要依靠的是koa-compose。首先我們來看下koa-compose的使用,koa-compose模塊可以將多個中間件合成為一個:

const Koa = require('koa')
const compose = require('koa-compose')
const app = new Koa()

const logger = (ctx, next) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`)
  next()
}

const main = ctx => {
  ctx.response.body = 'Hello Koa'
};

const middlewares = compose([logger, main])

app.use(middlewares)
app.listen(3000)

下面我們來分析一下koa-compose的源碼

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'))
      // 當前執行第 i 個中間件
      index = i
      let fn = middleware[i]
      // 所有的中間件執行完畢
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        // 執行當前的中間件
        // 這里的fn也就是app.use(fn)中的fn
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

Koa的中間件支持普通函數,返回一個Promise的函數,以及async函數。

redux的middleware執行原理及實現

"It provides a third-party extension point between dispatching an action,and the moment it reaches the reducer"
這是redux作者的描述。

正因為redux單一數據源的特點,數據從頂層流動,middleware就好比管道去輔助這些數據的流向,不同的管道具有不同的特點與功能。

每個middleware函數接受Store的dispatch和getState函數作為命名參數,并返回一個函數。該函數會被傳入被稱為next的下一個middleware的 dispatch方法,并返回一個接收action的新函數,這個函數可以直接調用 next(action),或者在其他需要的時刻調用,甚至根本不去調用它。調用鏈中最后一個middleware會接受真實的store的dispatch方法作為next參數,并借此結束調用鏈。

下面以redux-thunk為例,來介紹下如何寫一個redux middleware,下面是redux-thunk的源碼

redux-thunk幫助你統一了異步和同步action的調用方式,把異步過程放在action級別解決

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

一共只有11行,action本身是一個object,帶有type和arguments。上述將dispatch和getState傳入action,next()和action()是redux提供的方法。接著做判斷,如果action是一個function,就返回action(dispatch, getState,extraArgument),否則返回next(action)。

然后將他們引用到Redux Store中

import { createStore, combineReducers, applyMiddleware } from 'redux'

let todoApp = combineReducers(reducers)
let store = createStore(
  todoApp,
  // applyMiddleware() 告訴 createStore() 如何處理中間件
  applyMiddleware(thunk)
)

首先我們從源碼出發

import compose from './compose'

export default function applyMiddleware(...middleware){
    return (next) => (reducer,initialState) => {
        let store = next(reducer, initialState)
        let disptach = store.dispatch
        let chain = []
        
        var middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        } 
        chain = middlewares.map(middlre => middleware(middlewareAPI))
        dispatch = compose(...chain)(store.dispatch)
        
        return {
            ...store,
            dispatch
        }
    }
}

下面我們從以下幾個點介紹redux的middleware機制。

  1. 函數式編程思想
    redux middlreware的思想是使用匿名單參數函數來實現多參數函數的方法。

ES6實現一個curring函數

function curring(fn){
    return function curried(...args){
        return args.length >= fn.length ? fn.call(this,...args):(...rest)=>{
            return curried.call(this,...args,...rest)
        }
    }
}

好處:

  • 易串聯
  • 共享store
  1. 分發store

創建一個普通的store

let newStore = applyMiddleware(mid1,mid2,...)(createStore)(reducer,null)

由于

var middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        } 
chain = middlewares.map(middlre => middleware(middlewareAPI))

因為閉包,每個匿名函數都可以訪問相同的store,即middlewareAPI。

  1. compose
    compose 的源碼就是一個函數 compose :
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }
  // 獲取最后一個函數
    const last = funcs[funcs.length - 1];
    // 獲取除最后一個以外的函數[0,length-1)
    const rest = funcs.slice(0, -1)
   // 通過函數 curry 化
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

這里的compose跟上文中的koa-compose有些類似。屬于函數式編程中的組合,它將chain中的所有匿名函數[f1,f2,f3,...,fn]組成一個新的函數,即新的dispatch,假設n = 3:
dispatch = f1(f2(f3(store.dispatch)))
這時調用新dispatch,每一個middleware就依次執行了。

  1. 在middleware中調用dispatch
image

從上圖中得出結論,middleware通過next(action)一層層處理和傳遞action直到redux原生的dispatch。而如果某個middleware使用store.dispatch(action)來分發action,就相當于重新來一遍。

在middleware中使用dispatch的場景一般是接受一個定向action,這個action并不希望到達原生的分發action,往往用在一步請求的需求里,比如上面提到的redux-thunk,就是直接接受dispatch。

下面我們來總結一下三者的區別

express

  1. 中間件為一個方法,接受 req,res,next三個參數。
  2. 中間可以執行任何方法包括異步方法。
  3. 最后一定要通過res.end或者next來通知結束這個中間件方法。
  4. 如果沒有執行res.end或者next訪問會一直卡著不動直到超時。
  5. 并且在這之后的中間件也會沒法執行到。

koa

  1. 中間件為一個方法或者其它,接受ctx,next兩個參數。
  2. 方法中可以執行任何同步方法??梢允褂梅祷匾粋€Promise來做異步。
  3. 中間件通過方法結束時的返回來判斷是否進入下一個中間件。
  4. 返回一個Promise對象koa會等待異步通知完成。then中可以返回next()來跳轉到下一個中間件。
  5. 如果Promise沒有異步通知也會卡住。

Redux

  1. 中間件為一個方法,接受store參數。
  2. 中間可以執行任何方法包括異步方法。
  3. 中間件通過組合串聯middlware。
  4. 通過next(action)處理和傳遞action直到redux原生的dispatch,或者使用store.dispatch(actio)來分發action。
  5. 如果一只簡單粗暴調用store.dispatch(action),就會形成無限循環。

參考文檔


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Redux這個npm包,提供若干API讓我們使用reducer創建store,并能更新store中的數據或獲取st...
    不安分的三好份子閱讀 959評論 0 0
  • 為什么dispatch需要middleware 上圖表達的是 redux 中一個簡單的同步數據流動的場景,點擊 b...
    一個胖子的我閱讀 2,007評論 1 9
  • 學習必備要點: 首先弄明白,Redux在使用React開發應用時,起到什么作用——狀態集中管理 弄清楚Redux是...
    賀賀v5閱讀 8,943評論 10 58
  • 一、什么情況需要redux? 1、用戶的使用方式復雜 2、不同身份的用戶有不同的使用方式(比如普通用戶和管...
    初晨的筆記閱讀 2,053評論 0 11
  • http://gaearon.github.io/redux/index.html ,文檔在 http://rac...
    jacobbubu閱讀 80,057評論 35 198