从0.44版本开始Facebook放弃了原来的Navigator接口控制RN应用的路由跳转,并推荐使用react-navigation库实现应用的导航和跳转等功能。本文不止会介绍react-navigation的学习和使用,并同时也会介绍曾经的Navigator接口使用并介绍它们如何在应用中实现路由跳转的集中管理。笔者不会过多的介绍Navigator和react-navigation各个属性和方法的使用,笔者旨在学习和理解在react native中的栈结构路由的使用。
栈概念理解
对于手机应用中单页面应用(SPA)的路由,你可以这样理解:
- 应用(APP)== 整套扑克牌(包括牌盒)
- 栈容器(Navigator或React-Navigation中的 StackNavigator)== 牌盒
- 页面(路由) == 牌
现在我们往空牌盒里面放入牌J,这是你的初始化页面,你可以再放入一张牌Q,盖住了牌J,在你的应用中你看到的页面就变成了牌Q,这个操作就是PUSH,然后你又把牌Q从牌盒中拿出来,你可以返回到之前的牌J,这个操作就是POP,通常PUSH操作你只能按顺序一次次的放入一个个对象,这个对象也许是一张牌,但是也有可能是封装多个同级页面的容器,比如说你的Tab容器页面;这就是单页面最简单的跳转和返回路由操作,其他还有以下相关操作:
- reset(重置): 在已经有牌J、Q、K的牌盒里面,把所有的牌全部一次性拿出,放入牌A,这个过程就是重置你的整个路由;
- popTo(返回指定页面):对已经有牌J、Q、K的牌盒里面对各牌进行按顺序下标0,1,2,其实就是数组结构,当前情况下你可以看到牌K,你的pop()返回至Q其实相当于popTo(1),你还可以使用popTo(0),这样你就等于一次性移开了最上面的牌Q、K,而你的牌盒中只剩下了J,这样相当于一次性按顺序返回多个页面;
- getCurrentRoutes(获取当前所有路由): 对已经有牌J、Q、K的牌盒里面对各牌进行按顺序下标0,1,2,你可以获取到这个牌盒概念的路由数组,你可以对当前里面的牌进行指定操作。
延伸: 你的App即是你的牌盒,你只能对你牌盒中已经有的牌进行操作,当然你也可以新拿一张牌放入牌盒中进行操作,但是如果你的牌本身不在你的牌盒中,你是无法进行操作的,所以有时候如果这个牌都不在你的牌盒中,你使用通知-观察等这样的概念去操作一个不存在的页面对象是不会成功的。
Navigator使用和封装
0.44版本后Navigator已经从react-native库中移除,如需导入可按如下操作:
// install
$npm install React-native-deprecated-custom-components --save
// import API
import CustomerComponents, {Navigator} from 'react-native-deprecated-custom-components';
实际项目中对于单页面应用,我们可以把Navigator封装成一个组件,把各页面当作Navigator的一个个场景转换,在页面中实现跳转,返回,动画等的各种操作时只需要调用相应方法即可。
class APP extends Component {
constructor(props) {
super(props);
this._renderScene = this._renderScene.bind(this);
this.state = {};
}
/* eslint-disable */
_renderScene(route, navigator) {
let Component = route.component;
return (
<Component
{...route}
navigator={navigator}
passProps={route.passProps}
callback={route.callback}
/>
);
}
render() {
return (
<View style={{ flex: 1 }}>
<Navigator
ref="navigator"
renderScene={this._renderScene}
configureScene={(route) => ({
...route.sceneConfig || Navigator.SceneConfigs.HorizontalSwipeJump,
gestures: route.gestures
})}
initialRoute={{
component: Login
}}
/>
<LoadingView isVisible={this.props.showLoading} />
</View>
)
}
}
除了场景转换等操作,还可以在这个组件中集成控制App全局的一些操作,比如说,Loading的设置,网络状态检查等设置,在各页面就无须再单独设置。尽量在一个地方里面实现控制app的一些相近的默认操作
实际页面中跳转或其他操作:
_jumpPage() {
const { navigator } = this.props;
if (navigator) {
navigator.push({
component: TabBarList, //next route
sceneConfig: Navigator.SceneConfigs.FloatFromBottomAndroid, // animated config
callback: () => {} //callback
passProps: { //transfer parameters
tabs: 'home',
activeTab: 'home',
onPressHandler: this.props.goToPage
}
});
}
}
React Navigation理解和使用
react-native 0.44版本之前路由控制使用的Navigator虽然非常稳定,基本没出现过什么BUG,但是跳转效果一直被人诟病,跳转时候的动画和原生App的效果相比,非常明显差一等,在0.44版本后Facebook推荐使用react-navigation库来实现页面跳转,tab转换,侧边栏滑动等功能。
react-navigation主要包括导航,底部tab,顶部tab
,侧滑等,功能很强大,而且体验接近原生。接下来会一一介绍:
- 导航 -> StackNavigator
- 底部或者顶部tab -> TabNavigator
关于侧滑DrawerNavigator的使用,笔者不在本文介绍,但可以看这篇附带Demo的推荐博客
StackNavigator
StackNavigator在功能上就是相当于原来使用Navigator,但是他有着不一样的实现和非常好的跳转体验,使用上也非常简单,其实也就是三部曲:
- 路由配置(页面注册):
const routeConfigs = {
Login: { screen: Login },
TabBar: { screen: TabBarContainer },
Feedback: { screen: Feedback },
};
- 默认场景配置:
const stackNavigatorConfig = {
initialRouteName: 'Login',
navigationOptions: {
headerBackTitle: null,
headerTintColor: 'white',
showIcon: true,
swipeEnabled: false,
animationEnabled: false,
headerStyle: {
backgroundColor: '#f2f2f2'
}
},
mode: 'card',
paths: 'rax/: Login',
headerMode: 'float',
transitionConfig: (() => ({
screenInterpolator: CardStackStyleInterpolator.forHorizontal // android's config about jump to next page
})),
onTransitionStart: () => {},
onTransitionEnd: () => {}
};
- 容器生成与初始化:
const Nav = StackNavigator(routeConfigs, stackNavigatorConfig);
export default class QQDrawerHome extends Component {
render() {
return(
<Nav/>
);
}
}
这样就简单完成了路由的配置,开发时只需要把新页面添加到注册对象routeConfigs中,StackNavigator会对里面的的注册页面和注册时使用的KEY值形成对应关系,当你在页面时跳转时,只需要这样:
_jumpPage() {
const { navigation } = this.props;
if (navigation) {
const { navigation } = this.props;
navigation.navigate('TabBar');
}
}
带参数跳转时:
_jumpPage() {
const { navigation } = this.props;
if (navigation) {
const { navigation } = this.props;
navigation.navigate('TabBar', {
visible: false,
title: '首页'
});
}
}
在下个页面就可以拿到参数并设置头部或其他参数:
static navigationOptions = ({ navigation }) => {
const { state } = navigation;
const { title } = state.params;
return {
title: title,
};
};
其他reset,setParams等操作将可以学着本文后面封装到组件中去使用,当然你也可以直接在页面跳转函数中重置路由,就像这样:
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Login'})
]
})
this.props.navigation.dispatch(resetAction)
TabNavigator
0.44版本之前我们实现Tab页面通常都选择使用框架react-native-tab-navigator或者react-native-scrollable-tab-view,现在0.44版本后react-navigation库中推荐使用TabNavigator,同样的使用方式,类似StackNavigator三部曲:
const routeConfigs = {
Message:{
screen:QQMessage,
navigationOptions: {
tabBarLabel: '消息',
tabBarIcon: ({ tintColor }) => (
<Image
source={require('./notif-icon.png')}
style={[styles.icon, {tintColor: tintColor}]}
/>),
}
},
Contact:{
screen:QQContact,
navigationOptions: {
tabBarLabel: '联系人',
tabBarIcon: ({ tintColor }) => (
<Image
source={require('./notif-icon.png')}
style={[styles.icon, {tintColor: tintColor}]}
/>),
}
},
};
const tabNavigatorConfig = {
tabBarComponent:TabBarBottom,
tabBarPosition:'bottom',
swipeEnabled:false,
animationEnabled:false,
lazy:true,
initialRouteName:'Message',
backBehavior:'none',
tabBarOptions:{
activeTintColor:'rgb(78,187,251)',
activeBackgroundColor:'white',
inactiveTintColor:'rgb(127,131,146)',
inactiveBackgroundColor:'white',
labelStyle:{
fontSize:12
}
}
}
export default TabNavigator(routeConfigs, tabNavigatorConfig);
关于使用TabNavigator的一些注意点和当前问题:
- 如你甚至未使用StackNavigator,而想直接使用TabNavigator,还是用其他第三方框架吧,他和StackNavigator是配套使用的,你必须保证TabNavigator存在于StackNavigator中,TabNavigator才能良好工作。
- 当你当前页面使用了TabNavigator,那么TabNavigator所形成的容器组件应该是当前页面的顶层组件,否则报错,将会无法获取到tab中的router数组。
- 关于嵌套使用TabNavigator,即在TabNavigator的一个screen中再次使用了TabNavigator形成页面,安卓平台下无法渲染子组件,页面空白,且内层Tab基本失效,或者你的内层Tab容器使用其他第三方框架如react-native-tab-view等类似框架,问题依然存在,关于此问题可关注公关BUG#1796。
StackNavigator路由的集中封装
此部分集成了一部分Redux知识,建议可以看一下redux官方文档了解一下redux。StackNavigator本身就集成了Redux来进行路由数据的管理,如你想要将你自己的redux管理集成到StackNavigator中,官方同样提供接口addNavigationHelpers,这里我们关注的是如何把reset,setParams等Navigator中的Action直接封装到组件中形成页面调用接口。
以下是笔者的封装组件,类似之前封装Navigator组件封装集中管理组件的思路代码,我们把StackNavigator同样封装为一个组件作为管理中心
......
const AppNavigator = StackNavigator(RouteConfigs, stackNavigatorConfig);// eslint-disable-line
class MainContainer extends Component {
constructor(props) {
super(props);
this.resetRouteTo = this.resetRouteTo.bind(this);
this.resetActiveRouteTo = this.resetActiveRouteTo.bind(this);
this.backTo = this.backTo.bind(this);
this.setParamsWrapper = this.setParamsWrapper.bind(this);
this.state = {};
}
resetRouteTo(route, params) {
const { dispatch } = this.props;
if (dispatch) {
dispatch(
NavigationActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: route, params: params })],
})
);
}
}
resetActiveRouteTo(routeArray, activeIndex) {
const { dispatch } = this.props;
if (dispatch) {
const actionsArray = [];
for (let i = 0; i < routeArray.length; i++) {
actionsArray.push(NavigationActions.navigate({ routeName: routeArray[i] }));
}
const resetAction = NavigationActions.reset({
index: activeIndex,
actions: actionsArray,
});
dispatch(resetAction);
}
}
backTo(key) {
const { dispatch } = this.props;
if (dispatch) {
dispatch(
NavigationActions.reset({
key: key
})
);
}
}
setParamsWrapper(params, key) {
const { dispatch } = this.props;
if (dispatch) {
const setParamsAction = NavigationActions.setParams({
params: params,
key: key,
});
dispatch(setParamsAction);
}
}
render() {
const { dispatch, navigationState, screenProps } = this.props;
return (
<View
style={{ flex: 1 }}
onStartShouldSetResponder={() => dismissKeyboard()}
>
<StatusBar barStyle="light-content" />
<AppNavigator
navigation={addNavigationHelpers({
dispatch: dispatch,
state: navigationState,
resetRouteTo: (route, params) => this.resetRouteTo(route, params),
resetActiveRouteTo: (routeArray, activeIndex) => this.resetActiveRouteTo(routeArray, activeIndex),
backTo: (key) => this.backTo(key),
setParamsWrapper: (params, key) => this.setParamsWrapper(params, key)
})}
screenProps={screenProps}
/>
<Loading isVisible={true} mode="alipay" />
</View>
);
}
}
const mapStateToProps = (state) => {
const newNavigationState = state.navReducer;
if (state.screenProps) {
newNavigationState.params = {
...state.params,
...state.screenProps
};
}
return {
navigationState: newNavigationState,
screenProps: state.screenProps
};
};
export default connect(mapStateToProps)(MainContainer);
......
其中绑定navReducer文件的数据,可参考redux和react-navigation官网文档,此文不再列出
这样封装后,各页面使用reset,setParams等操作时,就可以像以前一样直接使用相关操作,如重置路由:
_jumpPage() {
const { navigation } = this.props;
if (navigation) {
navigation.resetRouteTo('TabBar', { title: '首页', selectedTab: 'home' });
}
}
写在最后
笔者第一次写博客,如果有什么不足之处,或者上面的一些问题有什么不对的,欢迎大家批评与指正,一起学习和进步。
相关文章可参考: