React Native: Redux 工程化實(shí)踐

Redux 中文文檔 在此. 如果想找具體可操作的案例, 文檔里面都有. 文末有彩蛋.

為什么會(huì)有 Redux ?


在 iOS 中, 隨著項(xiàng)目迭代, 功能越來越復(fù)雜, 如果還是采用 MVC 架構(gòu), 由于 Controller 內(nèi)部的職責(zé)太多, 而導(dǎo)致代碼塊耦合嚴(yán)重, 不利于測(cè)試和維護(hù), 由此, MVVM 應(yīng)運(yùn)而生.

MVVM 架構(gòu)中, 通過將表現(xiàn)邏輯和交互邏輯移到 view-model 中, 借助 RxSwift 等響應(yīng)式編程的框架, controller 監(jiān)聽 view-model 中的 view state 的變更, 而做出對(duì)應(yīng)的操作, 比如修改 view.

協(xié)調(diào)器持有對(duì) model 層的引用, 并且了解 view controller 樹的結(jié)構(gòu), 這樣, 它能夠?yàn)槊總€(gè)場(chǎng)景的 view-model 提供所需要的 model 對(duì)象. 如果不增加協(xié)調(diào)器, 那么 view controller 間就會(huì)有耦合.
實(shí)際項(xiàng)目中是否引入?yún)f(xié)調(diào)器, 得看具體情況. 如果是針對(duì)不是那么復(fù)雜的功能做重構(gòu), 太復(fù)雜的架構(gòu)反而是畫蛇添足.


回到 React Native 項(xiàng)目, 如果 JavaScript 單頁應(yīng)用功能越來越復(fù)雜, 我們同樣要處理功能模塊解耦, 更細(xì)一點(diǎn), 處理各種變化的 state. 這些 state 可能包括服務(wù)器響應(yīng)數(shù)據(jù), 緩存數(shù)據(jù), 也包括 UI 狀態(tài), 如被選中的標(biāo)簽, 是否顯示加載動(dòng)效或者分頁器等等.

所以我們選擇了 Redux.

Redux 是什么 ?


ReduxJavaScrip 狀態(tài)容器, 提供可預(yù)測(cè)化的 state 管理.

Redux 中, 這些 state , 也可以稱之為 model 數(shù)據(jù).
通過 action(交互邏輯, 顯示邏輯), 更改不同的 state, 最后顯示在界面上.

在下面代碼中, POPULAR_REFRESHPOPULAR_REFRESH_SUCCESS 代表兩種 action , 對(duì)于不同的 action, 內(nèi)部需要傳遞的 state 數(shù)據(jù)也不同, 最終傳遞到 JavaScript 頁面, 映射到 props 中, 做最后的處理.

case Types.POPULAR_REFRESH:   //下拉刷新中
    return {
        ...state, 
        [action.storeName]: {    // storeName 是類似于 java, ios等這些tab, 它是動(dòng)態(tài)的
            ...state[action.storeName],
            refreshState: 1,
        }
    };
case Types.POPULAR_REFRESH_SUCCESS:   //下拉刷新成功
    return {
        ...state, 
        [action.storeName]: {
            ...state[action.storeName],
            items: action.items, //原始數(shù)據(jù)
            projectModels: action.projectModels,  // 此次要展示的數(shù)據(jù)
            refreshState: 0,    // 默認(rèn)
            pageIndex: action.pageIndex
        }
    };

Redux 的工作流程

Redux 的工作流程

    1. 用戶操作View, 通過dispatch方法, 發(fā)出 Action.
    • Action 可以是網(wǎng)絡(luò)請(qǐng)求, 交互邏輯等.
    1. Store 自動(dòng)調(diào)用 Reducer, 并且傳入兩個(gè)參數(shù)(當(dāng)前 State 和收到的 Action ), Reducer 會(huì)返回新的 State.
    • 如果有 Middleware, Store 會(huì)將當(dāng)前 State 和收到的 Action 傳遞給 Middleware, Middleware 會(huì)調(diào)用 Reducer 然后返回新的 State.
    1. State 一旦有變化, Store 就會(huì)調(diào)用監(jiān)聽函數(shù), 更新 View.

在整個(gè)流程中, 數(shù)據(jù)都是單向流動(dòng)的.

Redux 的三原則
  1. Redux 應(yīng)用中所有的 state 都以一個(gè)對(duì)象樹的形式存儲(chǔ)在一個(gè) 單一store 中.
  2. state只讀 的: 唯一改變 state 的辦法是觸發(fā) action, action 是一個(gè)描述發(fā)生什么的對(duì)象.
  3. 使用純函數(shù)來執(zhí)行修改: 為了描述 action 如何改變 state 樹, 你需要編寫 reducers.
    reducer 是形式為 (state, action) => state 的純函數(shù). 根據(jù) action 修改 state, 將其轉(zhuǎn)變?yōu)橄乱粋€(gè) state.

