RN项目中实现React-Navigation动态底部导航栏

需求

这是去年 App 项目提出的一个需求,因为我们做的这个 App 区分了很多渠道,同时登录用户也有很多状态,一些菜单需要动态的显示隐藏。项目是使用的 React-Native 框架,路由库选择官方推荐的 react-navigation,所以要实现这个需求,必须是改动 react-navigation 的配置。但是 react-navigation 的文档非常扯淡,react-navigation 的文档是这样写的:

使用动态路由,需要对 React-Navigation 有一点了解才能充分发挥。React-Navigation 要求你静态的定义你的路由,如果你一定要使用动态路由,也有解决方案,但可能会有一些额外的复杂度。

what?你以为会有后续告诉我们解决方案是什么吗,文档写到这就没了,这不是脱了裤子放屁吗,既然说了有解决方案,又不给我说具体解决方案是什么。引用罗老师的名言:我怀疑你在外面有六个私生子,但我不能告诉你是谁 ?

好几个不同的解决方案

最终完美解决这个需求经历了 3 次方案更改:

  1. 第一次的方案:直接在根级组件处定义一个状态,根据后端返回的状态不同生成不同的 react-navigation 配置。这种方案虽可以解决问题,但是性能上有一个很大的问题,因为 react-navigation 是惰性加载页面组件的,也就是说只有导航到了这个页面才会渲染该页面组件,并且只要此页面未被移除 react-navigation 的路由栈,后续再导航到此页面是不会经历重新构建页面组件的流程的。在根级组件处根据 state 来重新渲染会让整个根组件重新 render,并且因为直接产生新的 react-navigation 根导航组件,在用户视觉上会有一个页面闪烁的感觉,对于用户体验来说是不友好的。

  2. 第二次方案,将底部菜单作为单独的导航系统来使用,虽然 react-navigation 要求项目中只能使用一个导航系统,但是实际这样用并不会报错,只不过会在 ios 系统上有警告。这种实现动态底部菜单的好处就是不会有闪烁,并且也只会重渲染这个单独的底部导航组件,不会整个根组件重新渲染。缺点是使用了两套导航系统,在底部菜单这个组件上的导航与整个 App 页面的导航使用的是不同的 navigation,需要在不同的页面使用不同的导航方式,并且无法在其他页面直接导航到底部菜单中的某一个子菜单。

  3. 第三次方案,这应该是目前最完美的方案了,只使用一套导航系统,而且动态改变菜单只需要刷新底部导航栏。

解决方案的示例代码

方案一:根组件控制导航菜单

//先定义两个不同的底部组件,根据用户权限决定使用哪个
//三个菜单: 首页,个人中心,和商品推荐页面,根据用户权限决定是否显示商品推荐页面
// App.js
const tabbarRoutes = {
  首页: Home,
  商品推荐: LoanMarket,
  个人中心: MyInfo
};

const anotherRoutes = {
  首页: Home,
  个人中心: MyInfo
};

const MyTabRouter = createBottomTabNavigator(tabbarRoutes);

export default class App extends Component {
  state = {
    showMarket: false
  };

  componentDidMount = () => {
    // 这里监听一个事件,如果需要显示商品推荐页面,则将showMarket置为true
    this.subscribe = DeviceEventEmitter.addListener('showMarket', () => {
      this.setState({
        showMarket: true
      });
    });
  };

  componentWillUnmount = () => {
    this.subscribe && this.subscribe.remove();
  };

  render() {
    const tabRoutes = this.state.showMarket ? tabbarRoutes : anotherRoutes;
    const MyTabRouter = createBottomTabNavigator(tabRoutes);
    // 一个系统一般不可能只有一个页面,这里简便只定义一个路由
    const AppStack = createStackNavigator({
      tabbar: {
        screen: MyTabRouter,
        navigationOptions: {
          header: null
        }
      }
    });
    return <AppStack />;
  }
}
复制代码

方案二:独立的底部导航系统

// App.js
import TabbarScreen from './tabbar';
import LoginScreen from './login';

const AppStack = createStackNavigator({
  login: {
    screen: LoginScreen,
    navigationOptions: {
      header: '登录'
    }
  }
  tabbar: {
    screen: TabbarScreen,
    navigationOptions: {
      header: null
    }
  }
});

export default class App extends Component {
  render() {
    return <AppStack />;
  }
}

//tabbar.js
const routes = {
  首页: Home,
  商品推荐: LoanMarket,
  个人中心: MyInfo
};

const anotherRoutes = {
  首页: Home,
  个人中心: MyInfo
};

export default class Tabbar extends Component {
  state = {
    showMarket: false
  }

  componentDidMount = () => {
    // 这里监听一个事件,如果需要显示商品推荐页面,则将showMarket置为true
    this.subscribe = DeviceEventEmitter.addListener('showMarket', () => {
      this.setState({
        showMarket: true
      });
    });
  };

  componentWillUnmount = () => {
    this.subscribe && this.subscribe.remove();
  };

