redux与react-navigation的集成

作者: 你好啊憨憨米 | 来源:发表于2017-05-21 09:51 被阅读6044次

    1. 闲扯一发

    作为一个技术小宅男,周末还是适合待在家里,总结总结在前端路上遇到过的各种技术。今天呢,咱接着上周的话题,继续扯扯react-native相关的事情。在react-native的开发中,感觉最难管理,同时也最需要管理的就是各种state和router了。截止目前为止,Github上最火的state管理库非redux莫属,至于路由管理的话,还是喜欢官方推荐的由社区开发和维护的react-navigation,接下来呢,咱就一起来扯扯他俩的集成。

    2. redux

    redux是什么,简单点来说呢,就是一个管理state的库,但咱不能这么随便啊,还是把官网的定义给搬过来吧。
    Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。不仅于此,它还提供 超爽的开发体验,Redux 除了和 React 一起用外,还支持其它界面库。它体小精悍(只有2kB,包括依赖)。
    。。。

    2.1 redux的三项基本原则
    • 单一数据源:整个应用的state被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个store。
    • State 是只读的:惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
    • 使用纯函数来执行修改:为了描述 action 如何改变 state tree ,需要编写 reducer。
    2.2 Action

    action 是把数据从应用传到store的有效载荷,是store数据的唯一来源。action为一个普通对象,这里建议按照Flux 标准 Action来定义。action 一般具有下面四个属性

    • type: 用户动作的唯一标识符
    • payload: action相关的数据,一般来自于后端接口,用于更新store,在出错的情况下为错误对象
    • error 出错的情况下为true, 其他情况不需要提供
    • meta 可选参数,其他可能需要的额外数据,不算作payload的一部分
    // 正常
    {
      type: 'ADD_TODO',
      payload: {
        text: 'Do something.'  
      },
      meta: {
         searchParms: {}
      }
    }
    //出错
    {
      type: 'ADD_TODO',
      error: true,
      payload: new Error('error object'),
      meta: {
         searchParms: {}
      }
    }
    
    2.3 Action Creator

    Action Creator就是生成 action 的方法,一般将action creator 的执行结果传递给 dispatch. 为了便于管理,这里建议使用第三方库redux-actions. 该库采用了Flux 标准的action定义。

    import { createAction } from 'redux-actions';
    
    let increment = createAction('INCREMENT', amount => amount);
    // same as
    increment = createAction('INCREMENT');
    
    expect(increment(42)).to.deep.equal({
      type: 'INCREMENT',
      payload: 42
    });
    
    2.4 Reducer

    Action只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。而这正是 reducer 要做的事情。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。这里呢,我还是建议采用redux-actions 来生成 reducer.

    import { handleAction } from 'redux-actions';
    
    var reducer = handleAction('FETCH_DATA', {
      next(state, action) {...},
      throw(state, action) {...}
    }, defaultState);
    
    2.5 Store

    Store 就是把action和reducer联系到一起的对象。Store 有以下职责:

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

    创建store的时候,我们使用 combineReducers 将多个 reducer 合并成为一个,并传递给 createStore 方法。

    2.6 数据流

    1 用户交互触发actionCreator的执行生成action
    2 调用store的dispatch方法,触发reducer的执行
    3 根据传入的action调用对应的reducer,更新store
    4 触发组件props的改变从而引起组件的更新

    redux的具体细节, 可以参考开发文档

    3. React Navigation

    React Navigation 是react native社区主推的一个导航器方案,Facebook官方建议使用其来进行导航的维护。
    React Navigation的路由写法使其非常容易扩展导航逻辑,或是整合到redux中。由于路由可以嵌套使用,因而开发者可以根据不同页面编写不同的导航逻辑,且彼此互不影响。
    具体的细节,这里我就不做特别的阐述了,大家可以直接阅读官方文档

    4 redux与react-navigation的集成

    前面我们已经介绍了redux和react-navigation的基本知识,接下来,就进入我们今天的主题,实现redux与react-navigation的集成。

    4.1 效果预览

    戳我查看效果哦。。。

    4.2 整体目录

    在项目根目录下建立src文件夹用于存放各种scripts和styles代码。

    • scripts 存放和应用相关的js代码
    • less 存放less文件
    • styles 根据less文件自动生成,可以参考react native样式分离

    scripts目录具体细节如下

    • actions 用于存放redux的action creator 和 action type
    • reducers 用于存放所有的reducer
    • store 用于存放redux中的store
    • components 所有的业务组件都躲在这儿,哈哈
    • routers 和导航相关的路由配置
    • Root.js 应用的根组件
    4.3 Actions

    在actions目录下建立两个文件夹actionTypes和news。actionTypes用于存放action的type, news用于存放和news相关的action creator。

    //actionTypes
    export const FETCH_NEWS_LIST = 'FETCH_NEWS_LIST';
    
    //news action creator
    import {
        FETCH_NEWS_LIST
    } from '../actionTypes';
    
    import { createAction } from 'redux-actions';
    
    const thumbnail = 'https://facebook.github.io/react/img/logo_og.png';
    // 获取news 列表数据
    export var fetchNewsList = createAction(FETCH_NEWS_LIST, () => {
        return [1,2,3,4,5,6,7,8,9,10].map(item => {
            return {
                id: item,
                title: `[${item}]夏季又要到,做好防脱准备很重要`,
                thumbnail: thumbnail,
                desc: '室内干燥,室外气温高,春夏季对于需要带状的上班族来说就是压力山大。'
            }
        });
    });
    
    4.4 Reducer

    在reducers目录下建立一个news文件夹,用于存放和news相关的reducer。

    /**
    * 这里以news的为例,还有其他的reducer,具体的可以参考样例完整代码
    */
    import {
        FETCH_NEWS_LIST
    } from '../../actions/actionTypes';
    
    import { handleActions } from 'redux-actions';
    
    export default handleActions({
        [FETCH_NEWS_LIST]: {
            next(state, action) {
                return { ret: true, data: action.payload };
            },
            throw(state, action) {
                return { ret: false, statusText: action.payload, data: [] };
            }
        }
    }, { ret: true, statusText: '', data: [] });
    
    

    接下来在reducers目录下建立一个index.js文件,用于组合各种reducer

    import { combineReducers } from 'redux';
    
    import news from './news';
    import categries from './category';
    
    //和导航相关的reducer通过从调用出传递进来
    export default function getReducers(navReducer) {
        return combineReducers({
            news,
            categries,
            nav: navReducer
        });
    }
    
    4.5 Store

    在store文件夹下建立一个index.js文件,用于存放store相关的数据

    import { createStore, applyMiddleware } from 'redux';
    import promiseMiddleware from 'redux-promise';
    
    import getReducers from '../reducers';
    //promiseMiddleware 是异步action的一个中间件,本例子中暂时没有使用
    export default function getStore(navReducer) {
        return createStore(
            getReducers(navReducer),
            undefined,
            applyMiddleware(promiseMiddleware)
        );
    }
    
    4.6 components

    在components目录下建立Home文件夹,用于存放首页的业务组件,除了首页之外,还提供以下业务组件。

    • Category 分类组件
    • Card 购物车组件
    • UserCenter 用户中心
    • NewsDetail 新闻详情

    接下来,我们看看Home的代码

    import React, { Component } from 'react';
    import Icon from 'react-native-vector-icons/Ionicons';
    import {
        View,
        Text,
        Image,
        FlatList,
        StatusBar,
        TouchableHighlight
    } from 'react-native';
    import { common, topicCard } from '../../../styles';
    import { connect } from "react-redux";
    import { bindActionCreators } from "redux";
    import * as newsActions from '../../actions/news';
    
    class Home extends Component {
        //定义底部tabBar的icon和name
        static navigationOptions = {
            tabBarLabel: '首页',
            tabBarIcon: ({ tintColor }) => (
                <Icon name="md-home" size={24} color={tintColor} />
            )
        };
        componentDidMount() {
            this.props.fetchNewsList();
        }
        _onPressItem = (id) => {
            this.props.navigation.navigate('NewsDetail', { id: id });
        };
        _keyExtractor = (item, index) => item.id;
        _renderItem = ({item}) => (
            <View style={topicCard.card}>
                <Image style={topicCard.cover} source={{uri: item.thumbnail}}></Image>
                <TouchableHighlight
                    style={topicCard.title}
                    onPress={this._onPressItem.bind(this, item.id)}>
                    <Text numberOfLines={1} style={topicCard.titleText}>{item.title}</Text>
                </TouchableHighlight>
                <View style={topicCard.desc}>
                    <Text numberOfLines={2} style={topicCard.descText}>{item.desc}</Text>
                </View>
            </View>
        );
        render() {
            return (
                <View style={[common.containerFixHeight, common.springWoodBg]}>
                    <FlatList
                        data={this.props.news.data}
                        refreshing={false}
                        keyExtractor={this._keyExtractor}
                        renderItem={this._renderItem}
                      />
                </View>
            );
        }
    }
    //让业务组件和redux建立关联
    export default connect(
        state => ({
            news: state.news
        }),
        dispatch => bindActionCreators(newsActions, dispatch)
    )(Home);
    
    

    在代码的结尾处,我们调用了react-redux的connect方法,让store, action 和 业务组件建立了关联,之后我们就可以在业务组件中通过this.props.news的方式来访问news相关的数据,以this.props.fetchNewsList() 的方式来触发请求news数据。

    4.7 路由配置

    和路由相关的配置,存放在routers文件夹里

    import {
        TabNavigator,
        StackNavigator,
        addNavigationHelpers
    } from "react-navigation";
    
    import Home from '../components/Home';
    import Category from '../components/Category';
    import Card from '../components/Card';
    import UserCenter from '../components/UserCenter';
    import NewsDetail from '../components/NewsDetail';
    
    //底部的tabBar导航
    const TabbarNavigator = TabNavigator({
        Home: { screen: Home },
        Category: { screen: Category },
        Card: { screen: Card },
        UserCenter: { screen: UserCenter }
    }, {
        initialRouteName: 'Home'
    });
    //整个应用的路由栈
    const AppNavigator = StackNavigator({
        TabBar: {
            screen: TabbarNavigator,
            navigationOptions: {
                header: null
            }
        },
        NewsDetail: {
            path: 'news/:id',
            screen: NewsDetail
        }
    });
    
    export {
        AppNavigator
    };
    
    
    4.8 根组件

    在上面的一切准备就绪之后,我们需要建立一个Root组件来实现redux,react-navigation以及业务组件的衔接。

    import React, { Component } from "react";
    import { Provider, connect } from "react-redux";
    import { addNavigationHelpers } from "react-navigation";
    
    import getStore from "./store";
    import { AppNavigator } from './routers';
    
    const navReducer = (state, action) => {
        const newState = AppNavigator.router.getStateForAction(action, state);
        return newState || state;
    };
    
    const mapStateToProps = (state) => ({
        nav: state.nav
    });
    
    class App extends Component {
        render() {
            return (
                <AppNavigator
                    navigation={addNavigationHelpers({
                        dispatch: this.props.dispatch,
                        state: this.props.nav
                    })}
                />
            );
        }
    }
    
    const AppWithNavigationState = connect(mapStateToProps)(App);
    
    const store = getStore(navReducer);
    
    export default function Root() {
        return (
            <Provider store={store}>
                <AppWithNavigationState />
            </Provider>
        );
    }
    

    接下来,我们只需要将根组件引入到整个应用的入口文件就ok啦,完美。

    
    import {
      AppRegistry,
    } from 'react-native';
    
    import Root from './src/scripts/Root.js';
    
    AppRegistry.registerComponent('reduxReactNavigation', () => Root);
    

    5. 总结一发

    至此,所有的介绍结束了,感谢你的耐心阅读,如果你想要了解更多,你可以直接查看完整的demo,也可以留言提issue。

    相关文章

      网友评论

      • boyrt:redux和react-navigation集成的目的是什么呢?作者能给我解答下吗?(我自己有写过demo,路由是react-navigation,项目中也使用了redux,但是没有像你这样做集成,所以集成的目的我不是很清楚,望解答)
        5bcd4bc7dc98:同问,用过很多router框架,项目中也用redux,问题是也没这么集成过,不知道有啥好处?
      • 一言不合拔萝卜:demo 跑不了
      • 听一半的歌_f558:楼主,跑不起来啊
      • 蜗壳美如画:先赞一个
      • 奔向大牛:朋友,你不断点击跳转出多个子页面后,回到主页的动画会一层一层推出,看起来感觉很尴尬,你有遇到这个问题吗,有解决这个问题的方法没有呢?
      • yuzhiyi_宇:可以请问一下redux+react-navigation,如何把监听安卓返回键加进去,实现按返回键返回键回到上个界面,可以贴一下代码吗
        ZYK的轮子:在组件外定义 let lastBackPressed = null;
        ZYK的轮子:@est7 用这个componentWillMount(){
        BackHandler.addEventListener('hardwareBackPress', this.onBackAndroid);
        }
        componentUnWillMount(){
        BackHandler.removeEventListener('hardwareBackPress', this.onBackAndroid);
        lastBackPressed = null;
        }
        onBackAndroid=()=>{
        console.log(this.props.navigation.state)
        if(this.props.navigation.state&&this.props.navigation.state.routeName&&this.props.navigation.state.routeName != "Index") {
        this.props.navigation.goBack();
        return true;
        }
        if (lastBackPressed && lastBackPressed + 2000 >= Date.now()) {
        return false;
        }
        lastBackPressed = Date.now();
        ToastAndroid.show('再点击一次退出应用',ToastAndroid.SHORT);
        return true;
        }
        李简书:我也遇到了这个问题,请问你解决了吗?
      • uuuuuuw:demo跑不了。github上面的

      本文标题:redux与react-navigation的集成

      本文链接:https://www.haomeiwen.com/subject/jrdxxxtx.html