Redux專題:中間件

本文是『horseshoe·Redux專題』系列文章之一,後續會有更多專題推出
來個人 GitHub repo 閱讀完整的專題文章
來個人 我的博客 得到無與倫比的閱讀體驗

Redux暴露很是少的API,優雅的將單向數據流落地。但有這些,Redux的做者Dan Abramov仍然以爲遠遠不夠。一個工具的強大之處體如今它的擴展能力上。Redux的中間件機制讓這種擴展能力一樣變的異常優雅。javascript

中間件在前端的意思是插入某兩個流程之間的一段邏輯。具體到Redux,就是在dispatch一個動做先後插入第三方的處理函數。前端

使用

還記得嗎?Store構造器createStore有三個參數,第三個參數叫作enhancer,翻譯過來就是加強器。咱們先將enhancer按下不表,而且告訴你其實Redux的另外一個APIapplyMiddleware就是一個enhancer。java

import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { userReducer } from './user/reducer';
import { todoReducer } from './todo/reducer';

const reducers = combineReducers({
    userStore: userReducer,
    todoStore: todoReducer,
});

const enhancer = applyMiddleware(thunk, logger);
const store = createStore(reducers, null, enhancer);

export default store;

只須要把全部中間件依次傳入applyMiddleware,就生成了一個加強器,它們就能夠發揮做用了。react

若是preloadedState爲空,enhancer能夠做爲第二個參數傳入。看源代碼:git

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.');
    }
    return enhancer(createStore)(reducer, preloadedState);
}

if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.');
}

服務器請求

一個組件免不了向服務器請求數據,然而開發者不但願組件內部有過多的邏輯,請求應該封裝成函數給組件調用,同時組件須要實時獲取請求的狀態以便展現不一樣的界面。最好的辦法就是將請求也歸入Redux的管理中。github

import api from './api';

export const fetchMovieAction = () => {
    dispatch({ type: 'FETCH_MOVIE_START' });
    api.fetchMovie().then(res => {
        dispatch({ type: 'FETCH_MOVIE_END', payload: { movies: res.data } });
    }).catch(err => {
        dispatch({ type: 'FETCH_MOVIE_ERROR', error: true, payload: { msg: err } });
    });
};
import React, { Component } from 'react';
import { connect } from 'react-redux';

import { fetchMovieAction } from './actions';
import Card from './Card';

class App extends Component {
    render() {
        const { movies } = this.props;
        return (
            <div className="movie">
                {movies.map(movie => <Card key={movie.id} {...movies} />)}
            </div>
        );
    }

    componentDidMount() {
        this.props.fetchMovie();
    }
}

const mapState = (state) => {
    return {
        movies: state.payload.movies,
    };
};

const mapDispatch = (dispatch) => {
    return {
        fetchMovie: () => dispatch(fetchMovieAction()),
    };
};

export default connect(mapState, mapDispatch)(App);

大功告成了。編程

只須要將請求封裝成一個函數,而後假裝成Action被髮射出去,請求調用先後,真正的Action會被髮射,在Store中存儲請求的狀態,而且可以被組件訂閱到。redux

異步Action

你是否是發現了什麼?對咯,這裏的Action不是一個純對象。api

由於請求必定是一個函數,爲了讓請求入會,只能反過頭來修改大會章程。可是大會章程豈能隨便推翻,這時意見領袖出來講話了:數組

當初規定Action必須是一個純對象不是爲了搞我的崇拜,而是出於實際須要。由於reducer必須是一個純函數,這決定了dispatch的參數Action必須是一個帶type字段的純對象。現現在咱們要拉異步請求入會,而中間件又能夠中途攔截作一些處理,那Action爲何不能是一個函數呢? Action必須是一個純對象這種說法是完徹底全的教條主義!

你們還動腦筋想出了一個異步Action的名頭,這下函數類型的Action終於名正言順了。

閉包

你是否是還發現了什麼?對咯,請求函數中的dispatch哪去了。

