看到標題,也許您會覺得奇怪,redux跟Koa以及Express并不是同一類別的框架,干嘛要拿來做類比。盡管,例如Express以及koa等這類的middleware是指可以被嵌入在框架接收請求到產生響應過程之中的代碼。而redux的middleware是提供的是位于 action 被發起之后,到達 reducer 之前的擴展點。我覺得,不管是什么框架,一種思想才是最重要的。相同的概念,可能實現的思路與方法不同,同樣值得我們去深究,去學習。
面對多樣的業務場景,前后端都需要一種插件機制,可以隨意組合。middleware即是這種可以自由組合,自由插拔的插件機制。所以我們就來橫向比較一下middleware在redux,koa以及express的運行機制,本文主要通過以下三點來進行比較:
- 異步編程模式
- middleware的使用
- 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的中間件是所謂的洋蔥型模型。
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機制。
- 函數式編程思想
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
- 分發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。
- 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就依次執行了。
- 在middleware中調用dispatch
從上圖中得出結論,middleware通過next(action)一層層處理和傳遞action直到redux原生的dispatch。而如果某個middleware使用store.dispatch(action)來分發action,就相當于重新來一遍。
在middleware中使用dispatch的場景一般是接受一個定向action,這個action并不希望到達原生的分發action,往往用在一步請求的需求里,比如上面提到的redux-thunk,就是直接接受dispatch。
下面我們來總結一下三者的區別
express
- 中間件為一個方法,接受 req,res,next三個參數。
- 中間可以執行任何方法包括異步方法。
- 最后一定要通過res.end或者next來通知結束這個中間件方法。
- 如果沒有執行res.end或者next訪問會一直卡著不動直到超時。
- 并且在這之后的中間件也會沒法執行到。
koa
- 中間件為一個方法或者其它,接受ctx,next兩個參數。
- 方法中可以執行任何同步方法??梢允褂梅祷匾粋€Promise來做異步。
- 中間件通過方法結束時的返回來判斷是否進入下一個中間件。
- 返回一個Promise對象koa會等待異步通知完成。then中可以返回next()來跳轉到下一個中間件。
- 如果Promise沒有異步通知也會卡住。
Redux
- 中間件為一個方法,接受store參數。
- 中間可以執行任何方法包括異步方法。
- 中間件通過組合串聯middlware。
- 通過next(action)處理和傳遞action直到redux原生的dispatch,或者使用store.dispatch(actio)來分發action。
- 如果一只簡單粗暴調用store.dispatch(action),就會形成無限循環。
參考文檔
- 你知道 koa 中間件執行原理嗎?
- redux gitbook
- redux-thunk of stackoverflow
- Koa2原理詳解
- 《深入React技術?!?/li>