RN開發(fā)一般都會(huì)結(jié)合一些處理數(shù)據(jù)流的插件庫,如redux豌鹤、mobx、dva等默怨,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-navigation、react-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無非兩種饥侵,如下圖所示:
實(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