不知道,可能會報錯吧(無辜臉)。

其實咱們還有一件事沒幹:把dispatch方法偷渡到請求函數中。

export const fetchMovieAction = () => {
    return (dispatch) => {
        dispatch({ type: 'FETCH_MOVIE_START' });
        api.fetchMovie().then(res => {
            dispatch({ type: 'FETCH_MOVIE_END', payload: { movies: res.data } });
        }).catch(err => {
            dispatch({ type: 'FETCH_MOVIE_ERROR', error: true, payload: { msg: err } });
        });
    };
};

很簡單哪,加一個閉包,dispatch從返回函數的參數中偷渡進來。

腦洞

咱們要的不就是一個dispatch方法麼,我能不能這樣:

export const fetchMovie = (dispatch) => {
    dispatch({ type: 'FETCH_MOVIE_START' });
    api.fetchMovie().then(res => {
        dispatch({ type: 'FETCH_MOVIE_END', payload: { movies: res.data } });
    }).catch(err => {
        dispatch({ type: 'FETCH_MOVIE_ERROR', error: true, payload: { msg: err } });
    });
};
const mapDispatch = (dispatch) => {
    return {
        fetchMovie: () => fetchMovie(dispatch),
    };
};

貌似是能行得通的,只不過這時候請求函數已經不能叫Action了。考慮到以前請求函數假裝成Action渾水摸魚,還要插入中間件來幫助特殊處理,咱們這樣作也不過度是吧。

好處就是再也不須要可以處理異步Action的中間件了。

壞處就是這不符合規範,是個人腦洞,闖了禍不要打我(蔑視)。

redux-thunk

前面屢次提處處理異步Action的中間件,究竟是何方神聖?

市面上流行的方案有不少種,咱們挑最簡單的一種來講一說(都不點贊怪我咯)。

redux-thunk算是Redux官方出品的異步請求中間件,可是它沒有集成到Redux中,緣由仍是爲了擴展性,社區能夠提出各類方案,開發者各取所需。

讓咱們來探討一下redux-thunk的思路:原來Action只有一種,就是純對象,如今Action有兩種,純對象和異步請求函數。只不過多了一種狀況,不算棘手嘛。若是Action是一個對象,不爲難它直接放走;若是Action是一個函數,就地執行,調用異步請求先後,真正的Action天然會釋放出來,又回到第一步,放它走。

這是redux-thunk簡化後的代碼,其實源代碼也跟這差很少。是否是很恐慌?

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

上面的函數就部署在個人我的博客中用來處理異步請求,徹底沒有問題。既然它這麼簡單,並且能夠預計它萬年不會變,那我爲何要憑空多一個依賴包。就將它放在個人眼皮底下不是挺好的嘛。

不過,它是一個研究中間件很好的範本。

咱們先將thunk先生降級成普通函數的寫法:

const thunk = function({ dispatch, getState }) {
    return function(next) {
        return function(action) {
            if (typeof action === 'function') {
                return action(dispatch, getState);
            }
            return next(action);
        }
    }
};

compose

我知道compose是Redux的五大護法之一,可爲何挑在這個時候講它呢?

先不告訴你。

compose在函數式編程中的含義是組合。假如你有一堆函數要依次執行,並且上一個函數的返回結果是下一個函數的參數,咱們怎樣寫看起來最裝逼?

const result = a(b(c(d(e('redux')))));

這種寫法讓人一眼就看穿了調用細節,裝逼明顯是不夠的。

咱們來看Redux是怎麼實現compose的:

export default function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg;
    }
    if (funcs.length === 1) {
        return funcs[0];
    }
    return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

誒,我看見reduce了,而後...就沒有而後了。

假設咱們如今有三個函數:

const funcA = arg => console.log('funcA', arg);
const funcB = arg => console.log('funcB', arg);
const funcC = arg => console.log('funcC', arg);

執行reduce的第一步返回的accumulator(accumulator是reduce中的概念),結果顯而易見:

(...args) => funcA(funcB(...args));

