關於Redux的一些總結(一):Action & 中間件 & 異步

淺說Flux開發中,簡單介紹了Flux及其開發方式。Flux能夠說是一個框架,其有自己的 Dispatcher 接口供開發者;也能夠說是一種數據流單向控制的架構設計,圍繞單向數據流的核心,其定義了一套行爲規範,以下圖:html

flux

Redux的設計就繼承了Flux的架構,並將其完善,提供了多個API供開發者調用。藉着react-redux,能夠很好的與React結合,開發組件化程度極高的現代Web應用。本文是筆者近半年使用react+redux組合的一些總結,不當之處,敬請諒解。react

Action

Action是數據從應用傳遞到 store/state 的載體,也是開啓一次完成數據流的開始。git

以添加一個todo的Action爲例:github

{
    type:'add_todo',
    data:'我要去跑步'
}

這樣就定義了一個添加一條todo的Action,而後就能經過某個行爲去觸發這個Action,由這個Action攜帶的數據(data)去更新store(state/reducer):redux

store.dispatch({
    type:'add_todo',
    data:'your data'
})

type 是一個常量,Action必備一個字段,用於標識該Action的類型。在項目初期,這樣定義Action也能愉快的擼碼,可是隨着項目的複雜度增長,這種方式會讓代碼顯得冗餘,由於若是有多個行爲觸發同一個Action,則這個Action要寫屢次;同時,也會形成代碼結構不清晰。於是,得更改建立Action的方式:segmentfault

const ADD_TODO = 'add_todo';

let addTodo = (data='default data') => {
    return {
        type: ADD_TODO,
        data: data
    }
}

//觸發action
store.dispatch(addTodo());

更改以後,代碼清晰多了,若是有多個行爲觸發同一個Action,只要調用一下函數 addTodo 就行,並將Action要攜帶的數據傳遞給該函數。相似 addTodo 這樣的函數,稱之爲 Action Creator。Action Creator 的惟一功能就是返回一個Action供 dispatch 進行調用。api

可是,這樣的Action Creator 返回的Action 並非一個標準的Action。在Flux的架構中,一個Action要符合 FSA(Flux Standard Action) 規範,須要知足以下條件:promise

  • 是一個純文本對象架構

  • 只具有 typepayloaderrormeta 中的一個或者多個屬性。type 字段不可缺省,其它字段可缺省app

  • 若 Action 報錯,error 字段不可缺省,切必須爲 true

payload 是一個對象,用做Action攜帶數據的載體。因此,上述的寫法能夠更改成:

let addTodo = (data='default data') => {
    return {
        type: ADD_TODO,
        payload: {
            data
        }
    }
}

在 redux 全家桶中,能夠利用 redux-actions 來建立符合 FSA 規範的Action:

import {creatAction} from 'redux-actions';

let addTodo = creatAction(ADD_TODO)
//same as
let addTodo = creatAction(ADD_TODO,data=>data)

能夠採用以下一個簡單的方式檢驗一個Action是否符合FSA標準:

let isFSA = Object.keys(action).every((item)=>{
   return  ['payload','type','error','meta'].indexOf(item) >  -1
})

中間件

在我看來,Redux提升了兩個很是重要的功能,一是 Reducer 拆分,二是中間件。Reducer 拆分可使組件獲取其最小屬性(state),而不須要整個Store。中間件則能夠在 Action Creator 返回最終可供 dispatch 調用的 action 以前處理各類事情,如異步API調用、日誌記錄等,是擴展 Redux 功能的一種推薦方式。

Redux 提供了 applyMiddleware(...middlewares) 來將中間件應用到 createStore。applyMiddleware 會返回一個函數,該函數接收原來的 creatStore 做爲參數,返回一個應用了 middlewares 的加強後的 creatStore。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    //接收createStore參數
    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)
    
    //返回經middlewares加強後的createStore
    return {
      ...store,
      dispatch
    }
  }
}

建立 store 的方式也會因是否使用中間件而略有區別。未應用中間價以前,建立 store 的方式以下:

import {createStore} from 'redux';
import reducers from './reducers/index';

export let store = createStore(reducers);

應用中間價以後,建立 store 的方式以下:

import {createStore,applyMiddleware} from 'redux';
import reducers from './reducers/index';

let createStoreWithMiddleware = applyMiddleware(...middleware)(createStore);
export let store = createStoreWithMiddleware(reducers);

那麼怎麼自定義一箇中間件呢?

