zoukankan      html  css  js  c++  java
  • 在 React Native 中使用 Redux 架构

    前言

    Redux 架构是 Flux 架构的一个变形,相对于 Flux,Redux 的复杂性相对较低,而且最为巧妙的是 React 应用可以看成由一个根组件连接着许多大大小小的组件的应用,Redux 也只有一个 Store,而且只用一个 state 树来管理组件的状态。随着应用逐渐变得复杂,React 将组件看成状态机的优势仿佛变成了自身的绊脚石。因为要管理的状态将会越来越多,直到你搞不清楚某个状态在不知道什么时候,由于什么原因,发生了什么变化。Redux 试图让状态的变化变得可预测。Redux 的官方文档

    如果你厌倦了官网的 ToDoList 例子,以及 React Native 上少之又少的关于应用 Redux的参考(遗憾的是,仍然是 ToDoList 案例)。本文将结合作者的亲身经历,帮助那些初次接触 Redux,并且需要将 Redux 架构应用到自己的 React Native 应用的开发者避开一些坑(反正我是被坑了T_T)。

    本文将和开发者一起学习如何将 Redux 架构应用到 React Native 应用上,并且使用jmessage-sdk 初步构建一个聊天应用。我们先来看看最终效果吧:

     
     

    本例的源码地址

    开始

    在开始使用 Redux 架构之前,我们先来捋一捋 action,reducer,以及 store 这三者的概念,以及他们之间的关系。尽管 Redux 文档已经写得相对清晰,但对于初学者来说还是不(you)太(dian)友(meng)好(bi)的。我们先来看一张图,大致了解一下它们之间的关系:

     
     

    Action: 把数据从应用传到 store 的有效载荷

    上面是官方给出的解释,但这个解释比较抽象,这么说吧,如果你想要获取一个数据,比如说一张图片,那么就可以定义一个 action 来完成这个动作。当你执行这个动作的时候,你就可以 dispatch 一个正在获取的 action,这时 Reducer 就能够根据这个动作来返回一个 state,这时候 UI 能够根据这个状态来做一些事情(比如你在拉图片时肯定不希望 UI 就这样卡住,可能要显示进度条之类的)。如果数据返回了,就可以再次dispatch 一个 action,然后 Reducer 就会返回新的 state。下面我们来看看例子:

    actions/conversationActions.js

    export function addFriend(username) {
        return dispatch => {
            //先直接 dispatch 一个类型为 adding 的 action
            type: types.ADDING_FRIEND,
            JMessageHelper.addFriend(username, (result) => {
                dispatch ({
                //数据返回后再 dispatch 一个 action,并且也返回了这个数据
                    type: types.ADD_FRIEND_SUCCESS,
                    conversation: JSON.parse(result)
                });
            })
        }
    }
    

    上面的代码就是一个 action 创建函数。根据传过来的 username,dispatch 一个正在添加好友的 action,这样 Reducer 就可以根据这个 action 返回一个正在添加的 state。当数据返回后,再 dispatch 一个添加成功的 action,并且同时返回数据。这里没有 dispatch 一个添加失败的 action,因为在 Native 中 catch住了,严格来说,是要处理请求失败的响应。在官网中的例子是使用了 fetch 方法,但本例是使用了 jmessage-sdk 来请求数据,因此是个混合的 React Native 应用。

    Middleware

    Middleware 其实是 action 抽象出来的,是对 action 的进一步封装,用来完成异步 API调用等其它事情。本例中没有使用 middleware。

    Reducer:根据一个 action 来返回一个 new state 以更新 state

    在上面的例子中,我们 dispatch 的每一个 action 都会被 reducer 捕捉到。reducer 可以根据 action 的t ype 来返回一个新的 state。在 Redux 架构中,所有的 state 都保存在一个单一的对象中。这个对象会随着应用的复杂而变得越来越庞大,state 的结构也会变得更加复杂。这个时候就需要拆分 reducer,使得每个 reducer 只负责改变一小部分 state。来看看例子:

    reducers/conversationReducer.js

    export default function conversationList(state, action) {
        state = state || {
            type: types.INITIAL_CONVERSATION_LIST,
            dataSource: [],
            fetching: true,
            adding: true,
            error: false,
        }
    
        switch(action.type) {
            //正在添加
            case types.ADDING_FRIEND:
                return {
                    //使用扩展运算符返回继承之前的状态,如果不更新状态的话这样写就行了,
                    //在 Component 中通过 this.props.state 就可以得到整个 state,在 Component 中可以看到具体使用场景
                    ...state,
                    //在这里也返回了 action,这样可以在 Component 通过 this.props.action 直接调用某个 action
                    ...action,
                    adding: true
                }
                break;
            //添加成功
            case types.ADD_FRIEND_SUCCESS:
                var convList = [...state.convList];
                convList.unshift(action.conversation);
                dataSource = state.dataSource.cloneWithRows(convList);
                console.log('add success convList: ' + convList.toString());
                return {
                    ...state,
                    ...action,
                    convList: convList,
                    dataSource,
                    adding: false
                }
            default:
                return {
                    ...state
                }
        }
    }
    

    这是一个 reducer 函数,根据 action.type 来返回 new state,reducer 应该是一个纯函数,这里不允许对数据做额外的处理:

    • 修改传入参数;
    • 执行有副作用的操作,如API请求和路由跳转;
    • 调用非纯函数,如 Date.now()或 Math.random()等。

    reducer 就是一个函数,接收旧的 state 和 action,返回新的 state:

    (previousState, action) => newState

    改变了 state 后,就会触发 Component 的 render 方法,重新进行渲染。

    Store:把 Action 和 Reducer 联系到一起的对象

    上面是官方的描述,简而言之,可以理解为 store 负责绑定 action 和 reducer。一个 Redux 应用只用一个 store。store 有以下职责:

    • 维持应用的 state;
    • 提供 getState() 方法获取 state;
    • 提供 dispatch(action) 方法更新 state;
    • 通过 subscribe(listener) 注册监听器;
    • 通过 subscribe(listener) 返回的函数注销监听器。

    来看下例子:

    store/configureStore.js

    import { createStore, applyMiddleware } from 'redux';
    import thunk from 'redux-thunk';
    import rootReducer from '../reducers/index';
    //使用 Redux 提供的 createStore 方法即可,thunk 提供了异步功能有兴趣的可以了解一下,这里不准备深入
    const createStoreWithMiddleware = applyMiddleware(
        thunk
    ) (createStore);
    
    //initialState 可以设置初始状态,可以用于把服务器端生成的 state 转变后传给应用
    export default function configureStore(initialState) {
        const store = createStoreWithMiddleware(rootReducer, initialState);
    
        if (module.hot) {
            module.hot.accept('../reducers', () => {
                const nextReducer = require('../reducers');
                store.replaceReducer(nextReducer);
            });
        }
    
        return store;
    }
    ```
    需要注意的是上面的 import 的 reducers/index 是对 reducer 的合并,后面会提到。上面的代码是 store 的一种写法,可以支持热更新。关于热更新,目前笔者尚未研究(hahaha)。
    
    ##配置
    现在我们已经知道 action,reducer 以及 store 的基本概念和作用了,接下来就要把它们连接起来。先来看看我们的目录结构:
    
    ```
    |---index.android.js
    |---react-native-android
            |---actions
            |---containers
            |---reducers
            |---store
    ```
    
    先来看看JS入口 index.android.js:
    >index.android.js
    
    ```
    'use strict';
    
    import React from 'react-native';
    import ReactJChat from './react-native-android/containers/ReactJChat';
    var MyAwesomeApp = React.createClass({
      
        render() {
          return (
            <ReactJChat />
          );
        }
    });
    
    React.AppRegistry.registerComponent('JChatApp', () => MyAwesomeApp);
    
    ```
    可以看到这里仅仅是把入口交给了 React JChat。这里再次提醒一下,尽量使用 ES6 的语法 import,而不是 require,否则会出现莫名其妙的错误(T_T)。接下来是ReactJChat.js。
    >containers/ReactJChat.js
    
    ```
    
    import React, { Component } from 'react-native';
    import { Provider } from 'react-redux';
    import BaseApp from './BaseApp';
    import configureStore from '../store/configureStore';
    
    const store = configureStore();
    
    export default class ReactJChat extends Component {
        render() {
            return (
                <Provider store = { store }>
                    <BaseApp />
                </Provider>
            );
        }
     }
    ```
    这里首先通过 configureStore()得到 store,然后通过 Provider 把 store 传给真正的入口 BaseApp。
    >containers/BaseApp.js
    
    ```
    class BaseApp extends Component {
        renderScene(route, navigator) {
            _navigator = navigator;
            let Component = route.component;
            const { state, dispatch } = this.props;
            const action = bindActionCreators(actions, dispatch);
            return <Component 
                        //将 state,params,actions,navigator 通过属性传递到 Component
                        {...route.params}
                        state = { state }
                        actions = { action }
                        navigator = { navigator }
                    />
        }
    
        render() {
            return (
                <Navigator
                    initialRoute = { this.initialRoute }
                    configureScene = { this.configureScene }
                    renderScene = { this.renderScene }
                />
                
            );
        }
    }
    function mapSateToProps(state) {
        return {
            state: state
        }
    }
    
    
    export default connect(mapSateToProps) (BaseApp)
    ```
    这里我们主要看一下 renderScene() 这个函数,const { state, dispatch } = this.props;这句通过 this.props 可以从 store 中得到 state 及 dispatch,然后通过 bindActionCreators() 将我们定义的所有 actions 通过 dispatch 关联到 state,这样reducer 就能够接收 dispatch 的 action,然后返回新的 state 了。在 renderScene() 的返回中,将 state、action 及 navigator 都作为属性传递到 Component 了。接下来还定义了一个函数 mapStateToProps(),这个方法即是将 state 作为 props 可以在所传递的 Component 中通过 this.props 得到 state,也就是包含所有数据的对象。最后通过 react-redux 提供的 connect 方法,将使得 BaseApp 可以通过 this.props 得到 state 。到此为止,我们已经完成了连接了 action、reducer 以及 store。接下来就可以在 Component 中发起从 Component->Action->Reducer->(Store->)Component 的数据流了。
    
    ##使用
    如果你完成了上面的步骤,我们就可以在 Component 中发起一个 action 来发动数据流。在本例中,我们从一个会话列表开始讲解数据是如何流动的。先来看一下我们的会话列表片段:
    >containers/conv_fragment.js
    
    ```
    render() {
            //这里通过 this.props.state 得到 conversationReducer 对象,之所以能得到,是因为在 main_activity 中将
           //state 作为属性传过来了
            const { conversationReducer } = this.props.state;
            //通过 conversationReducer 得到 convList 数组
            _convList = conversationReducer.convList;
            var content = conversationReducer.dataSource.length === 0 ?
                <View style = { styles.container }>
                { conversationReducer.fetching && <View style = { {alignItems: 'center', justifyContent: 'center'} }>
                        <Text style = { {fontSize: 24, }}>
                            正在加载...
                        </Text>
                    </View> } 
                </View> :
                <ListView style = { styles.listView }
                    ref = 'listView'
                    //通过得到 conversationReducer 的 dataSource 作为 ListView 的 dataSource
                    dataSource = { conversationReducer.dataSource }
                    renderHeader = { this.renderHeader }
                    renderRow = { this.renderRow }
                    enableEmptySections = { true }
                    keyboardDismissMode="on-drag"
                    keyboardShouldPersistTaps={ true }
                    showsVerticalScrollIndicator={ false }/>;
    
    ```
    以上我们通过 reduer 对象,来获得渲染 ListView 所需的数据。而 reducer 又是根据 action 来返回 state (也就是数据)的,这样以来我们只要发起一个 action 就可以使得数据流通起来。
    
    - **定义 Action**
    在发起一个 action 之前,我们先来想象一下,这个 action 的内容。一个用户登录后,将希望看到自己的会话列表,这个时候我们需要先拉取一下本地的会话列表记录。这样一来,我们的 action 就诞生了(这里我通过一个例子希望大家理解,把需求转换为具体的 action 的过程)。来看一下具体的代码:
    ```
    export function loadConversations() {
        return dispatch => {
            //这里我在另一个文件中声明了 type,也可直接在 action 中声明
            type: types.INITIAL_CONVERSATION_LIST,
            JMessageHelper.getConvList((result) => {
                dispatch({
                    //返回数据后,定义一个 action 类型用来在 reducer 中进行相关操作
                    type: types.LOAD_CONVERSATIONS,
                    convList: JSON.parse(result),
                });
            }, () => {
                dispatch({
                    type: types.LOAD_ERROR,
                    convList: []
                })
            });
        }
    }
    ```
    上面是一个获取本地所有会话列表的 action 声明函数。在开始获取数据之前,直接 dispatch 了一个 action,即 INITIAL_CONVERSATION_LIST,在数据返回后,又 dispatch 了一个成功和一个失败的 action。接着我们在 reducer 中来实现一下捕获这几个 action 的函数:
    
    - **定义 reducer**
    >reducers/conversationReducer.js
    
    ```
    export default function conversationList(state, action) {
        //如果 state == 上一个 state,或者为 INITIAL_CONVERSATION_LIST 的 action,那么返回空的数据以及一些 boolean 值
        state = state || {
            type: types.INITIAL_CONVERSATION_LIST,
            dataSource: [],
            fetching: true,
            adding: true,
            error: false,
        }
    
        switch(action.type) {
            case types.LOAD_CONVERSATIONS:
                var dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
                var convList = action.convList;
                dataSource = dataSource.cloneWithRows(convList);
                return {
                    ...state,
                    ...action,
                    convList: convList,
                    dataSource,
                    fetching: false,
                }
            case types.LOAD_ERROR:
                var dataSource = action.convList;
                return {
                    ...state,
                    ...action,
                    dataSource,
                }
              ...
    ```
    上面声明了一个名为 conversationList 的 reducer 函数,根据参数 (state, action) 来返回一个新的 state。switch 语句中根据 action 的类型分别进行了处理。当 action 被 dispatch 后,与之绑定的 reducer 函数就会执行。可以看到,上面 action 的类型为 LOAD_CONVERSATIONS 时,表明得到了所有本地会话列表的数据。这样 reducer 要做的就非常明确了:把 action 中的数据取出来,返回这个数据就行了。再次强调, reducer 中不能够出现复杂的操作,以及非纯函数的调用。由于我们的 ListView 要展示的数据来源于 dataSource,这样我们就直接声明了 dataSource,然后把数据放进去。至于返回时,要带哪些参数,可以取决于自己的需求。注意,这里的 ...state 以及 ...action,这是为了在返回新的 state 后,仍然可以在 Component 中通过 this.props 得到 state 以及 action,否则会出现 undefined 的错误。
    
    - **发起 action**
    其实人的惯性思维是先发起 action,然后再去考虑 action 里面需要什么内容,然后是 reducer 应该怎样处理 action,并返回 state。这里我之所以没有按照这种顺序,是因为,站在编程的角度上来说,我从一个功能需求出发,把需求转化成 action,然后再去决定在一个恰当的时候发起这个 action。这两种想法并没有褒贬之意,大家见仁见智,希望起到一个抛砖引玉的作用。我们回到会话列表界面:
    >containers/conv_fragment.js
    
    ```
     componentDidMount() {
            //得到 loadConversations 这个 action
            const { loadConversations } = this.props.actions;
            const { conversationList } = this.props.state;
            //发起 action
            loadConversations();
    }
    ```
    这里我在组件的生命周期函数 componentDidMount 中发起了拉取本地所有会话列表的 action,因为 componentDidMount 是在组件 render 以后马上会执行的函数。通过 this.props 得到之前在 conversationActions 中定义的 loadConversations 函数就可以通过直接调用这个函数来发起 action 了。这样一来,一旦数据返回,就会执行 action->reducer 这个流程,由于在 reducer 中返回的 state 改变了,就会触发 render 重新对界面进行渲染,会话列表也就能够在 ListView 中展示了。
    
    ###注意事项
    如果你的界面使用了 Fragment(比如本例),在使用 Redux 架构时,一定要注意将 state 或者 action 作为属性传递到 Component 中,这样才能在 Component 中通过 this.props 得到 state 及 action。
    >containers/main_activity.js
    
    ```
          pages.push(
                <View key = { 0 } style = { pageStyle } collapsable = { true }>
                    <Conv
                        //将 state,action 传到 Conv Component
                        state = { this.props.state }
                        actions = { this.props.actions }
                        navigator = { this.props.navigator }
                    />
                </View>
            );
    ```
    另外,还有关于 actions 及 reducers 的合并,可以参考本例源码中的 actions/index.js 以及 reducers/index.js。在做好这些后,就可以尽情使用 Redux 了(就跟做填空题一样)。还有一点,在 Redux 应用中,所有的 state 都放在一个单一的对象也就是 state 树中,官网建议,UI 相关的 state 与数据相关的 state 尽量分开,即 UI 相关的 state 不要放在 state 树里面。
    
    ###关于一些坑
    - **ListView**
    接下来我来说一下关于 ListView 的坑点吧。ListView 中的数据来源于 dataSource,dataSource 的一般写法是这样的:
    ```
    var dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
    ```
    rowHasChanged 的作用是当 r1 和 r2 **引用不同**时,就会刷新 ListView。这是一个坑点,只是数据不同是不能刷新 ListView 的。下面用一个删除会话作为示例:
    >reducers/conversationReducer.js
    
    ```
            case types.DELETE_CONVERSATION:
                var selected = action.selected;
                var convList = [...state.convList];
                convList.splice(selected, 1);
                var newList = new Array();
                newList = convList;
                dataSource = state.dataSource.cloneWithRows(newList);
                return {
                    ...state,
                    ...action,
                    convList: newList,
                    dataSource
                }
    ```
    这是 reducer 中的一段代码,action 的类型为 DELETE_CONVERSATION 时,从 dataSource 中删除一个选中的元素,并返回。这里先在 convList 中删除掉被选中的会话,然后通过 var newList = new Array(); 新建的数据来复制数据。这其实是一种效率不高的做法,可以[参考 Immutable JS](https://facebook.github.io/immutable-js/)。
    
    - **Image**
    官方的 Image 组件功能还是蛮强大的,要显示网络图片的话提供一个 uri 就可以自动显示图片;如果要显示 drawable 文件夹下的图片,直接在 uri 中填写图片的名字就行了,如:
    ```
    <Image 
        style = { styles.titlebarImage }
        source = { {uri: 'msg_titlebar_right_btn'}}
    />
    ```
    如果要显示在 sd 卡中的图片的话,可以在 uri 中加 file:/// 路径就可以显示,如:
    
    ```
    <Image 
        style = { styles.titlebarImage }
        source = { {uri: 'file:///data/jchat/images/head_icon.png'}}
    />
    
    ```
    但是如果要显示应用包名下的图片(放在 sd 卡有可能被其他应用修改),那就要费一些周折了。这里本例的做法是把图片在 native 中转成 base64 字符串(都是小头像),然后传到 JS。Image 控件也是支持 base64 显示的,不过要加上前缀:
    
    ```
    //**注意加上前缀**
    avatar = "data:image/png;base64," + getBinaryData(avatarFile);
    
    private String getBinaryData(File file) {    
      try {        
        FileInputStream fis = new FileInputStream(file);        
        byte[] byteArray = new byte[fis.available()];        
        fis.read(byteArray);        
        fis.close();        
        byte [] encode = Base64.encode(byteArray, Base64.DEFAULT);        
        return new String(encode);    
      } catch (Exception e) {        
        e.printStackTrace();        
        return "head_icon";    
      }
    }
    ```
    
    - **Modal**
    由于 React Native 中无法自定义 dialog(目前只有 AlertDialog),所以使用了 Modal 来替代 dialog,可以[参考这个](https://github.com/magicismight/react-native-root-modal)。但是 Modal 实际上是 Root View 的一个 sibling View,也就是说 Modal 会始终悬浮在最前面。尽管 Modal 有这个“缺陷”,但是为一个 Modal 添加各种动画在 React Native 中是非常容易的。
    >containers/conv_fragment.js
    
    ```
        showDropDownMenu() {
            if (!this.state.showAddFriendDialog && !this.state.showDelConvDialog) {
                if (this.state.showDropDownMenu) {
                    this.dismissDropDownMenu();
                } else {
                    this.state.y.setValue(-600);
                    this.state.scaleAnimation.setValue(1);
                    //spring 是一个弹跳物理模型,这里让y的值从 -600 减到 0,就是从上面掉下来的动画
                    Animated.spring(this.state.y, {
                        toValue: 0
                    }).start();
                    this.setState({ showDropDownMenu: true });
                }
            }
        }
    
        dismissDropDownMenu() {
            Animated.timing(this.state.y, {
                toValue: -600
            }).start(() => {
                this.setState({ showDropDownMenu: false});
            });
        }
    
              <Animated.Modal
                  style = { [styles.dropDownMenu, {transform: [{translateY: this.state.y}, {scale: this.state.scaleAnimation}]}] }
                  visible = { this.state.showDropDownMenu }>
              </Animated.Modal>
    ```
    
    ###后记
    React Native JChat 目前尚未完善,之后会慢慢实现。如果你想对 [React Native JChat] (https://github.com/jpush/jmessage-react-plugin)提一些改进,欢迎 fork,并提交 PR。有什么问题可与我联系:caiyaoguan@gmail.com


    作者:KenChoi
    链接:https://www.jianshu.com/p/4139babc6d5e
    來源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    引用原文:https://www.jianshu.com/p/4139babc6d5e

    写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,文章可以转载,无需版权。希望尽自己的努力,做到更好,大家一起努力进步!

    如果有什么问题,欢迎大家一起探讨,代码如有问题,欢迎各位大神指正!

  • 相关阅读:
    随机数表示方法
    何时用重定向何时用转发
    http中重定向和请求转发
    Java正则表达式
    自定义圆形的ProgressBar
    Android内存管理机制
    Android 安全机制
    8位颜色值的含义
    Shape使用
    Bitmap(三)
  • 原文地址:https://www.cnblogs.com/summary-2017/p/8626469.html
Copyright © 2011-2022 走看看