React Redux 與胖虎他媽

Mindjet發表於2018-01-15

本文將涉及以下三塊內容:

  • 多 Reducer
  • 中介軟體
  • 封裝元件方便獲取 Store

前言

在上一篇文章《React Redux與胖虎》 中我們詳盡地介紹了 React Redux,也寫了一個簡單的計數器。

這篇文章叫《React Redux與胖虎他媽》,因為在哆啦A夢裡面,胖虎雖然很屌老是欺負大雄和小夫,但是在他媽面前沒少捱揍,胖虎他媽還是他媽,所以這篇文章主要是介紹 React Redux 的一些進階用法。

React Redux 與胖虎他媽

多 Reducer

單 Reducer 不好嗎

開發過程中,我們由於業務或者功能的劃分,一般不同模組的資料也是不同的,如果只用一個 Reducer,那麼這個 Reducer 要處理所有模組過來的事件,然後返回一個 state,所有的資料都糅合在這個 state 裡面,所有接收到這個 state 的模組還得解析出其中跟自己有關的部分。

所以單個 Reducer 並不能滿足當下需求,多 Reducer 的出現有利於我們模組化開發,降低耦合度。

redux 提供了 combineReducers 函式來組合 Reducer,注意不是 react-redux 庫。

// reducers/index.js
import { combineReducers } from 'redux';
import firstReducer from './first-reducer';
import secondReducer from './second-reducer';

const reducers = combineReducers({ 
  firstReducer, 
  secondReducer 
});
複製程式碼

注意上面 combineReducers 的引數使用 ES6 的語法,相當於:

const reducers = combineReducers({ 
  firstReducer: firstReducer, 
  secondReducer: secondReducer
});
複製程式碼

注意一點:每發出一個事件,所有 Reducer 都會收到

多 Reducer 返回的 state

我們知道,在 Reducer 只有一個的情況下,容器元件的 mapStateToProps 函式接收到的 state 即為唯一 Reducer 返回的物件。

而在 Reducer 有多個的情況下,就會有多個返回值。這時候容器元件的 mapStateToProps 函式接收到的 state 其實是包含所有 Reducer 返回值的物件。可以用 key 值來區它們,這個 key 值就是我們在 combineReducers 時傳入的。

const mapStateToProps = (state) => {
  const firstReducer = state.firstReducer;
  const secondReducer = state.secondReducer;
  return {
    value1: firstReducer.value,
    value2: secondReducer.value
  };
}

export default connect(mapStateToProps)(Counter);
複製程式碼

當然,一般都是隻需要用其中一個 state,那麼我們可以寫成:

const mapStateToProps = ({ firstReducer }) => {
  return {
      value: firstReducer.value
  };
}
//或者更加語義化地表示為state
const mapStateToProps = ({ firstReducer: state }) => {
  return {
      value: state.value
  };
}
複製程式碼

這樣一來可以有效地隔離各個模組之間的影響,也方便多人協作開發。

React Redux 與胖虎他媽

(由於胖虎他媽實在沒什麼表情,所以還是用胖虎開涮吧)

中介軟體

網上對於中介軟體的解釋基本上都是“位於應用程式和作業系統之間的程式”之類,這只是一個基本的概述。在 React Redux 裡面,中介軟體的位置很明確,就是在 Action 到達 Reducer 之前做一些操作

React Redux 的中介軟體實際上是一個高階函式:

function middleware(store) {
    return function wrapper(next) {
        return function inner(action) {
            ...
        }
    }
}
複製程式碼

其中最內層的函式接收的正是 Action。

中介軟體可以多個疊加使用,在中介軟體內部使用 next 函式來將 Action 傳送到下一個中介軟體讓其處理。如果沒有下一個中介軟體,那麼會將 Action 傳送到 Reducer 去。

我們看如何將中介軟體應用到 React Redux 應用中。

redux 提供了 applyMiddleware, compose 函式來幫助新增中介軟體:

import { applyMiddleware, compose, createStore } from 'redux';
import api from '../middlewares/api';
import thunk from 'redux-thunk';
import reducers from "../reducers";

const withMiddleware = compose(
    applyMiddleware(api),
)(createStore);

const store = withMiddleware(reducers);

export default store;
複製程式碼

可以看到 applyMiddleware 函式可以將中介軟體引入,使用 compose 函式將多個函式整合成一個新的函式。

