React 沒有中間件還能用嗎?

原文可以參考:

從源碼看redux中間件 | villianHR?

www.villainhr.com圖標

或者,關注公眾號,《前端小吉米》


redux middleware 是 redux 的一個 advanced feature. 這個概念並不是很新奇,以為在 Koa 裡面早已經實現過了. 對比與原生的redux middleware , koa 的 middleware 差不多相當於是爸爸級的 level 了. 這麼說,是有依據的. 我們這裡,來深入一下源碼,具體看一下redux middleware 到底做了些啥.

我們首先來探討一下基本的源碼吧.

redux 的中間件具體流程為:

redux 的源碼可以說是比較簡單的。 首先,入口文件是 index.js 。我們來看一下,裡面具體的內容:

// 從不同的文件裡面導出相關方法export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose}

這裡,其實是 redux 的提供的所有方法了. createStore,combineReducers,bindActionCreators 這三個方法,與 middleware 關係不大,我這裡就不贅述了. 這裡,主要講解一下 applyMiddleware 方法和 compose 方法.

in fact, compose 是一個非常基礎的方法, 用來以函數式的編程來組合中間件, 在 koa 中我們也同樣遇見過這樣的寫法. applyMiddleware 也是用到這樣的方法的. so, 我們來具體看看.

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

第一眼看的時候, 肯定超級S B。。。 md... 這寫個啥... 看了一下官方給的注釋就是:

// 簡單 demo 就是compose(f, g, h) is identical to doing (...args) => f(g(h(...args))).

合著就是個函數嵌套的寫法。。。

關鍵坑爹的在於他的reduceRight方法, 寫的真心6. 由於,return 兩個函數時,只會返回第二個執行的結果:

function test(a,b){ return a(),b();}console.log(test(a=>1,b=>2));// 開玩笑的. 上面那種只是科普一下. 他真正的機制實際上是利用 reduceRight 的第二個參數來執行的Array.reduceRight(fn,start); // 主要還是這裡的start, 相當於就是 last(...args)// 將上面代碼翻譯一下就是rest.reduceRight(function(composed, f){return f(composed)}, last(...args));//... 慢慢去體會吧...

所以, 一開始看的時候,在糾結 最後一個 composed 都沒執行... 後來發現, 原來還有一層 last(...args).

不過實話說, 真心沒有 koa 裡面的 compose 函數寫得好, 你直接先寫一個 noop 函數不行嗎!!!

// 俺 實際寫了一個替換的compose. 感覺這個看的清楚一點function compose(...funcs) { return function(...args) { var len = funcs.length, middle = funcs[--len](...args); while (len--) { middle = funcs[len].call(this, middle); } return middle; }}// 測試console.log(compose(a => a, b => b, (c,b) => c+b)(a, b));

這個是簡單的compose 函數. 下面,我們來看一下重點,關於 redux-middleware 的核心方法, applyMiddleware.

applyMiddleware 中間件

由於這個中間件有點複雜, 對傳入的函數有具體的要求. 我們先來看一下使用該方法的上下文。

offical website 找到一個 demo:

let store = createStore( todoApp, // applyMiddleware() tells createStore() how to handle middleware applyMiddleware(logger, crashReporter))

最終 applyMiddleware return 的結果,還需要返回到 createStore 里去的. 通過 createStore 傳入方法時, 函數裡面並未對 裡面做什麼處理.

function createStore(reducer, preloadedState, enhancer) { // 這裡就是一些列條件判斷, 如果你使用 middle 是上面的形式,那麼就會直接將參數賦給 enhancer if (typeof preloadedState === function && typeof enhancer === undefined) { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== undefined) { if (typeof enhancer !== function) { throw new Error(Expected the enhancer to be a function.) } // 實際上調用 applyMiddleware 方法的地方. 注意他傳入的參數即可. z return enhancer(createStore)(reducer, preloadedState) }}

ok, 我們了解了傳入 applyMiddleware 的參數後, 繼續看. 中間件的寫法:

// 這裡就看一下logger 就Okconst logger = store => next => action => { // debug info console.group(action.type) console.info(dispatching, action) let result = next(action) // debug info console.log(next state, store.getState()) console.groupEnd(action.type) return result}// 我們將 debug 信息去掉之後,可以得到一個精簡版的 middlewareconst logger = store => next => action => { // 傳遞前, 執行的代碼 let result = next(action) // 傳遞完, 執行的代碼 return result}

看到這裡,有種 koa 的感覺. next(action) 顯示的將控制權交給下一個函數進行執行. 相當於就是 onion model.

這裡, 放一下 applyMiddleware 的源碼:

export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { var store = createStore(reducer, preloadedState, enhancer) var dispatch = store.dispatch var chain = [] var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } }}

看到這裡, 我也是有點驚訝, 第一是他的中間件傳遞的複雜度巧妙的轉化為箭頭函數的寫法, 第二是他顯示的改變了真正的dispatch的內容。

最後實際調用整個流程,是直接根據applyMiddleware提供的方法來的:

// 注意這裡是 applyMiddleware 提供的 dispatch 方法store.dispatch(action)

如果按照上面的調用方式寫的話,具體調用順序就是:

applyMiddleware(logger, crashReporter)

其中,中間件最常用的就是用來檢查登錄狀態的信息。

登錄狀態的檢查

有時候,需要對指定的介面進行登錄態的判斷。我們可以看到中間件的寫法:

