React Native: Redux 工程化实践

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

为什么会有 Redux ?


在 iOS 中, 随着项目迭代, 功能越来越复杂, 如果还是采用 MVC 架构, 由于 Controller 内部的职责太多, 而导致代码块耦合严重, 不利于测试和维护, 由此, MVVM 应运而生.

MVVM 架构中, 通过将表现逻辑和交互逻辑移到 view-model 中, 借助 RxSwift 等响应式编程的框架, controller 监听 view-model 中的 view state 的变更, 而做出对应的操作, 比如修改 view.

协调器持有对 model 层的引用, 并且了解 view controller 树的结构, 这样, 它能够为每个场景的 view-model 提供所需要的 model 对象. 如果不增加协调器, 那么 view controller 间就会有耦合.
实际项目中是否引入协调器, 得看具体情况. 如果是针对不是那么复杂的功能做重构, 太复杂的架构反而是画蛇添足.


回到 React Native 项目, 如果 JavaScript 单页应用功能越来越复杂, 我们同样要处理功能模块解耦, 更细一点, 处理各种变化的 state. 这些 state 可能包括服务器响应数据, 缓存数据, 也包括 UI 状态, 如被选中的标签, 是否显示加载动效或者分页器等等.

所以我们选择了 Redux.

Redux 是什么 ?


ReduxJavaScrip 状态容器, 提供可预测化的 state 管理.

Redux 中, 这些 state , 也可以称之为 model 数据.
通过 action(交互逻辑, 显示逻辑), 更改不同的 state, 最后显示在界面上.

在下面代码中, POPULAR_REFRESHPOPULAR_REFRESH_SUCCESS 代表两种 action , 对于不同的 action, 内部需要传递的 state 数据也不同, 最终传递到 JavaScript 页面, 映射到 props 中, 做最后的处理.

case Types.POPULAR_REFRESH:   //下拉刷新中
    return {
        ...state, 
        [action.storeName]: {    // storeName 是类似于 java, ios等这些tab, 它是动态的
            ...state[action.storeName],
            refreshState: 1,
        }
    };
case Types.POPULAR_REFRESH_SUCCESS:   //下拉刷新成功
    return {
        ...state, 
        [action.storeName]: {
            ...state[action.storeName],
            items: action.items, //原始数据
            projectModels: action.projectModels,  // 此次要展示的数据
            refreshState: 0,    // 默认
            pageIndex: action.pageIndex
        }
    };

Redux 的工作流程

Redux 的工作流程

    1. 用户操作View, 通过dispatch方法, 发出 Action.
    • Action 可以是网络请求, 交互逻辑等.
    1. Store 自动调用 Reducer, 并且传入两个参数(当前 State 和收到的 Action ), Reducer 会返回新的 State.
    • 如果有 Middleware, Store 会将当前 State 和收到的 Action 传递给 Middleware, Middleware 会调用 Reducer 然后返回新的 State.
    1. State 一旦有变化, Store 就会调用监听函数, 更新 View.

在整个流程中, 数据都是单向流动的.

Redux 的三原则
  1. Redux 应用中所有的 state 都以一个对象树的形式存储在一个 单一store 中.
  2. state只读 的: 唯一改变 state 的办法是触发 action, action 是一个描述发生什么的对象.
  3. 使用纯函数来执行修改: 为了描述 action 如何改变 state 树, 你需要编写 reducers.
    reducer 是形式为 (state, action) => state 的纯函数. 根据 action 修改 state, 将其转变为下一个 state.

Redux 在 React Native 中的应用


准备工作

根据需要, 安装以下组件.

  • redux(必选).
  • react-redux(必选): redux 作者开发的一个在 React 上使用的 redux 库.
  • redux-devtools(可选): Redux 开发者工具, 支持热加载, action 重放, 自定义 UI 等功能.
  • redux-thunk(可选): 实现 action 异步的 middleware.
  • redux-persist(可选): 支持 store 本地持久化.
  • redux-observable(可选): 实现可取消的 action.

安装方式

yarn add redux react-redux redux-devtools

react-redux 介绍

react-reduxRedux 官方提供的 React 绑定库.

有几个位置需要注意:

  • <Provider> 组件: 这个组件需要包裹在整个组件树的最外层(根组件). 让所有的子组件都能使用 connect() 方法绑定 store.
  • connect(): 这是 react-redux 提供的一个方法, 如果一个组件想要响应状态的变化, 就需要把自己作为参数传给 connect() 的结果, connect() 方法会处理与 store 绑定的细节, 并通过 selector 确定该绑定 store 的哪一部分的数据.
  • selector: 这是我们自定义的函数, 这个函数声明了你的组件需要整个 store 中的哪一部份数据作为自己的 props.
  • dispatch: 每当需要改变应用中的 state, 都需要 dispatch 一个 action.

