RN項目中實現React-Navigation動態底部導航欄

需求

這是去年 App 項目提出的一個需求,由於咱們作的這個 App 區分了不少渠道,同時登陸用戶也有不少狀態,一些菜單須要動態的顯示隱藏。項目是使用的 React-Native 框架,路由庫選擇官方推薦的 react-navigation,因此要實現這個需求,必須是改動 react-navigation 的配置。可是 react-navigation 的文檔很是扯淡,react-navigation 的文檔是這樣寫的:react

使用動態路由,須要對 React-Navigation 有一點了解才能充分發揮。React-Navigation 要求你靜態的定義你的路由,若是你必定要使用動態路由,也有解決方案,但可能會有一些額外的複雜度。ios

what?你覺得會有後續告訴咱們解決方案是什麼嗎,文檔寫到這就沒了,這不是脫了褲子放屁嗎,既然說了有解決方案,又不給我說具體解決方案是什麼。引用羅老師的名言:我懷疑你在外面有六個私生子,但我不能告訴你是誰 😏後端

只要中杯

好幾個不一樣的解決方案

最終完美解決這個需求經歷了 3 次方案更改:框架

  1. 第一次的方案:直接在根級組件處定義一個狀態,根據後端返回的狀態不一樣生成不一樣的 react-navigation 配置。這種方案雖能夠解決問題,可是性能上有一個很大的問題,由於 react-navigation 是惰性加載頁面組件的,也就是說只有導航到了這個頁面纔會渲染該頁面組件,而且只要此頁面未被移除 react-navigation 的路由棧,後續再導航到此頁面是不會經歷從新構建頁面組件的流程的。在根級組件處根據 state 來從新渲染會讓整個根組件從新 render,而且由於直接產生新的 react-navigation 根導航組件,在用戶視覺上會有一個頁面閃爍的感受,對於用戶體驗來講是不友好的。性能

  2. 第二次方案,將底部菜單做爲單獨的導航系統來使用,雖然 react-navigation 要求項目中只能使用一個導航系統,可是實際這樣用並不會報錯,只不過會在 ios 系統上有警告。這種實現動態底部菜單的好處就是不會有閃爍,而且也只會重渲染這個單獨的底部導航組件,不會整個根組件從新渲染。缺點是使用了兩套導航系統,在底部菜單這個組件上的導航與整個 App 頁面的導航使用的是不一樣的 navigation,須要在不一樣的頁面使用不一樣的導航方式,而且沒法在其餘頁面直接導航到底部菜單中的某一個子菜單。this

  3. 第三次方案,這應該是目前最完美的方案了,只使用一套導航系統,並且動態改變菜單隻須要刷新底部導航欄。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進行導航,就是想去哪兒就去哪兒🆒

相關文章
相關標籤/搜索