Redux 在 React Native 中的應(yīng)用


準(zhǔn)備工作

根據(jù)需要, 安裝以下組件.

  • redux(必選).
  • react-redux(必選): redux 作者開發(fā)的一個(gè)在 React 上使用的 redux 庫.
  • redux-devtools(可選): Redux 開發(fā)者工具, 支持熱加載, action 重放, 自定義 UI 等功能.
  • redux-thunk(可選): 實(shí)現(xiàn) action 異步的 middleware.
  • redux-persist(可選): 支持 store 本地持久化.
  • redux-observable(可選): 實(shí)現(xiàn)可取消的 action.

安裝方式

yarn add redux react-redux redux-devtools

react-redux 介紹

react-reduxRedux 官方提供的 React 綁定庫.

有幾個(gè)位置需要注意:

  • <Provider> 組件: 這個(gè)組件需要包裹在整個(gè)組件樹的最外層(根組件). 讓所有的子組件都能使用 connect() 方法綁定 store.
  • connect(): 這是 react-redux 提供的一個(gè)方法, 如果一個(gè)組件想要響應(yīng)狀態(tài)的變化, 就需要把自己作為參數(shù)傳給 connect() 的結(jié)果, connect() 方法會(huì)處理與 store 綁定的細(xì)節(jié), 并通過 selector 確定該綁定 store 的哪一部分的數(shù)據(jù).
  • selector: 這是我們自定義的函數(shù), 這個(gè)函數(shù)聲明了你的組件需要整個(gè) store 中的哪一部份數(shù)據(jù)作為自己的 props.
  • dispatch: 每當(dāng)需要改變應(yīng)用中的 state, 都需要 dispatch 一個(gè) action.

使用步驟

1. 創(chuàng)建 action

定義 action 類型

// 各種 action 類型
export const THEME_CHANGE = 'THEME_CHANGE'
export const POPULAR_REFRESH = 'POPULAR_REFRESH'

// 各種 action 類型
export default {
    THEME_CHANGE: "THEME_CHANGE", 
    POPULAR_REFRESH: "POPULAR_REFRESH"
}

創(chuàng)建 action 函數(shù)

import Types from '../types';

export function onThemeChange(theme) {
    // 同步 action
    // return {   
    //     type: Types.THEME_CHANGE,
    //     theme: theme,
    // }

    // 異步 action  需要引入 'redux-thunk'
    return dispatch => {
        dispatch({
            type: Types.THEME_CHANGE,
            theme: theme,
        })
    };
}

注意:

  • 這里我們傳入了一個(gè)參數(shù) theme, 是我們將要修改的主題樣式.
  • action 既可以同步實(shí)現(xiàn), 也可以異步實(shí)現(xiàn). 對(duì)于網(wǎng)絡(luò)請(qǐng)求, 數(shù)據(jù)庫加載等應(yīng)用場(chǎng)景, 我們必須使用異步 action,
  • 異步 action 可以理解為, 在 action 內(nèi)部進(jìn)行異步操作, 等操作返回后, 在 dispatch 一個(gè) action.
  • 為了使用異步 action, 我們需要引入 redux-thunk 庫. 將異步中間件添加到 store 中.
import thunk from 'redux-thunk'

const middlewares = [
    thunk,
    middleware2,
    middleware3,
];

export default createStore(reducers, applyMiddleware(...middlewares));
  • 默認(rèn)情況下, createStore() 所創(chuàng)建的 Redux store 沒有使用 middleware, 所以只支持同步數(shù)據(jù)流.

  • 我們可以使用 applyMiddleware() 來增強(qiáng) createStore(), 添加 thunk 這類中間件來實(shí)現(xiàn)異步 action.

  • redux-thunkredux-promise 這類支持異步的 moddleware 都包裝了 storedispatch() 方法. 因此我們可以 dispatch 一些除了 action 以外的內(nèi)容. 例如函數(shù)或者 Promise.

  • 注意: 當(dāng) middleware 鏈中的最后一個(gè) moddleware 開始 dispatch action 時(shí), 這個(gè) action 必須是一個(gè)普通對(duì)象.

2. 創(chuàng)建 reducers

reducer 是根據(jù) action 類型 修改 state, 將其轉(zhuǎn)變成下一個(gè) state. 這里面根據(jù)實(shí)際的需要, 定義了各種不同的 state 樹.

import Types from '../../action/types';