使用步骤

1. 创建 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"
}

创建 action 函数

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,
        })
    };
}

注意:

  • 这里我们传入了一个参数 theme, 是我们将要修改的主题样式.
  • action 既可以同步实现, 也可以异步实现. 对于网络请求, 数据库加载等应用场景, 我们必须使用异步 action,
  • 异步 action 可以理解为, 在 action 内部进行异步操作, 等操作返回后, 在 dispatch 一个 action.
  • 为了使用异步 action, 我们需要引入 redux-thunk 库. 将异步中间件添加到 store 中.
import thunk from 'redux-thunk'

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

export default createStore(reducers, applyMiddleware(...middlewares));
  • 默认情况下, createStore() 所创建的 Redux store 没有使用 middleware, 所以只支持同步数据流.

  • 我们可以使用 applyMiddleware() 来增强 createStore(), 添加 thunk 这类中间件来实现异步 action.

  • redux-thunkredux-promise 这类支持异步的 moddleware 都包装了 storedispatch() 方法. 因此我们可以 dispatch 一些除了 action 以外的内容. 例如函数或者 Promise.

  • 注意: 当 middleware 链中的最后一个 moddleware 开始 dispatch action 时, 这个 action 必须是一个普通对象.

2. 创建 reducers

reducer 是根据 action 类型 修改 state, 将其转变成下一个 state. 这里面根据实际的需要, 定义了各种不同的 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 是一个纯函数, 他仅仅用于返回下一个 state, 为了保证 reducer 尽可能简单, 我们不能在这里面改变 state, 只能在 action 创建函数 内部做.
  • reducer 内部也不要调用非纯函数, Date.now()Math.random() 这种.
  • 在默认的情况下, 要返回旧的 state. 以应对未知 action 的情况.
  • 对于独立 page 的 reducer, 我们应该针对各个页面进行拆分, 以免 action 太多. 导致不容易维护. 拆分完我们需要合并进行使用.
import {combineReducers} from 'redux';

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

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

export default index;
  • combineReducers() 所做的只是生成一个函数, 这个函数来调用你的一系列 reducer, 每个 reducer 根据他们的 key 来筛选出 state 中的一部分数据并处理, 然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象. 如果combineReducers() 所包含的所有 reducers 都没有更改 state, 那么就不会创建一个新的对象.

3. 使用 store

Store 是 存储 state 的容器.

  • 它会把两个参数(当前的 state 树 和 action) 传入 reducer
  • reducer 会把新的 state 返回给 store,
  • store 更新 state 到 view 中.

store 里有几个方法

  • 提供 getState() 方法获取 state.
  • 提供 dispatch(action) 方法更新 state.
  • 通过 subscribe(listener) 注册监听器.
  • 通过 subscribe(listener) 返回的函数, 注销监听器.

配置 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. 在组件中应用 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 中的目标 state, 和处理该 state 的 action 所在的 selector 传入其中.
  • 并且将目标组件传入 connect() 的结果中, 使得目标组件响应 state 的变化.
  • 这样该组件就可以通过 props 取出对应的state, 和操作 action.


react-navigation + Redux

如果你是通过 react-navigation 做 App 的基础架构, Redux 也对其做了支持.
我们需要导入 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';  // 设置根路由

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

export const RootNavigator = createAppContainer(MainNavigator);

/**
 * 1. 初始化 react-navigation 与 redux 的中间件
 * 该方法的一个很大的作用是为 reduxifyNavigator 的 key 设置 actionSubscribers (行为订阅者)
 */
export const middleware = createReactNavigationReduxMiddleware(
    state => state.nav,
);

/**
 * 2. 将导航器传递给 reduxifyNavigator 函数
 * 并返回一个将 navigation state 和 dispatch 函数作为 props 的新组件
 * 注意: 要在 createReactNavigationReduxMiddleware 之后执行
 */
const AppWithNavigationState = createReduxContainer(RootNavigator);

/**
 * State 和 Props 的映射关系
 */
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. 创建自己的 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 的基本使用都已经介绍完了.

Tips

  • Redux 应用只有一个 store, 当需要拆分数据, 处理逻辑时, 应该使用 reducer 组合.
  • redux 有一个特点, 状态共享, 所有的状态都放在一个 store 中, 任何 component 都可以订阅 store 中的 state 数据.
  • 并不是所有的 state 都适合放在 store 中, 这样会使 store 越来越大. 如果某个 state 只被一个组件使用, 不存在状态共享, 可以不放在 store 中.
  • 如果你的项目不追求极致的条理, 可以不使用 Redux, 就好像 iOS 中再大的项目, MVC 这种架构都是可以应对, 并且有针对其架构的方案, 而不是使用 MVVM, 新的架构是有学习成本的.
  • 具体适不适合你自己的项目, 自己掂量.

enjoy :).

参考

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容