reactnative之react-navigation和redux實(shí)踐

RN開發(fā)一般都會(huì)結(jié)合一些處理數(shù)據(jù)流的插件庫,如redux豌鹤、mobxdva等默怨,dva基于 redux和 redux-saga,內(nèi)置了 react-router和 fetch骤素;mobx使用簡單靈活易上手匙睹;而redux,很多新手都覺得不僅要寫多一些代碼济竹,而且集成起來還有點(diǎn)麻煩痕檬,其實(shí)不然,可能封裝或者使用不當(dāng)送浊,造成濫用redux梦谜,導(dǎo)致看起來繁瑣。本篇博文從應(yīng)用實(shí)踐出發(fā)袭景,介紹react-navigation和redux在RN中的使用唁桩。

RN主要有兩個(gè)路由庫react-navigationreact-native-router-flux耸棒,后者其實(shí)也是基于前者進(jìn)行封裝的荒澡,但是使用起來更加簡單,文檔介紹方面肯定就沒有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奈籽、創(chuàng)建底部Tab

  • createBottomTabNavigator
    底部Tab無非兩種饥侵,如下圖所示:


    tab1.jpg
tab2.jpg

實(shí)現(xiàn)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); 

實(shí)現(xiàn)Tab2的效果需要自定義tabBarComponent,代碼如下:

const TabNavigator = createBottomTabNavigator({
    Home: {
        screen: HomeScreen,
    },
    Goods: {
        screen: GoodsScreen,
    },
    Message: {
        screen: MessageScreen,
    },
    Mine: {
        screen: MineScreen,
    }
}, {
     tabBarComponent: (props) => (
         <MyCustomTaBar {...props} />
     )
});
  • MyCustomTaBar的實(shí)現(xiàn)也很簡單衣屏,讓UI設(shè)計(jì)一張?zhí)厥獾谋尘皥D即可躏升,代碼如下:
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、創(chuàng)建抽屜

  • createDrawerNavigator
    抽屜組件的使用無非就是開啟關(guān)閉:

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

集成代碼如下所示:

const DrawerNavigator = createDrawerNavigator({
    Main: {
        screen: AppNavigator,
    },
    drawerA: {
        screen: DrawerScreen
    },
    drawerB: {
        screen: DrawerBScreen
    },
}, {
    order: ['Main', 'drawerA', 'drawerB'],//定義抽屜項(xiàng)目的順序
    initialRouteName: 'Main',
    drawerType: 'front',
    drawerLockMode: 'unlocked',//是否響應(yīng)手勢
    drawerWidth: 250, //抽屜的寬度
    drawerPosition: 'left', //選項(xiàng)是left或right
    useNativeAnimations: true, //啟用原生動(dòng)畫
    drawerBackgroundColor: colors.theme, //抽屜背景顏色
    contentComponent: (props) => (<DrawerBScreen {...props} />)
});
export default AppContainer = createAppContainer(DrawerNavigator);

DrawerBScreen組件是你自定義的頁面狼忱。
有個(gè)不足之處就是react-navigation自帶的抽屜組件不支持手勢返回膨疏,所以建議使用react-native-drawer-layout,效果還不錯(cuò)钻弄。

react-navigation的使用就點(diǎn)到為止佃却,本文不是為了介紹每個(gè)api的使用,旨在闡述一些常見的應(yīng)用場景窘俺,建議新手過一遍官方文檔饲帅。

二、redux

redux有三大原則:

  • 單一數(shù)據(jù)源:整個(gè)應(yīng)用的state統(tǒng)一放在一個(gè)store中
  • State 是只讀的:只能通過派發(fā)action來改變state
  • 使用純函數(shù)來執(zhí)行修改:Reducer 只是一些純函數(shù),它接收先前的 state 和 action灶泵,并返回新的 state育八。
    官方示例代碼可以去看下:redux示例代碼,這里只展示redux在RN中的應(yīng)用赦邻。

在RN中集成redux髓棋,步驟如下:

  • createStore--創(chuàng)建一個(gè)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統(tǒng)一放在這里
