React Native路由理解和react-navigation库封装学习

从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' });
    }
}

写在最后


笔者第一次写博客,如果有什么不足之处,或者上面的一些问题有什么不对的,欢迎大家批评与指正,一起学习和进步。

相关文章可参考:

ReactNative导航新宠儿react-navigation

React Native未来导航者:react-navigation 使用详解

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

推荐阅读更多精彩内容