reactnative之react-navigation和redux实践

RN开发一般都会结合一些处理数据流的插件库,如reduxmobxdva等,dva基于 redux和 redux-saga,内置了 react-router和 fetch;mobx使用简单灵活易上手;而redux,很多新手都觉得不仅要写多一些代码,而且集成起来还有点麻烦,其实不然,可能封装或者使用不当,造成滥用redux,导致看起来繁琐。本篇博文从应用实践出发,介绍react-navigation和redux在RN中的使用。

RN主要有两个路由库react-navigationreact-native-router-flux,后者其实也是基于前者进行封装的,但是使用起来更加简单,文档介绍方面肯定就没有react-navigation写的具体了,所以本人建议入门使用react-navigation。

package.json引入以下库:

 "react-native-gesture-handler": "^1.0.12",
 "react-native-reanimated": "^1.13.2",
 "react-navigation": "^4.4.3",
 "react-navigation-drawer": "^2.6.0",
 "react-navigation-redux-helpers": "^4.0.1",
 "react-navigation-stack": "^2.10.2",
 "react-navigation-tabs": "^2.10.1",
 "react-redux": "5.1.1",
 "redux": "^4.0.1",
 "redux-logger": "^3.0.6",
 "redux-thunk": "^2.3.0"

一、react-navigation

1、创建底部Tab

  • createBottomTabNavigator
    底部Tab无非两种,如下图所示:


    tab1.jpg
tab2.jpg

实现Tab1的代码如下:

import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import { createBottomTabNavigator } from 'react-navigation-tabs';
import { createDrawerNavigator } from 'react-navigation-drawer';

const TabNavigator = createBottomTabNavigator({
    Home: {
        screen: HomeScreen,
    },
    Goods: {
        screen: GoodsScreen,
    },
    Message: {
        screen: MessageScreen,
    },
    Mine: {
        screen: MineScreen,
    }
}, {
    defaultNavigationOptions: ({ navigation }) => ({
        tabBarIcon: ({ focused, horizontal, tintColor }) => {
            const { routeName } = navigation.state;
            let icon;
            if (routeName === 'Home') {
                icon = focused ? Images.tab.home_sel : Images.tab.home
            } else if (routeName === 'Goods') {
                icon = focused ? Images.tab.goods_sel : Images.tab.goods
            } else if (routeName === 'Message') {
                icon = focused ? Images.tab.message_sel : Images.tab.message
            } else if (routeName === 'Mine') {
                icon = focused ? Images.tab.mine_sel : Images.tab.mine
            }
            return <Image source={icon} style={{ width: 24, height: 24 }} />
        },
    }),
    tabBarOptions: {
        initialRouteName: 'Home',
        activeTintColor: colors.activeTintColor,
        inactiveTintColor: colors.inactiveTintColor,
    }
});
const AppNavigator = createStackNavigator({
    Main: {
        screen: TabNavigator,
    },
    Login: {
        screen: Login
    },
}, {
    mode: 'modal',
    headerMode: 'none',
});
export default AppContainer = createAppContainer(AppNavigator); 

实现Tab2的效果需要自定义tabBarComponent,代码如下:

const TabNavigator = createBottomTabNavigator({
    Home: {
        screen: HomeScreen,
    },
    Goods: {
        screen: GoodsScreen,
    },
    Message: {
        screen: MessageScreen,
    },
    Mine: {
        screen: MineScreen,
    }
}, {
     tabBarComponent: (props) => (
         <MyCustomTaBar {...props} />
     )
});
  • MyCustomTaBar的实现也很简单,让UI设计一张特殊的背景图即可,代码如下:
import React, { Component } from 'react';
import { View, Text, ImageBackground, Image, StyleSheet } from 'react-native';
import { TouchableOpacity, TouchableWithoutFeedback } from 'react-native-gesture-handler';

import { colors } from '../../common/theme/color';
import { Images } from '../../image';
export default class MyCustomTaBar extends Component {