import { combineReducers } from 'redux';
import {navReducer} from './navReducer'
import {login} from './loginReducer'
const appReducer = combineReducers({
    nav:navReducer,
    login:login
});
export default appReducer
  • 創(chuàng)建navReducer惶洲,存放路由相關(guān)數(shù)據(jù)
import {
    createNavigationReducer,
} from 'react-navigation-redux-helpers';
import  AppContainer  from '../../router/index' //這里的AppContainer就是上面展示的路由相關(guān)配置的代碼

export const navReducer = createNavigationReducer(AppContainer);

  • 以loginReducer為例子闡述reducer的整個(gè)流程:
    1按声、新建actionTypes
//登錄相關(guān)action
export const LOGINING = 'LOGINING'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_ERROR = 'LOGIN_ERROR'
export const LOGOUT = 'LOGOUT'

2、新建要派發(fā)的action函數(shù)

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)

不知道大家有沒有注意到一個(gè)寫代碼的小技巧,如果你是用VSCode開發(fā)react币呵,在新建一個(gè)react組件的時(shí)候,敲打rcredux會(huì)索引很快敲出一個(gè)react包含redux的組件出來侨颈,很省事余赢。當(dāng)然還有很多類似這個(gè)生成代碼的,vue也有哈垢。有點(diǎn)類似.vue文件下妻柒,輸入vbase可以快速生成模板代碼。

三耘分、react-navigation和redux雙劍合璧

有了以上基礎(chǔ)举塔,react-navigation和redux實(shí)現(xiàn)雙劍合璧就容易多了,實(shí)現(xiàn)代碼如下:

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
        }

        //退出應(yīng)用
        if (this.lastBackPressed && this.lastBackPressed + 2000 >= Date.now()) {
            //最近2秒內(nèi)按過back鍵求泰,可以退出應(yīng)用央渣。
            return false;
        }

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

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

  • 提供了一個(gè)Provider組件,最外層傳入store渴频,這樣以下的所有子組件都可以拿到reducer的state芽丹,原理就是react的context
  • 需要注意這里的key--'root',要跟上面的配置store的key一致:

const AppWithRedux=createReduxContainer(AppContainer,'root')

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

  • 安卓還要寫一個(gè)返回物理鍵返回監(jiān)聽卜朗,控制是否退出應(yīng)用和返回頁面

如果你還是嫌棄redux麻煩拔第,那么mobx也是較好的選擇〕《ぃ可以參考下面的demo進(jìn)行配置mobx:
https://github.com/vonovak/react-navigation-mst-demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蚊俺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子逛万,更是在濱河造成了極大的恐慌泳猬,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異暂殖,居然都是意外死亡价匠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門呛每,熙熙樓的掌柜王于貴愁眉苦臉地迎上來踩窖,“玉大人,你說我怎么就攤上這事晨横⊙笕” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵手形,是天一觀的道長啥供。 經(jīng)常有香客問我,道長库糠,這世上最難降的妖魔是什么伙狐? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮瞬欧,結(jié)果婚禮上贷屎,老公的妹妹穿的比我還像新娘。我一直安慰自己艘虎,他們只是感情好唉侄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著野建,像睡著了一般属划。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上候生,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天同眯,我揣著相機(jī)與錄音,去河邊找鬼陶舞。 笑死嗽测,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的肿孵。 我是一名探鬼主播唠粥,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼停做!你這毒婦竟也來了晤愧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤蛉腌,失蹤者是張志新(化名)和其女友劉穎官份,沒想到半個(gè)月后只厘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡舅巷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年羔味,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钠右。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡赋元,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出飒房,到底是詐尸還是另有隱情搁凸,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布狠毯,位于F島的核電站护糖,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏嚼松。R本人自食惡果不足惜嫡良,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望献酗。 院中可真熱鬧皆刺,春花似錦、人聲如沸凌摄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锨亏。三九已至,卻和暖如春忙干,著一層夾襖步出監(jiān)牢的瞬間器予,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國打工捐迫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留乾翔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓施戴,卻偏偏與公主長得像反浓,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赞哗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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