React 專案中Redux 中介軟體的理解

Deot發表於2016-12-18

前言

React/Redux專案結束後,當我在研究react-router原始碼的時候發現當中有一部分含中介軟體的思想,所以才想把中介軟體重新梳理一遍;在之前看redux瞭解到中介軟體,redux層面中介軟體的理解對專案前期比較有幫助,雖然專案中後期基本可以忽略這層概念;現在對這部分的筆記重新梳理,這裡只針對這個中介軟體做一個理解。

如果想學習專案的底層建設,建議先去學習官網redux案例,之後在學習react-router的使用

Redux 中介軟體介紹

Redux 目的是提供第三方外掛的模式,改變action -> reducer 的過程。變為 action -> middlewares -> reducer 。自己在專案中使用它改變資料流,實現非同步 action ;下面會對日誌輸出做一個開場。

使用 Redux 中介軟體

Redux 中 applyMiddleware 的方法,可以應用多箇中介軟體,這裡先只寫一箇中介軟體,以日誌輸出中介軟體為例

//利用中介軟體做列印log
import {createStore,applyMiddleware} from `redux`;
import logger from `../api/logger`;
import rootReducer from `../reducer/rootReducer`;


let createStoreWithMiddleware = applyMiddleware(logger)(createStore);
let store = createStoreWithMiddleware(rootReducer);
// 也可以直接這樣,可以參考createStore
// createStore(
//     rootReducer,
//     applyMiddleware(logger)
// )
export default store;

logger 中介軟體結構分析

const logger = store => next => action => {
    let result = next(action); // 返回的也是同樣的action值
    console.log(`dispatch`, action);
    console.log(`nextState`, store.getState());
    return result;
};

export default logger;

store => next => action =>{} 實現了三層函式巢狀,最後返回 next ,給下一個中介軟體使用,接下來把三層函式拆解;

從applyMiddleware原始碼開始分析

///redux/src/applyMiddleware.js
export default function applyMiddleware(...middlewares) {
    return (createStore) => (reducer, initialState, enhancer) => {
        var store = createStore(reducer, initialState, 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
        }
    }
}
最外層store
//原始碼分析
chain = middlewares.map(middleware => middleware(middlewareAPI));

我們發現store是middlewareAPI,

//store
var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
}

然後就剩下

next => action => {
    let result = next(action); // 返回的也是同樣的action值
    console.log(`dispatch`, action);
    console.log(`nextState`, store.getState());
    return result;
};
中間層next
//原始碼分析
dispatch = compose(...chain)(store.dispatch)

先來分析compose(…chain)

//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]
    const rest = funcs.slice(0, -1)
    return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

compose利用Array.prototype.reduceRight的方法

//reduceRight遍歷介紹
[0, 1, 2, 3, 4].reduceRight(function(previousValue, currentValue, index, array) {
    return previousValue + currentValue;
}, 10);

//結果 10+4+3+2+1+0 = 20

因為我們這裡的中介軟體就只有一個,所以沒有使用到reduceRight直接返回,直接返回func[0](本身);再由compose(...chain)(store.dispatch),我們可以知道next就是store.dispatch

(action) => {
    let result = store.dispatch(action); // 這裡的next就是store.dispatch
    console.log(`dispatch`, action);
    console.log(`nextState`, store.getState());
    return result;
};

我們之後呼叫的dispath就是觸發的是上面這個函式(這裡就單箇中介軟體);

多箇中介軟體

  • 通過上面的 applyMiddleware , compose 和中介軟體的結構,

  • 假設應用瞭如下的中介軟體: [A, B, C],這裡我們使用es5的結構做分析

  • 分析action觸發的完整流程

三個中介軟體

//A
function A(store) {
    return function A(next) {
        return function A(action) {
            /*...*/;
            next(action);
            /*...*/;
            return /*...*/;
        }
    }
}
//B
function B(store) {
    return function B(next) {
        return function B(action) {
            /*...*/;
            next(action);
            /*...*/;
            return /*...*/;
        }
    }
}
//C
function C(store) {
    return function C(next) {
        return function C(action) {
            /*...*/;
            next(action);
            /*...*/;
            return /*...*/;
        }
    }
}

通過chain = middlewares.map(middleware => middleware(middlewareAPI)),三個中介軟體的狀態變化

//A
function A(next) {
    return function A(action) {
        /*...*/;
        next(action);
        /*...*/;
        return /*...*/;
    }
}
//B
function B(next) {
    return function B(action) {
        /*...*/;
        next(action);
        /*...*/;
        return /*...*/;
    }
}
//C
function C(next) {
    return function C(action) {
        /*...*/;
        next(action);
        /*...*/;
        return /*...*/;
    }
}

再由dispatch = compose(...chain)(store.dispatch),我們轉化下

const last = C;
const rest = [A,B]
dispatch = rest.reduceRight(
    (composed, f) =>{
        return f(composed)
    }, 
    last(store.dispatch)
)

我們得到的結果

dispatch = A(B(C(store.dispatch)));

進一步分析,我們得到的結果

dispatch = A(B(C(store.dispatch)));

//執行C(next),得到結果

A(B(function C(action) {/*...*/;next(action);/*...*/;return /*...*/;})); 
//此時的next = store.dispatch

//繼續執行B(next)
A(function B(action) {/*...*/;next(action);/*...*/;return /*...*/;});    
//此時的next = function C(action) {/*...*/;next(action);/*...*/;return /*...*/;}

//繼續執行A(next)
function A(action) {/*...*/;next(action);/*...*/;return /*...*/;};
//此時的next = function B(action) {/*...*/;next(action);/*...*/;return /*...*/;}