    render() {
        // console.log(JSON.stringify(this.props));
        const { state } = this.props.navigation
        state.routes.forEach((e, index) => {
            if (state.index == index) {
                e.focused = true
            } else {
                e.focused = false
            }
        });
        return (
            <View>
                <ImageBackground
                    style={{ width: SCREEN_WIDTH, height: 56, backgroundColor: 'transparent' }}
                    source={Images.tab.foot}>
                    <View style={{ flex: 1, flexDirection: 'row', backgroundColor: 'transparent' }}>
                        {
                            state.routes.length > 0 && state.routes.map((item, index) => {
                                return <Item {...this.props} key={index}
                                    routeName={item.routeName} focused={item.focused} />
                            })
                        }
                    </View>
                </ImageBackground>
            </View>
        );
    }
}
const Item = class extends Component {
    getIcon = () => {
        const { routeName, focused } = this.props;
        let icon;
        if (routeName === 'Home') {
            icon = focused ? Images.tab.home_sel : Images.tab.home
        } else if (routeName === 'Goods') {
            icon = focused ? Images.tab.goods_sel : Images.tab.goods
        } else if (routeName === 'Message') {
            icon = focused ? Images.tab.message_sel : Images.tab.message
        } else if (routeName === 'Mine') {
            icon = focused ? Images.tab.mine_sel : Images.tab.mine
        }
        return icon
    }
    getName = () => {
        const { routeName, focused } = this.props;
        let name;
        if (routeName === 'Home') {
            name = '首页'
        } else if (routeName === 'Goods') {
            name = '好货'
        } else if (routeName === 'Message') {
            name = '消息'
        } else if (routeName === 'Mine') {
            name = '我的'
        }
        return name
    }
    gotoRoute = (routeName) => {
        this.props.navigation.navigate(routeName)
    }
    render() {
        const { routeName, focused } = this.props;
        if (routeName == 'Goods') {
            return (<TouchableWithoutFeedback
                onPress={() => { this.gotoRoute(routeName) }}
                style={{
                    // flex: 1,
                    height: 100,
                    width: SCREEN_WIDTH / 3,
                    justifyContent: 'center',
                    alignItems: 'center',
                    top: -30,
                    backgroundColor: 'transparent'
                }}>
                <View style={{ bottom: 10, }}>
                    <View style={{
                        width: 50, height: 50, borderRadius: 25,
                        backgroundColor: colors.theme
                    }}></View>
                    {/* <Image source={this.getIcon()} style={{ width: 40, height: 40 }} /> */}
                </View>
                <View>
                    <Text style={focused ? styles.activeTintColor : styles.inactiveTintColor}>{this.getName()}</Text>
                </View>
            </TouchableWithoutFeedback>)
        }
        return (
            <TouchableOpacity
                onPress={() => { this.gotoRoute(routeName) }}
                style={{
                    flex: 1,
                    width: SCREEN_WIDTH / 3,
                    justifyContent: 'center',
                    alignItems: 'center',
                }}>
                <Image source={this.getIcon()} style={{ width: 20, height: 20 }} />
                <Text style={focused ? styles.activeTintColor : styles.inactiveTintColor}>{this.getName()}</Text>
            </TouchableOpacity>
        )
    }
}
const styles = StyleSheet.create({
    activeTintColor: {
        color: colors.activeTintColor,
        fontSize: 12,
    },
    inactiveTintColor: {
        color: colors.inactiveTintColor,
        fontSize: 12
    }
});

2、创建抽屉

  • createDrawerNavigator
    抽屉组件的使用无非就是开启关闭:

this.props.navigation.openDrawer()
this.props.navigation.closeDrawer()

集成代码如下所示:

const DrawerNavigator = createDrawerNavigator({
    Main: {
        screen: AppNavigator,
    },
    drawerA: {
        screen: DrawerScreen
    },
    drawerB: {
        screen: DrawerBScreen
    },
}, {
    order: ['Main', 'drawerA', 'drawerB'],//定义抽屉项目的顺序
    initialRouteName: 'Main',
    drawerType: 'front',
    drawerLockMode: 'unlocked',//是否响应手势
    drawerWidth: 250, //抽屉的宽度
    drawerPosition: 'left', //选项是left或right
    useNativeAnimations: true, //启用原生动画
    drawerBackgroundColor: colors.theme, //抽屉背景颜色
    contentComponent: (props) => (<DrawerBScreen {...props} />)
});
export default AppContainer = createAppContainer(DrawerNavigator);

DrawerBScreen组件是你自定义的页面。
有个不足之处就是react-navigation自带的抽屉组件不支持手势返回,所以建议使用react-native-drawer-layout,效果还不错。

react-navigation的使用就点到为止,本文不是为了介绍每个api的使用,旨在阐述一些常见的应用场景,建议新手过一遍官方文档。

二、redux

redux有三大原则:

  • 单一数据源:整个应用的state统一放在一个store中
  • State 是只读的:只能通过派发action来改变state
  • 使用纯函数来执行修改:Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。
    官方示例代码可以去看下:redux示例代码,这里只展示redux在RN中的应用。

在RN中集成redux,步骤如下:

  • createStore--创建一个store
import {
    createStore,
    applyMiddleware
} from 'redux';
import thunk from "redux-thunk"

import {
    createReactNavigationReduxMiddleware,
} from 'react-navigation-redux-helpers';
import appReducer from './reducers/index'

const middleware = createReactNavigationReduxMiddleware(
    state => state.nav,
    'root'
);
const middlewares = [
    middleware,
    thunk
]

const store = createStore(
    appReducer,
    applyMiddleware(...middlewares),
);

export default store
  • 封装appReducer,所有reducer统一放在这里
import { combineReducers } from 'redux';
import {navReducer} from './navReducer'
import {login} from './loginReducer'
const appReducer = combineReducers({
    nav:navReducer,
    login:login
});
export default appReducer
  • 创建navReducer,存放路由相关数据
import {
    createNavigationReducer,
} from 'react-navigation-redux-helpers';
import  AppContainer  from '../../router/index' //这里的AppContainer就是上面展示的路由相关配置的代码

