这是去年 App 项目提出的一个需求,由于咱们作的这个 App 区分了不少渠道,同时登陆用户也有不少状态,一些菜单须要动态的显示隐藏。项目是使用的 React-Native 框架,路由库选择官方推荐的 react-navigation,因此要实现这个需求,必须是改动 react-navigation 的配置。可是 react-navigation 的文档很是扯淡,react-navigation 的文档是这样写的:react
使用动态路由,须要对 React-Navigation 有一点了解才能充分发挥。React-Navigation 要求你静态的定义你的路由,若是你必定要使用动态路由,也有解决方案,但可能会有一些额外的复杂度。ios
what?你觉得会有后续告诉咱们解决方案是什么吗,文档写到这就没了,这不是脱了裤子放屁吗,既然说了有解决方案,又不给我说具体解决方案是什么。引用罗老师的名言:我怀疑你在外面有六个私生子,但我不能告诉你是谁 😏后端
最终完美解决这个需求经历了 3 次方案更改:框架
第一次的方案:直接在根级组件处定义一个状态,根据后端返回的状态不一样生成不一样的 react-navigation 配置。这种方案虽能够解决问题,可是性能上有一个很大的问题,由于 react-navigation 是惰性加载页面组件的,也就是说只有导航到了这个页面才会渲染该页面组件,而且只要此页面未被移除 react-navigation 的路由栈,后续再导航到此页面是不会经历从新构建页面组件的流程的。在根级组件处根据 state 来从新渲染会让整个根组件从新 render,而且由于直接产生新的 react-navigation 根导航组件,在用户视觉上会有一个页面闪烁的感受,对于用户体验来讲是不友好的。性能
第二次方案,将底部菜单做为单独的导航系统来使用,虽然 react-navigation 要求项目中只能使用一个导航系统,可是实际这样用并不会报错,只不过会在 ios 系统上有警告。这种实现动态底部菜单的好处就是不会有闪烁,而且也只会重渲染这个单独的底部导航组件,不会整个根组件从新渲染。缺点是使用了两套导航系统,在底部菜单这个组件上的导航与整个 App 页面的导航使用的是不一样的 navigation,须要在不一样的页面使用不一样的导航方式,而且没法在其余页面直接导航到底部菜单中的某一个子菜单。this
第三次方案,这应该是目前最完美的方案了,只使用一套导航系统,并且动态改变菜单只须要刷新底部导航栏。spa
//先定义两个不一样的底部组件,根据用户权限决定使用哪一个
//三个菜单: 首页,我的中心,和商品推荐页面,根据用户权限决定是否显示商品推荐页面
// 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,也就是下图这个东西:rest
可是若是真要彻底本身写个这东西感受不现实啊,毕竟涉及到不一样机型分辨率的适配处理,以及路由跳转的逻辑。但我仍是去看了 createBottomTabNavigator 的源码,我看源码中 tabBarComponent 是怎样处理的: createBottomTabNavigator 是在 react-navigation-tabs 这个包中,使用的 tabbarComponent 在 react-navigation-tabs/dist/views/BottomTabBar.js, 部分源码以下图:code
能够看到决定在 tabBarComponent 渲染几个按钮的关键就是 props.navigation.state 的 routes 和 index,并且 props 中 navigation 就只用到了这个属性,并无使用 navigation 的其余功能,因此个人想法就是单独引入这个 BottomTabBar 组件,而后再基于这个封装本身的 BottomTabBar。component
//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进行导航,就是想去哪儿就去哪儿🆒