const logger = store => next => action => { // 傳遞前, 執行的代碼 let result = next(action) // 傳遞完, 執行的代碼 return result}

在需要登錄的 action 裡面手動再帶上一個參數。比如:

# 通過額外傳遞一個 check 屬性值,我們就可以實現登錄態的檢查store.dispatch({ type: "LOGINED-FETCH", check: true})# 中間件登錄態檢查const Logined = store => next => action =>{ let user = _.getCookie("skey"); if(!action.check){ return next(action); } if(!user){ clearCookie(); hisotry.push(/); }}// 然後,再通過 applyMiddleware 直接綁定即可:applyMiddleware(Logined);

初始化狀態 initialState

initialState 相當於設置初始化所有 reducer 的信息,它直接通過 createStore 方法,利用 Object.assign() 模式來將屬性拷貝進入。其實不用 initialState 也是 OK 的,一般我們是直接在每個 Reducer 中直接設置好 state:

export const initialState = { currentTime: new Date().toString(),}export const reducer = (state = initialState, action) => { switch(action.type) { case types.FETCH_NEW_TIME: return { ...state, currentTime: action.payload} default: return state; }}

有時候,為了方便,也可以通過 initialState 抽到一個事件當中去:

# 設置初始化 reducer 信息export const initialState = { currentTime: currentTime.initialState, currentUser: currentUser.initialState,}# 然後通過 createStore 方法直接進行合併即可import { rootReducer, initialState } from ./reducers// ...export const configureStore = () => { const store = createStore( rootReducer, initialState, ); return store;}

combineReducers 做了什麼

combineReducers 主要和是 createStore API 結合在一起使用的。用來將多個 reducer 合併起來。他的最終結果是,針對不同 reducers 返回的結果,一般只能影響到本 reducers 裡面的內容。

export default createStore( combineReducers({ roomInfo, videoInfo }))

這樣,通過 roomInfo 和 videoInfo 兩個 reducers 返回的結果只能作用在當前 state 下的作用域。例如:通過 roomInfo 返回的 state,只能通過 state.roomInfo 獲得。

// 在 roomInfo.js 里export function showSuperAdmin(isAnchor,userUin) { return { type: SHOW_SUPER, admin:{ isAnchor, userUin, isShow:true, isSuper:true } };}// 在 index.js 中獲取得到:state.roomInfo.admin.xxx

這裡,combineReducers 也是對 state 做了命名空間的劃分。

applyMiddleware all procedure

applyMiddleware 整個執行過程:

對應於上文, 整個API的流程圖為:

關鍵點在於applyMiddleware 和 中間件兩個內容.

redux 中間件機制

middleware 有 3 個參數 store,action,next。例如:

const log = store => next => action => { //...}

他們分別代表著

關於 redux-middleware 還有一個比較流行的庫, 即, redux-thunk . 該庫灰常簡單, 就一個函數.

redux-thunk

直接看源碼算了:

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;

他和原來的中間件的寫法有一個非常不同的地方,在於. 他寫中間件的地方, 不在 createStore 裡面, 而在 dispatch 裡面.

// 初始化調用const store = createStore( rootReducer, applyMiddleware(thunk));// thunk 類型的中間件function doSth(forPerson) { // 這裡必須返回一個函數... 才能達到中間件的效果 return function (dispatch) { return async().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize(The Sandwich Shop, forPerson, error)) ); };}// 更簡便的寫法可以為:let doSth = forPerson=>dispatch=> async().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize(The Sandwich Shop, forPerson, error)))

看源碼,我們可以很容易發現, 在實際調用 dispatch 時, 不僅僅只有 dispatch 這一個參數,還有 getState,extraArgument 這兩個參數。 so, what are they doing?

getState 這個就不用說了, 就是用來獲取當前 redux 的 state.

那 extraArgument 幹啥嘞?

看源碼很容易發現, 就是在初始化 thunk 時, 傳入的參數. 其實, 也不會經常用到. 所以, 我們中間件實際可以寫為:

let doSth = forPerson=>(dispatch,getState,extArgs)=> async().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize(The Sandwich Shop, forPerson, error)))

對比與上面的 applyMiddle 來說, 使用 redux-thunk 可以實現 私有, 定製化的 中間件操作. 而,原來的 applyMiddleware 則 只能在初始階段設置相關的中間件, 但 卻可以實現 next 的執行域的分離. 所以, 兩個都可以使用, 只是看你具體需求是啥.

react-redux 中間件

不過,在 react-redux 中,已經將兩者都已經實現了,connect 方法和上面的一致,差別的是 dispatch。這裡,react-redux 將子單元的中間件的寫法應用到 dispatch 當中。即,一般使用 dispatch 返回 plain object(例如,type:xxx),這樣,可以通過全局的 actions 捕獲到而執行相同的方法。不過,dispatch 還可以接受一個函數格式如下:

// actions.jsfunction requestPosts(subreddit) { return { type: REQUEST_POSTS, subreddit }}function receivePosts(subreddit, json) { return { type: RECEIVE_POSTS, id }}export function fetchPosts(params) { return (dispatch,getState)=>{ dispatch(requestPosts(subreddit)) fetch(`http://www.subreddit.com/r/${subreddit}.json`) .then(response => response.json()) .then(json => dispatch(receivePosts(subreddit, json)) ) }}# 可以簡寫為export let fetchPosts = params=>(dispatch,getState)=>{ dispatch(requestPosts(subreddit)) fetch(`http://www.subreddit.com/r/${subreddit}.json`) .then(response => response.json()) .then(json => dispatch(receivePosts(subreddit, json)) )}// 在 index.js 中dispatch(fetchPosts(roomID));

getState 這個就不用說了, 就是用來獲取當前 redux 的 state.

這樣,可以在通過 dispatch 觸髮指定 action 時,進行非同步數據的拉取,不過最終它還是需要通過 dispatch 返回一個 plain object 來進行觸發全局的 actions。 整個中間件的流程圖為:


推薦閱讀:

什麼是redis?Redis的各項功能解決了哪些問題?以及redis的入門實戰

TAG:中間件 | 前端開發 | React |