一個action觸發執行順序,A(action) -> B(action) -> C(action) -> store.dispatch(action)(生產最新的 store 資料);

如果next(action)下面還有需要執行的程式碼,繼續執行 C(next 後的程式碼)->B(next 後的程式碼)->A(next 後的程式碼)

總結:先從內到外生成新的func,然後由外向內執行。本來我們可以直接使用store.dispatch(action),但是我們可以通過中介軟體對action做一些處理或轉換,比如非同步操作,非同步回撥後再執行next;這樣的設計很巧妙,只有等待next,才可以繼續做操作,和平時直接非同步回撥又有些不一樣

專案實踐 ->非同步

我們知道redux中actions分為actionType,actionCreator,然後在由reducer進行修改資料;

官方例子中async直接在actionCreator做了ajax請求;

我們把ajax放入中介軟體觸發下面要講的與官方real-world類似

我這邊使用redux-thunk

applyMiddleware(reduxThunk, api)

先來看看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;

這樣一來我們可以把非同步寫成一個複用的actionCreator;

import * as types from `../../constants/actions/common`;

export function request(apiName, params, opts = {}) {
    return (dispatch, getState) => {
        let action = {
            `API`: {
                apiName: apiName,
                params: params,
                opts: opts
            },
            type: types.API_REQUEST
        };
        return dispatch(action);
    };
}


//其他地方呼叫複用的方法如下:
export { request } from `./request`;

正常的寫法,不是非同步的,就是之前的寫法

export function cartSelect(id) {
    return { 
        type: types.CART_MAIN_SELECT, 
        id
    };
}

然後就是下一個中介軟體的處理 api.js

//自己封裝的ajax,可以使用別的,比如isomorphic-fetch
import net from `net`;
//專案中全部的介面,相當於一個關於非同步的actionType有一個對應的後端介面
import API_ROOT from `apiRoot`;

export default store => next => action => {
    let API_OPT = action[`API`];

    if (!API_OPT) {
        //我們約定這個沒宣告,就不是我們設計的非同步action,執行下一個中介軟體
        return next(action);
    }

    let ACTION_TYPE = action[`type`];
    let { apiName, params = {} , opts = {} } = API_OPT;
    /**
     * 如果有傳遞localData,就不會觸發ajax了,直接觸發_success
     * 當前也可以傳其他引數
     */
    let { localData } = opts;
    let {
        onSuccess,
        onError,
        onProgress,
        ajaxType = `GET`,
        param
    } = params;
    // 觸發下一個action
    let nextAction = function(type, param, opts) {
        action[`type`] = type;
        action[`opts`] = opts;
        delete param[`onSuccess`];
        delete param[`onError`];
        const nextRequestAction = {...action,...param}
        return nextRequestAction;
    };

    params={
        ...params,
        data: null
    };
    // 觸發正在請求的action
    let result = next(nextAction(apiName + `_ON`, params, opts));
    net.ajax({
        url: API_ROOT[apiName],
        type: ajaxType,
        param,
        localData,
        success: data => {
            onSuccess && onSuccess(data);
            params={
                ...params,
                data
            };
            //觸發請求成功的action
            return next(nextAction(apiName + `_SUCCESS`, params, opts));
        },
        error: data => {
            onError && onError(data);
            //觸發請求失敗的action
            return next(nextAction(apiName + `_ERROR`, params, opts));
        }
    });

    return result;
};

強調一點:專案中全部的介面,相當於一個關於非同步的actionType有一個對應的後端介面,所以我們才可以通過API_ROOT[apiName]找到這個介面

以cart為列子(下面是對應的每個檔案):

actionType:

//非同步
export const CART_MAIN_GET = `CART_MAIN_GET`;
//非非同步
export const CART_MAIN_SELECT = `CART_MAIN_SELECT`;

api:

const api = {
    `CART_MAIN_GET`:`/shopping-cart/show-shopping-cart`
};
export default api;

APIROOT修改:

import cart from `./api/cart`;
const APIROOT = {
    ...cart
};
export default API;

actionCreator:

//專案中使用redux的bindActionCreators做一個統一的繫結,所以在這裡單獨引入
export { request } from `./request`;
//下面是非非同步的方法
export function cartSelect(id) {
    return { 
        type: types.CART_MAIN_SELECT, 
        id
    };
}

專案中發起結構是這樣的:

let url = types.CART_MAIN_GET;
let param = {};
let params = {
    param: param,
    ajaxType: `GET`,
    onSuccess: (res) => {
        /*...*/
    },
    onError: (res) => {
        /*...*/
    }
};
request(url, params, {});

其對應的reducers就是下面

import * as types from `../constants/actions/cart`;
const initialState = {
    main:{
        isFetching: 0,//是否已經獲取 
        didInvalidate:1,//是否失效
        itemArr:[],//自定義模版
        itemObj:{},//自定義模版資料
        header:{}//頭部導航
    }
};
export default function(state = initialState, action) {
    let newState;
    switch (action.type) {
        case types.HOME_MAIN_GET + `_ON`://可以不寫
            /*...*/
            return newState;
        case types.HOME_MAIN_GET + `_SUCCESS`:
            /*...*/
            return newState;
        case types.HOME_MAIN_GET + `_ERROR`://可以不寫
            /*...*/
            return newState;
        default:
            return state;
    }
};

非同步,資料驗證都可以通過中介軟體做處理;引用Generator,Async/Await,Promise處理,可以參考社群中的一些其他方式,比如:

相關文章