  render() {
    const tabRoutes = this.showMarket ? routes : anotherRoutes
    return React.createElement(createBottomTabNavigator(tabRoutes));
  }

}

// 注意:使用这种方案的时候,tabbar页面其实是与App导航系统独立的一个导航系统,以下举例
//我现在处于Login页面,想跳转到tabbar页面:
this.props.navigation.navigate('tabbar'); //成功跳转
// 我现在处于tabbar中Home页面,我现在想跳转Login:
this.props.navigation.navigate('login'); //不会有任何效果,因为这个navigation只是tabbar中的navigation,不能导航到tabbar路由以外的页面。
// 我现在处于Login页面,想跳转到tabbar中的个人中心(MyInfo)
this.prop.navigation.navigate('个人中心'); //无效,因为顶层导航中根本没有这条导航路线

/*解决tabbar中页面跳转其他页面的问题:
使用顶层导航,即在App.js中设置顶层导航为AppStack的navigation,在tabbar中想跳转tabbar之外的页面就使用导出的顶层导航进行跳转。
*/

//第二个问题,想在其他页面直接跳转到tabbar中某个页面,这种方案下无法解决
复制代码

方案三:自定义底部导航栏

这个方案是我看过 react-navigation 中 createBottomTabNavigator 中的部分源码才想到的一个方案,createBottomTabNavigator 支持传入一个自定义的 tabBarComponent,也就是下图这个东西:

但是如果真要完全自己写个这东西感觉不现实啊,毕竟涉及到不同机型分辨率的适配处理,以及路由跳转的逻辑。但我还是去看了 createBottomTabNavigator 的源码,我看源码中 tabBarComponent 是怎样处理的: createBottomTabNavigator 是在 react-navigation-tabs 这个包中,使用的 tabbarComponent 在 react-navigation-tabs/dist/views/BottomTabBar.js, 部分源码如下图:

可以看到决定在 tabBarComponent 渲染几个按钮的关键就是 props.navigation.state 的 routes 和 index,而且 props 中 navigation 就只用到了这个属性,并没有使用 navigation 的其他功能,所以我的想法就是单独引入这个 BottomTabBar 组件,然后再基于这个封装自己的 BottomTabBar。

//tabbar.js
import { BottomTabBar } from 'react-navigation-tabs';

const tabbarRoutes = {
  首页: Home,
  商品推荐: LoanMarket,
  我的: MyInfo
};

const originalRoutes = [
  { key: '首页', routeName: '首页', params: undefined },
  { key: '商品推荐', routeName: '商品推荐', params: undefined },
  { key: '我的', routeName: '我的', params: undefined }
];

//自定义BottomTabBar
class CustomBottomTabBar extends PureComponent {
  state = {
    showMarket: false
  };

  componentDidMount = () => {
    // 这里监听一个事件,如果需要显示商品推荐页面,则将showMarket置为true
    this.subscribe = DeviceEventEmitter.addListener('showMarket', () => {
      this.setState({
        showMarket: true
      });
    });
  };

  componentWillUnmount = () => {
    this.subscribe && this.subscribe.remove();
  };

  // 这里对navigation进行处理,注意这里不能直接修改props.navigation,会报错,
  //所以只需要传入一个自定义的navigation,而BottomTabBar只会用到navigation.state中routes和index,
  //所以就构造这么一个虚假的navigation就可以了
  dealNavigation = () => {
    const { routes, index } = this.props.navigation.state;
    // 根据是否需要显示商品推荐菜单来决定state中的routes
    let finalRoutes = originalRoutes;
    if (!this.state.showMarket) {
      finalRoutes = originalRoutes.filter(route => route.key !== '商品推荐');
    }
    const currentRoute = routes[index];
    return {
      state: {
        index: finalRoutes.findIndex(route => currentRoute.key === route.key), //修正index
        routes: finalRoutes
      }
    };
  };

  render() {
    const { navigation, ...restProps } = this.props;
    const myNavigation = this.dealNavigation();
    return <BottomTabBar {...restProps} navigation={myNavigation} />;
  }
}

const MyTabRouter = createBottomTabNavigator(tabbarRoutes, {
  navigationOptions: tabRouterConfig.navigationOptions,
  tabBarOptions: tabRouterConfig.tabBarOptions,
  tabBarComponent: CustomBottomBar
});

export default class Tabs extends PureComponent {
  //这里必须有这个静态属性,表示将这个页面视为一个navigator,这样才能和AppStack共用一套导航系统
  static router = MyTabRouter.router;
  render() {
    return <MyTabRouter navigation={this.props.navigation} />;
  }
}
复制代码

这种方案是最好的方案,在一开始就静态定义所有可能用到的路由,然后在底部菜单这里做文章,根据用户的权限来决定隐藏或显示某个路由入口,而且整个App也都用同一个navigation进行导航,就是想去哪儿就去哪儿?

转载于:https://juejin.im/post/5cff4f516fb9a07ea803c0f5

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值