執行reduce的第二步返回的accumulator,注意到,這時reduce已經執行完了,返回的是一個函數。

(...args) => funcA(funcB(funcC(...args)));

特別提醒:執行compose最終返回的是一個函數。也就是說開發者得這麼幹compose(a, b, c)()才能讓傳入的函數依次執行。

另外須要注意的是:傳入的函數是從右到左依次執行的。

applyMiddleware

廢話少說,先上源代碼:

export default function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        const store = createStore(...args);
        let dispatch = () => {
            throw new Error(
                `Dispatching while constructing your middleware is not allowed. ` +
                `Other middleware would not be applied to this dispatch.`
            );
        };
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args),
        };
        const chain = middlewares.map(middleware => middleware(middlewareAPI));
        dispatch = compose(...chain)(store.dispatch);
        return { ...store, dispatch };
    }
}

還記得中間件閉包好幾層的寫法嗎?如今咱們就來一層一層的剝開它。

middlewareAPI是一個對象,正好是傳給第一層中間件函數的參數。執行它,返回的chain是由第二層函數組成的中間件數組。貼一下redux-thunk第二層轉正後的樣子:

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

中間件第二層函數接收一個next參數,那這個next具體指什麼呢?我先透露一下,next是整個Redux中間件機制的題眼,理解了next就能夠對Redux中間件的理解達到大徹大悟的化境。

以前咱們已經拆解了compose的內部機制,從右到左執行,最右邊的中間件的參數就是store.dispatch,它返回的值就是倒數第二個中間件的next。它返回什麼呢?咱們再剝一層:

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

別看redux-thunk麻雀雖小,你們發現沒有,第三層函數纔是它的邏輯,前面兩層都是配合redux的演出。也就是說呀同窗們,除了最後一箇中間件的next是原始的dispatch以外,倒數往前的中間件傳入的next都是上一個中間件的邏輯函數。

Redux中間件本質上是將dispatch套上一層本身的邏輯。

最終applyMiddleware裏獲得的這個dispatch是通過無數中間件精心包裝,植入了本身的邏輯的dispatch。而後用這個臃腫的dispatch覆蓋原有的dispatch,將Store的API返回。

每個Action就是這樣穿太重重的邏輯代碼才能最後被髮射成功。只不過處理異步請求的中間件再也不往下走,直到異步請求發生,真正的Action被髮射出來,纔會走到下一個中間件的邏輯。

構建dispatch過程當中禁止執行dispatch

middlewareAPI中的dispatch爲何是一個拋出錯誤的函數?

咱們如今已經知道,applyMiddleware的目的只有一個:用全部中間件組裝成一個超級dispatch,並將它覆蓋原生的dispatch。可是若是超級dispatch還沒組裝完成,就被中間件調用了原生的dispatch,那這遊戲別玩了。

因此Redux來了一手掉包。

middlewareAPI初始傳入的dispatch是一個炸彈,中間件的開發者膽敢在頭兩層閉包函數的外層做用域調用dispatch,炸彈就會引爆。而一旦超級dispatch構建完成,這個超級dispatch就會替換掉炸彈。

怎麼替換呢?

函數也是引用類型對吧,炸彈dispatch之因此用let定義,就是爲了未來修改它的引用地址:

let dispatch = () => {
    throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
        `Other middleware would not be applied to this dispatch.`
    );
};
// ...
dispatch = compose(...chain)(store.dispatch);

固然,這是對中間件開發者的約束,若是你只是一箇中間件的使用者,這可有可無。

applyMiddleware的花式調用

咱們注意到,執行applyMiddleware返回的是一個函數,這個函數有惟一的參數createStore。

WTF?

applyMiddleware不是createStore的參數之一麼:

const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));

怎麼createStore也成了applyMiddleware的參數了?

貴圈真亂。

首先咱們明確一點,applyMiddleware是一個加強器,加強器是須要改造Store的API的,這樣才能達到加強Store的目的。因此applyMiddleware必須傳入createStore以生成初始的Store。