const defaultState = {
    theme: 'red'
}

export default function onAction(state=defaultState, action) {
    switch (action.type) {
        case Types.THEME_CHANGE:
            return {
                ...state,
                theme: action.theme,
            }
        default:
            return state;
    }
}

注意

  • reducer 是一個(gè)純函數(shù), 他僅僅用于返回下一個(gè) state, 為了保證 reducer 盡可能簡(jiǎn)單, 我們不能在這里面改變 state, 只能在 action 創(chuàng)建函數(shù) 內(nèi)部做.
  • reducer 內(nèi)部也不要調(diào)用非純函數(shù), Date.now()Math.random() 這種.
  • 在默認(rèn)的情況下, 要返回舊的 state. 以應(yīng)對(duì)未知 action 的情況.
  • 對(duì)于獨(dú)立 page 的 reducer, 我們應(yīng)該針對(duì)各個(gè)頁面進(jìn)行拆分, 以免 action 太多. 導(dǎo)致不容易維護(hù). 拆分完我們需要合并進(jìn)行使用.
import {combineReducers} from 'redux';

import theme from './theme';
import popular from './popular';

// 合并 reducer
const index = combineReducers({
    themeReducer: theme,
    popularReducer: popular,
})

export default index;
  • combineReducers() 所做的只是生成一個(gè)函數(shù), 這個(gè)函數(shù)來調(diào)用你的一系列 reducer, 每個(gè) reducer 根據(jù)他們的 key 來篩選出 state 中的一部分?jǐn)?shù)據(jù)并處理, 然后這個(gè)生成的函數(shù)再將所有 reducer 的結(jié)果合并成一個(gè)大的對(duì)象. 如果combineReducers() 所包含的所有 reducers 都沒有更改 state, 那么就不會(huì)創(chuàng)建一個(gè)新的對(duì)象.

3. 使用 store

Store 是 存儲(chǔ) state 的容器.

  • 它會(huì)把兩個(gè)參數(shù)(當(dāng)前的 state 樹 和 action) 傳入 reducer
  • reducer 會(huì)把新的 state 返回給 store,
  • store 更新 state 到 view 中.

store 里有幾個(gè)方法

  • 提供 getState() 方法獲取 state.
  • 提供 dispatch(action) 方法更新 state.
  • 通過 subscribe(listener) 注冊(cè)監(jiān)聽器.
  • 通過 subscribe(listener) 返回的函數(shù), 注銷監(jiān)聽器.

配置 store

import {createStore, applyMiddleware} from 'redux';
import reducers from '../reducer/reducer';
import thunk from 'redux-thunk'

const logger = store => next => action => {
    if (typeof action === 'function') {
        console.log('dispatching a function')
    } else {
        console.log('dispatching', action)
    }
    console.log('nextState', store.getState());
};

const middlewares = [
    logger,  // 打印 state 信息
    thunk,   // 提供異步 action
];

// 在 store 中添加中間件, 配置 reducer
export default createStore(reducers, applyMiddleware(...middlewares));

使用 store, 我們首先要引入 react-redux 庫, 在 App 的根組件, 通過 <Provider/> 配置 store.

import {Provider} from 'react-redux';
import store from './js/store';

export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <AppNavigators />
      </Provider>
    )
  }
}

3. 在組件中應(yīng)用 Redux

訂閱 state, dispatch

import {connect} from 'react-redux';
import actions from '../action/index';

class HomePage extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Button 
          title='改變主題'
          onPress={()=> {
            this.props.onThemeChangeProp('orange')
          }}
        />
      </View>
    )
  }
}

const mapStateToProps = state => ({
  themeProp: state.themeReducer.theme
});

const mapDispatchToProps = dispatch => ({
  onThemeChangeProp: theme => dispatch(actions.onThemeChange(theme)),
});

export default connect(mapStateToProps, mapDispatchToProps)(HomePage);

在上述代碼中, 我們訂閱了 store 中的 theme state 和 dispatch,

  • 我們通過 react-redux 提供的 connect() 方法, 將 store 中的目標(biāo) state, 和處理該 state 的 action 所在的 selector 傳入其中.
  • 并且將目標(biāo)組件傳入 connect() 的結(jié)果中, 使得目標(biāo)組件響應(yīng) state 的變化.
  • 這樣該組件就可以通過 props 取出對(duì)應(yīng)的state, 和操作 action.


react-navigation + Redux

如果你是通過 react-navigation 做 App 的基礎(chǔ)架構(gòu), Redux 也對(duì)其做了支持.
我們需要導(dǎo)入 react-navigation-redux-helpers 庫.

1. 配置 navigator