對於 applyMiddleware, compose, createStore 這三個函式的實現,可以自己去參考原始碼。

這裡說一下,這三個函式雖然程式碼量不大,但是其實用了挺多函數語言程式設計的思想和做法,一開始看會很抽象,特別是幾個箭頭符號連著用更是懵逼。但是看原始碼總是好的,一旦你漸入佳境,定會發現新的天地。不過,這裡就只講用法了,說實話我也還沒認真去看(逃

簡單中介軟體

我們可以實現一個炒雞簡單的中介軟體來看看效果,比如說,在事件到達 Reducer 之前,把事件列印出來。

export default store => next => action => {
  console.log(action);
  next(action);
}
複製程式碼

emmmm,是挺簡單的....

複雜中介軟體

在談複雜中介軟體時,我們需要先說說同步事件、非同步事件。

在 React Redux 應用中,我們平時發出去的事件都是直接到達中介軟體(如果有中介軟體的話)然後到達 Reducer,乾淨利落毫不拖拉,這種事件我們稱為同步事件。

而非同步事件,按照我個人理解,指的是,你發出去的事件,經過中介軟體時有了可觀的時間停留,並不會立即傳到 Reducer 裡面處理。也就是說,這個非同步事件導致事件流經過中介軟體時發生了耗時操作,比如訪問網路資料、讀寫檔案等,在操作完成之後,事件才繼續往下流到 Reducer 那兒。

嗯...同步事件我們都知道怎麼寫:

{
  type: 'SYNC_ACTION',
  ...
}
複製程式碼

非同步事件的話,一般是定義成一個函式:

function asyncAction({dispatch, getState}) {
  const action = {
    type: 'ASYNC_ACTION',
    api: {
      url: 'www.xxx.com/api',
      method: 'GET'         	
    }
  };
  dispatch(action);
}
複製程式碼

但是,現在我們的非同步事件是一個函式,你如果不作任何處理的話直接執行 dispatch(asyncAction) ,那麼會報錯,告訴你只能傳送 plain object,即類似於同步事件那樣的物件。

redux-thunk

我們要在中介軟體搞些事情,讓函式型別的 Action 可以用,簡單地可以使用 redux-thunk

P.S. 雖然我不是專門搞前端的,雖然我是男的,但是作者 gaearon 真的好帥......

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;

複製程式碼

胖虎都懶得看

emmmm,其實它做的事情就是判斷傳進來的 Action 是不是 function 型別的,如果是,就執行這個 action 並且把 store.dispatchstore.getState 傳給它;如果不是,那麼呼叫 next 將 Action 繼續往下傳送就行了。

帶有網路請求的中介軟體

行吧... 那我們仿照 redux-thunk 寫一箇中介軟體,整合進網路請求的功能。

  1. 首先當然是允許 function 型別的 Action

    export default store => next => action => {
        if (typeof action === 'function') {
            return action(store);
        }
    }
    複製程式碼
  2. 然後當 Action 是 plain object 而且沒有 api 欄位時,當成同步事件處理

    export default store => next => action => {
        if (typeof action === 'function') {
            return action(store);
        }
    
        const { type, api, isFetching, ...rest } = action;
        if (!api) {
            return next(action);
        }
    }
    複製程式碼
  3. 如果有 api 欄位,那麼先傳送一個事件,告訴下游的 Reducer 我先要開始來拿網路資料了嘿嘿,即 isFetching 欄位值為 true

    export default store => next => action => {
        if (typeof action === 'function') {
            return action(store);
        }
    
        const { type, api, isFetching, ...rest } = action;
        if (!api) {
            return next(action);
        }
    
        next({ type, api, isFetching: true, ...rest });
    }
    複製程式碼
  4. 然後就開始進行非同步操作,即網路請求。並且請求成功、請求失敗和請求異常三種情況都會傳送不同的事件給下游的 Reducer

    import fetch from 'isomorphic-fetch';
    import React from "react";
    
    export default store => next => action => {
        if (typeof action === 'function') {
            return action(store);
        }
    
        const { type, api, isFetching, ...rest } = action;
        if (!api) {
            return next(action);
        }
    
        next({ type, api, isFetching: true, ...rest });
    
        fetch(api.url, {
            method: api.method,
        }).then(response => {
            if (response.status !== 200) {
                next({
                    type,
                    api,
                    status: 'error',
                    code: response.status,
                    response: {},
                    isFetching: false,
                    ...rest
                });
            } else {
                response.json()
                    .then(json => {
                        next({
                            type,
                            api,
                            status: 'success',
                            code: 200,
                            response: json.response,
                            isFetching: false,
                            ...rest
                        });
                    })
            }
        }).catch(err => {
            next({ type, api, status, code: 0, response: {}, isFetching: false, msg: err, ...rest });
        });
    }
    複製程式碼

到此為止,一個比較複雜的帶有網路請求的中介軟體就完成了。

封裝元件方便獲取 Store

遺留的問題

還記得上一篇文章我們說到“一個深度為 100 的元件要去改變一個淺層次元件的文案”的例子嗎?我們當時說,只要從深層次的元件裡面傳送一個事件出來就可以了,也就是使用 dispatch 函式來傳送。

emmmm,我們到現在好像還沒遇到過直接在元件裡面 dispatch 事件的情況,我們之前都是在容器元件的 mapDispatchToProps 裡面 dispatch 的。

所以在 UI 元件裡面不能拿到 dispatch 函式?

這裡先說明一點,我們親愛的 dispatch 函式,是存在於 Store 中的,可以用 Store.dispatch 呼叫。有些機靈的同學已經想到,那我們全域性的 Store 引入 UI 元件不就好咯。

哦我親愛的上帝,瞧瞧這個優秀的答案,來,我親愛的湯姆斯·陳獨秀先生,這是你的獎盃...

是的沒錯,這是一種方式,但是我覺得這很不 React Redux。

不給胖虎面子

利用 this.context

在上一篇文章中,我們說到引入了 Provider 元件來講 Store 作用於整個元件樹,那麼是否在每一個元件中都能獲取到 Store 呢?

當然可以,Store 是穿透到整個元件樹裡面的,這個特性依賴於 context 這個玩意,context 具體的介紹可以參看 官方文件

只需要在頂層的元件宣告一些方法就可以實現穿透,這部分工作 Provider 元件內部已經幫我們做好了。

不過在想使用 Store 的元件內部,也要宣告一些東西才能拿到:

import PropTypes from 'prop-types';

export default class DeepLayerComponent extends React.Component {
  
  static contextTypes = {
      store: PropTypes.object
  }

  componentDidMount() {
      this.context.store.dispatch({type: 'DO_SOMETHING'});
  }
  
}
複製程式碼

這裡我們宣告 contextTypesstore 欄位,然後就可以通過 this.context.store 來使用了。

注意,由於 react 庫自帶的 PropTypes 在 15.5 版本之後抽離到 prop-types 庫中,需要自行引入才能使用

封裝

但是如果每個要使用 Store 的元件都這麼搞,不得累死,所以我們考慮做一下封裝,建立一個能通過 this.store 就能拿到全域性 Store 的元件。

import React from "react";
import PropTypes from 'prop-types';

export default class StoreAwareComponent extends React.Component {

    static contextTypes = {
        store: PropTypes.object
    };

    componentWillMount() {
        this.store = this.context.store;
    }

}
複製程式碼

嘿嘿,然後你只要繼承這個元件就可以輕鬆拿到全域性 Store 了。

import React from "react";
import PropTypes from 'prop-types';

export default class DeepLayerComponent extends StoreAwareComponent {

  componentDidMount() {
      this.store.dispatch({type: 'DO_SOMETHING'});
  }
  
}
複製程式碼

這篇我就不作總結了。(逃

React Redux 與胖虎他媽

———

技術上的問題,歡迎討論。

個人部落格:mindjet.github.io

最近在 Github 上維護的專案:

  • LiteWeather [一款用 Kotlin 編寫,基於 MD 風格的輕量天氣 App],對使用 Kotlin 進行實際開發感興趣的同學可以看看,專案中會使用到 Kotlin 的委託機制、擴充套件機制和各種新奇的玩意。
  • Reask [用 React&Flask 開發的全棧專案,前端採用 react-redux]
  • LiteReader [一款基於 MD 的極輕閱讀 App,提供知乎日報、豆瓣電影等資源],專案主要使用了 MVVM 設計模式,介面遵循 Material Design 規範,提供輕量的閱讀體驗。
  • LiveMVVM [Kotlin 編寫的 Android MVVM 框架,基於 android-architecture],輕量 MVVM+Databinding 開發框架。

歡迎 star/fork/follow 提 issue 和 PR。

相關文章