export const navReducer = createNavigationReducer(AppContainer);

  • 以loginReducer为例子阐述reducer的整个流程:
    1、新建actionTypes
//登录相关action
export const LOGINING = 'LOGINING'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_ERROR = 'LOGIN_ERROR'
export const LOGOUT = 'LOGOUT'

2、新建要派发的action函数

import * as actionType from '../actionsTypes/index'
import { LoginInfo } from '../../redux/reducers/loginReducer'

export function login(name, psw) {
    // console.log(name, psw);
    return dispatch => {
        //登录中
        dispatch(logining())
        fetch('https://www.baidu.com/', 'get')
            .then(res => {
                dispatch(loginSuccess({
                    name, psw
                }))
            })
            .catch(e => {
                dispatch(loginFail())
            })
    }
}
export function logining() {
    return {
        type: actionType.LOGINING
    }
}

export function loginSuccess(userInfo) {
    return {
        type: actionType.LOGIN_SUCCESS,
        state: userInfo
    }
}

export function loginFail() {
    return {
        type: actionType.LOGIN_ERROR
    }
}

export function loginOut() {
    return {
        type: actionType.LOGOUT
    }
}

3、reducer改变state并返回

import * as type from '../actionsTypes/index'

export const LoginInfo = {
    status: "未登录",
    isLogin: false,
    user: {},
};
export const login = function (state = LoginInfo, action) {
    switch (action.type) {
        case type.LOGINING:
            return {
                ...state,
                status: "登录中",
                isLogin: false,
            };
        case type.LOGIN_SUCCESS:
            return {
                ...state,
                status: "登陆成功",
                isLogin: true,
                user: action.state
            };
        case type.LOGIN_ERROR:
            return {
                ...state,
                status: "登录失败",
                isLogin: false,
                user: {}
            };
        case type.LOGOUT:
            return {
                ...state,
                status: "未登录",
                isLogin: false,
                user: {}
            };
        default:
            return state;
    }
}

4、最后就是在你的页面中通过connect来访问reducer

const mapStateToProps = (state) => ({
    nav: state.nav,
    status: state.login.status,
    user: state.login.user
})

const mapDispatchToProps = dispatch => ({
    login: (name, psd) => dispatch(actions.login(name, psd)),
    loginOut: () => dispatch(actions.loginOut())
});

export default connect(mapStateToProps, mapDispatchToProps)(Login)

不知道大家有没有注意到一个写代码的小技巧,如果你是用VSCode开发react,在新建一个react组件的时候,敲打rcredux会索引很快敲出一个react包含redux的组件出来,很省事。当然还有很多类似这个生成代码的,vue也有。有点类似.vue文件下,输入vbase可以快速生成模板代码。

三、react-navigation和redux双剑合璧

有了以上基础,react-navigation和redux实现双剑合璧就容易多了,实现代码如下:

import React, { Component } from 'react';
import { StatusBar, BackHandler, ToastAndroid } from 'react-native';
import { Provider, connect } from 'react-redux'
import store from './redux/index'
import AppContainer from './router/index'
import { NavigationActions } from 'react-navigation';
import {createReduxContainer} from 'react-navigation-redux-helpers'

const AppWithRedux=createReduxContainer(AppContainer,'root')
const mapStateToProps = (state) => ({
    state: state.nav,
  });
const AppWithNavigationState = connect(mapStateToProps)(AppWithRedux)
export default class App extends Component {
    constructor(props) {
        super(props)
        this.lastBackPressed = null
    }

    componentDidMount() {
        BackHandler.addEventListener("hardwareBackPress", this.onBackPress);

    }
    componentWillUnmount() {
        BackHandler.removeEventListener("hardwareBackPress", this.onBackPress);
    }

    onBackPress = () => {
        // alert(JSON.stringify(store.getState()))
        if (store.getState().nav.index !== 0) {
            store.dispatch(NavigationActions.back());
            return true
        }

        //退出应用
        if (this.lastBackPressed && this.lastBackPressed + 2000 >= Date.now()) {
            //最近2秒内按过back键,可以退出应用。
            return false;
        }

        this.lastBackPressed = Date.now();
        ToastAndroid.show('再按一次退出应用', ToastAndroid.SHORT);
        return true;

    };
    render() {
        return (
            <Provider store={store}>
                <AppWithNavigationState />
            </Provider>
        )
    }
}

  • 提供了一个Provider组件,最外层传入store,这样以下的所有子组件都可以拿到reducer的state,原理就是react的context
  • 需要注意这里的key--'root',要跟上面的配置store的key一致:

const AppWithRedux=createReduxContainer(AppContainer,'root')

const middleware = createReactNavigationReduxMiddleware(
state => state.nav,
'root'
);

  • 安卓还要写一个返回物理键返回监听,控制是否退出应用和返回页面

如果你还是嫌弃redux麻烦,那么mobx也是较好的选择。可以参考下面的demo进行配置mobx:
https://github.com/vonovak/react-navigation-mst-demo

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

推荐阅读更多精彩内容