import {
    createAppContainer, createStackNavigator, createSwitchNavigator
}
from 'react-navigation';

import HomePage from '../page/HomePage';

import { connect } from "react-redux";
import {
    createReactNavigationReduxMiddleware, 
    createReduxContainer
}
from  "react-navigation-redux-helpers"

export const rootCom = 'HomePage';  // 設(shè)置根路由

const MainNavigator = createStackNavigator({
    HomePage: {
        screen: HomePage,
    }, 
}, {
    initialRouteName: rootCom
});

export const RootNavigator = createAppContainer(MainNavigator);

/**
 * 1. 初始化 react-navigation 與 redux 的中間件
 * 該方法的一個(gè)很大的作用是為 reduxifyNavigator 的 key 設(shè)置 actionSubscribers (行為訂閱者)
 */
export const middleware = createReactNavigationReduxMiddleware(
    state => state.nav,
);

/**
 * 2. 將導(dǎo)航器傳遞給 reduxifyNavigator 函數(shù)
 * 并返回一個(gè)將 navigation state 和 dispatch 函數(shù)作為 props 的新組件
 * 注意: 要在 createReactNavigationReduxMiddleware 之后執(zhí)行
 */
const AppWithNavigationState = createReduxContainer(RootNavigator);

/**
 * State 和 Props 的映射關(guān)系
 */
const mapStateToProps = state => ({
    state: state.nav  
})

/**
 * 連接 React 組件 與 Redux store
 */
export default connect(mapStateToProps)(AppWithNavigationState);

2. 配置 reducer

import {combineReducers} from 'redux';
import {createNavigationReducer} from 'react-navigation-redux-helpers'

import {RootNavigator} from '../navigator/AppNavigators';

import theme from './theme';
import popular from './popular';

// 1. 創(chuàng)建自己的 navigation reducer
const navReducer = createNavigationReducer(RootNavigator)

// 2. 合并 reducer
const index = combineReducers({
    nav: navReducer,
    themeReducer: theme,    // 子 reducer
    popularReducer: popular,// 子 reducer
})

export default index;

3. 使用 navigator

import {Provider} from 'react-redux';
import store from './js/store';

import AppNavigators from './js/navigator/AppNavigators';

export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <AppNavigators />
      </Provider>
    )
  }
}

至此, Redux 的基本使用都已經(jīng)介紹完了.

Tips

  • Redux 應(yīng)用只有一個(gè) store, 當(dāng)需要拆分?jǐn)?shù)據(jù), 處理邏輯時(shí), 應(yīng)該使用 reducer 組合.
  • redux 有一個(gè)特點(diǎn), 狀態(tài)共享, 所有的狀態(tài)都放在一個(gè) store 中, 任何 component 都可以訂閱 store 中的 state 數(shù)據(jù).
  • 并不是所有的 state 都適合放在 store 中, 這樣會(huì)使 store 越來越大. 如果某個(gè) state 只被一個(gè)組件使用, 不存在狀態(tài)共享, 可以不放在 store 中.
  • 如果你的項(xiàng)目不追求極致的條理, 可以不使用 Redux, 就好像 iOS 中再大的項(xiàng)目, MVC 這種架構(gòu)都是可以應(yīng)對(duì), 并且有針對(duì)其架構(gòu)的方案, 而不是使用 MVVM, 新的架構(gòu)是有學(xué)習(xí)成本的.
  • 具體適不適合你自己的項(xiàng)目, 自己掂量.

enjoy :).

參考

如何理解 redux 的流程
react native redux 指南
官方中文文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末菩收,一起剝皮案震驚了整個(gè)濱河市雷则,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)隔节,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寂呛,“玉大人怎诫,你說我怎么就攤上這事〈荆” “怎么了幻妓?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)肉津。 經(jīng)常有香客問我偶洋,道長(zhǎng),這世上最難降的妖魔是什么距糖? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮悍引,結(jié)果婚禮上哆料,老公的妹妹穿的比我還像新娘唬渗。我一直安慰自己嫉鲸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布升略。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪额嘿。 梳的紋絲不亂的頭發(fā)上球拦,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天蟀悦,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的且叁。 我是一名探鬼主播未妹,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼看尼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起盟步,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎躏结,沒想到半個(gè)月后却盘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡媳拴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年黄橘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屈溉。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡塞关,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出子巾,到底是詐尸還是另有隱情帆赢,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布线梗,位于F島的核電站椰于,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏仪搔。R本人自食惡果不足惜瘾婿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧偏陪,春花似錦抢呆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至揪罕,卻和暖如春梯码,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背好啰。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工轩娶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人框往。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓鳄抒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親椰弊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子许溅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容