根據 redux 文檔,中間件的簽名以下:

({ getState, dispatch }) => next => action

根據上文的 applyMiddleware 源碼,每一箇中間件接收 getState & dispatch 做爲參數,並返回一個函數,該函數會被傳入下一個中間件的 dispatch 方法,並返回一個接收 action 的新函數。

以一個打印 dispatch action 先後的 state 爲例,建立一箇中間件示例:

export default function({getState,dispatch}) {
    return (next) => (action) => {
        console.log('pre state', getState());
        // 調用 middleware 鏈中下一個 middleware 的 dispatch。
        next(action);
        console.log('after dispatch', getState());
    }
}

在建立 store 的文件中調用該中間件:

import {createStore,applyMiddleware} from 'redux';

import reducers from './reducers/index';
import log from '../lib/log';

//export let store = createStore(reducers);

//應用中間件log
let createStoreWithLog = applyMiddleware(log)(createStore);
export let store = createStoreWithLog(reducers);

能夠在控制檯看到輸出:

img

能夠對 store 應用多箇中間件:

import log from '../lib/log';
import log2 from '../lib/log2';

let createStoreWithLog = applyMiddleware(log,log2)(createStore);
export let store = createStoreWithLog(reducers);

log2 也是一個簡單的輸出:

export default function({getState,dispatch}) {
    return (next) => (action) => {
        console.log('我是第二個中間件1');
        next(action);
        console.log('我是第二個中間件2');
    }
}

看控制檯的輸出:

img2

應用多箇中間件時,中間件調用鏈中任何一個缺乏 next(action) 的調用,都會致使 action 執行失敗

異步

Redux 自己不處理異步行爲,須要依賴中間件。結合 redux-actions 使用,Redux 有兩個推薦的異步中間件:

兩個中間件的源碼都是很是簡單的,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;

從源碼可知,action creator 須要返回一個函數給 redux-thunk 進行調用,示例以下:

export let addTodoWithThunk = (val) => async (dispatch, getState)=>{
    //請求以前的一些處理
        
    let value = await Promise.resolve(val + ' thunk');
    dispatch({
        type:CONSTANT.ADD_TO_DO_THUNK,
        payload:{
            value
        }
    });
};

效果以下:

thunk

這裏之因此不用 createAction,如前文所說,由於 createAction 會返回一個 FSA 規範的 action,該 action 會是一個對象,而不是一個 function:

{
    type: "add_to_do_thunk",
    payload: function(){}
}

若是要使用 createAction,則要自定義一個異步中間件。

export let addTodoWithCustom = createAction(CONSTANT.ADD_TO_DO_CUSTOM, (val) => async (dispatch, getState)=>{
    let value = await Promise.resolve(val + ' custom');
    return {
        value
    };
});

在通過中間件處理時,先判斷 action.payload 是不是一個函數,是則執行函數,不然交給 next 處理:

if(typeof action.payload === 'function'){
    let res = action.payload(dispatch, getState);
} else {
    next(action);
}

而 async 函數返回一個 Promise,於是須要做進一步處理:

res.then(
    (result) => {
        dispatch({...action, payload: result});
    },
    (error) => {
        dispatch({...action, payload: error, error: true});
    }
);

這樣就自定義了一個異步中間件,效果以下:

custom

固然,咱們能夠對函數執行後的結果是不是Promise做一個判斷:

function isPromise (val) {
    return val && typeof val.then === 'function';
}

//對執行結果是不是Promise
if (isPromise(res)){
    //處理
} else {
    dispatch({...action, payload: res});
}

那麼,怎麼利用 redux-promise 呢?redux-promise 是能處理符合 FSA 規範的 action 的,其對異步處理的關鍵源碼以下:

action.payload.then(
    result => dispatch({ ...action, payload: result }),
    error => {
        dispatch({ ...action, payload: error, error: true });
        return Promise.reject(error);
    }
)

於是,返回的 payload 再也不是一個函數,而是一個 Promise。而 async 函數執行後就是返回一個 Promise,因此,讓上文定義的 async 函數自執行一次就能夠:

export let addTodoWithPromise = createAction(CONSTANT.ADD_TO_DO_PROMISE, (val) =>
    (async (dispatch, getState)=>{
        let value = await Promise.resolve(val + ' promise');
        return {
            value
        };
    })()
);

結果以下圖:

promise

示例源碼:redux-demo

原文:https://github.com/dwqs/blog/...

相關文章
相關標籤/搜索