因此生成一個最終的Store其實能夠這樣寫:

const enhancedCreateStore = applyMiddleware(middleware1, middleware2, middleware3)(createStore);
const store = enhancedCreateStore(reducer);

那一般的那種寫法,Redux內部是怎麼處理的呢?

if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
        throw new Error('Expected the enhancer to be a function.')
    }
    return enhancer(createStore)(reducer, preloadedState)
}

上面是createStore源代碼中的一段。

若是enhancer存在而且是一個函數,那麼直接傳入createStore執行,再傳入reducer和preloadedState執行(這時候再傳入enhancer就沒完沒了了),而後直接返回。

喵,後面還有好多代碼呢,怎麼就返回了?

不,就這麼任性。

這麼看下來,如下寫法纔是正宗的Redux:

const store = applyMiddleware(middleware1, middleware2, middleware3)(createStore)(reducer);

如下寫法只是Redux爲開發者準備的語法糖:

const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));

洋蔥圈模型

想必你們都據說過中間件的洋蔥圈模型,這個比喻很是形象,乍聽上去,啊,好像明白了。可是你們真的對洋蔥圈模型有一個具象化的理解嗎?

假設如今有三個中間件:

const middleware1 = ({ dispatch, getState }) => next => action => {
    console.log('middleware1 start');
    next(action);
    console.log('middleware1 end');
}

const middleware2 = ({ dispatch, getState }) => next => action => {
    console.log('middleware2 start');
    next(action);
    console.log('middleware2 end');
}

const middleware3 = ({ dispatch, getState }) => next => action => {
    console.log('middleware3 start');
    next(action);
    console.log('middleware3 end');
}

如今將它傳入applyMiddleware:

function reducer(state = {}, action) {
    console.log('reducer return state');
    return state;
}

const middlewares = [middleware1, middleware2, middleware3];
const store = createStore(reducer, applyMiddleware(...middlewares));

咱們看一下打印的結果:

middleware1 start
middleware2 start
middleware3 start
reducer return state
middleware3 end
middleware2 end
middleware1 end

對結果感到驚訝嗎?其實理解函數調用棧的同窗就能明白爲何是這樣的結果。reducer執行以前也就是dispatch真正執行以前的日誌好理解,dispatch被一層一層包裝,一層一層的深刻調用。可是dispatch執行完之後呢?這時候的執行權在調用棧最深的那一層邏輯那裏,也就是最接近原始dispatch的邏輯函數那裏,因此以後的執行順序是從最深處往上調用。

總的看下來,一個Action的更新Store之旅就像穿過一個洋蔥圈的旅行。一堆中間件簇擁着Action鑽到洋蔥的中心,Action執行本身的使命更新Store後就地圓寂,而後中間件帶着它的遺志再從洋蔥的中心鑽出來。

回看compose

其實我解釋上面的打印日誌,還有一個關節沒有打通。

記得applyMiddleware的源代碼嗎?內部調用了compose來執行chain。

咱們強調過,compose的函數類型參數的執行順序是從右到左的,我相信你們在很多的地方都見到過這樣的表述。可是你們想過沒有,爲何要從右到左執行?原生JavaScript除了實現reduce以外還有一個reduceRight,從左到右執行並無什麼技術障礙,那麼爲何要讓執行順序這麼彆扭呢?

答案就在上面的打印日誌裏。

打印日誌很好哇,根據傳入的順序執行。對,執行compose是從右到左,可是compose返回的終極dispatch是一層一層從外面包裹的呀,最後一箇中間件也就是最左邊的中間件的邏輯,包裹在最外面一層,天然它的日誌最早被打印出來。

因此compose被設計成參數從右到左執行,不是有技術障礙,也不是Redux特立獨行,而是其中原本就要經歷一次反轉,compose只有再反轉一次才能將它扭轉過來。

Redux專題一覽

考古
實用
中間件
時間旅行

相關文章
相關標籤/搜索