React Navigation 4x到5x的迁移指南

Posted by CrazyCodeBoy的技术博客官网 on October 25, 2020

提示:本教程不定期更新,请关注课程章节列表及时获取更新

在这个教程中将向大家分享React Navigation 4x到5x的迁移指南,本教程主要以课程源码为例讲解4x到5x迁移过程中所需要做的事情以及一些经验和心得。

如果你的项目还没有使用4x版本,那么可以直接参考下面教程按照和使用4x或5x版本的导航器:

目录

  • 热门问答
  • 迁移指南
    • 包依赖迁移
    • Navigation Container迁移
    • 路由配置迁移
    • 关于navigation prop的不同
    • 不支持Switch Navigator
  • 如何将本课程项目Github_RN从4x迁移都5x?
  • 5x还不支持的功能

已经迁移到5x后的课程源码:

热门问答

  1. 5x相比4x有哪些优缺点?
    • 答:5x相比4x最大的优点是在使用的写法上支持了JSX的写法,但相比比较成熟的4x缺点还比是较明显的,主要体现在4x所支持的一些API或功能在5x还没有完全支持;另外因为5x新发布不久,bug会比4x多一些;还有就是5x生态没有4x健全,用的公司也不是很多,遇到问题在网上能够找到的资料没有4x全
  2. 4x到5x迁移的迁移成本大吗?
    • 答:因为5x相比4x主要是在使用上写法变了,大部分API和4x都是一致的,从现在迁移的经验上来看总体迁移成本并不大。
  3. 4x还在更新吗,可以不迁移到5x吗?
    • 答:从React Navigation的官库上看,目前官方还一直在不停的更新4x的版本,因为目前业界大部分公司用的版本还主要集中在4x、3x上,所以除非有特殊要求需要使用5x,否则可以继续使用4x的版本。

迁移指南

包依赖迁移

在5x中对应的包名发生了变化,要完成4x到5x的迁移,首先我们需要将下面的包迁移到5x中去:

4x的中的包 对应5x中的包
react-navigation @react-navigation/native
react-navigation-stack @react-navigation/stack
react-navigation-tabs @react-navigation/bottom-tabs, @react-navigation/material-top-tabs
react-navigation-material-bottom-tabs @react-navigation/material-bottom-tabs
react-navigation-drawer @react-navigation/drawer

我们只需要对照上表,将我们在package.json中依赖的4x的包删除,然后重新安装对应右侧5x的包即可。

在React Navigation 5x 由NavigationContainer中来代替4x的createAppContainer

import { NavigationContainer } from '@react-navigation/native';

export default function App() {
  return <NavigationContainer>{/*...*/}</NavigationContainer>;
}

另外,5x中NavigationContaineronStateChange API用来代替createAppContaineronNavigationStateChange,下文会重点介绍。

提示:如果你的项目中需要用到多个NavigationContainer嵌套的情况,那么需要在被嵌套的NavigationContainer中设置independent={true}

<NavigationContainer
    independent={true}
>
...

路由配置迁移

在React Navigation 4.x,我们通常使用createXNavigator 函数来创建对已的导航器配置,在5x中则是通过XX.Navigator + XX.Screen 以组件的方式进行配置的:

4x的配置:

const RootStack = createStackNavigator(
  {
    Home: {
      screen: HomeScreen,
      navigationOptions: { title: 'My app' },
    },
    Profile: {
      screen: ProfileScreen,
      params: { user: 'me' },
    },
  },
  {
    initialRouteName: 'Home',
    defaultNavigationOptions: {
      gestureEnabled: false,
    },
  }
);

对应5x的配置:

const Stack = createStackNavigator();

function RootStack() {
  return (
    <Stack.Navigator
      initialRouteName="Home"
      screenOptions=
    >
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options=
      />
      <Stack.Screen
        name="Profile"
        component={ProfileScreen}
        initialParams=
      />
    </Stack.Navigator>
  );
}

不难发现在5x中所有的配置是通过props的方式传递个navigator的。

  • 另外,通过一个 Screen 元素来表示一个页面;
  • params 变成了 initialParams
  • navigationOptions 变成了options
  • defaultNavigationOptions变成了screenOptions

关于navigation prop的不同

  • navigation prop:主要包含navigate, goBack等在内的一些工具方法;
  • route prop:则包含之前navigation.state在内的一些页面的数据;

这点影响最大的就我们之前从this.props.navigation.state.params中取数据,现在要改成this.props.route.params中取数据,可以对比下我们DetailPage.js迁移所做的修改;

不支持Switch Navigator

5x不在为Switch Navigator提供支持,关于它的替代方案,我会在[NavigationUtil.js修改](https://git.imooc.com/coding-304/GitHub_Advanced/src/7398f2e9d3fc71399d1baa9ada5887bd649f478f/js/navigator/NavigationUtil.js)的部分进行讲解。

如何将本课程项目Github_RN从4x迁移都5x?

讲过上述的内容学习之后,接下来我们就将我们课程中Github_RN从4x迁移到5x,以下是迁移步骤:

  1. package.json依赖修改
  2. AppNavigators.js修改
  3. DynamicTabNavigator.js修改
  4. NavigationUtil.js修改
  5. PopularPage.js修改
  6. 其它文件修改

package.json依赖修改

在 package.json中移除:

  • react-navigation
  • react-navigation-stack
  • react-navigation-tabs

然后安装:

  • @react-navigation/bottom-tabs
  • @react-navigation/material-top-tabs
  • @react-navigation/native
  • @react-navigation/stack
  • react-native-tab-view

AppNavigators.js修改

AppNavigators.js中的代码替换为:

import React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
//@ https://github.com/react-navigation/react-navigation/releases/tag/v4.0.0
import WelcomePage from '../page/WelcomePage';
import HomePage from '../page/HomePage';
import WebViewPage from '../page/WebViewPage';
import DetailPage from '../page/DetailPage';
import SortKeyPage from '../page/SortKeyPage';
import SearchPage from '../page/SearchPage';
import CustomKeyPage from '../page/CustomKeyPage';
import AboutPage from '../page/about/AboutPage';
import AboutMePage from '../page/about/AboutMePage';
import CodePushPage from '../page/CodePushPage';


const Stack = createStackNavigator();

export default function App() {
    return (
        <NavigationContainer>
            <Stack.Navigator>
                <Stack.Screen name="WelcomePage" component={WelcomePage}
                              options=/>
                <Stack.Screen name="HomePage" component={HomePage}
                              options=/>
                <Stack.Screen name="DetailPage" component={DetailPage}
                              options=/>
                <Stack.Screen name="WebViewPage" component={WebViewPage}
                              options=/>
                <Stack.Screen name="AboutPage" component={AboutPage}
                              options=/>
                <Stack.Screen name="AboutMePage" component={AboutMePage}
                              options=/>
                <Stack.Screen name="CustomKeyPage" component={CustomKeyPage}
                              options=/>
                <Stack.Screen name="SortKeyPage" component={SortKeyPage}
                              options=/>
                <Stack.Screen name="SearchPage" component={SearchPage}
                              options=/>
                <Stack.Screen name="CodePushPage" component={CodePushPage}
                              options=/>

            </Stack.Navigator>
        </NavigationContainer>
    );
}

提示:从上述代码中不难发现HomePage的配置比其他页面的配置多了个animationEnabled: false,这是为了关闭别的页面跳转到首页时的转场动画而加的,如果大家在项目研发中也有关闭页面转场动画的需求可以参照上述方式来实现。

另外,需要注意我们AppNavigators.js中的代码替换完之后,那它的调用的地方也需要进行相应的修改:

App.js

...
render() {
    const App = AppNavigator();
    /**
     * 将store传递给App框架
     */
    return <Provider store={store}>
        {App}
    </Provider>;
}
...

DynamicTabNavigator.js修改

DynamicTabNavigator.js中的代码替换为:

import React, {Component} from 'react';
import {NavigationContainer} from '@react-navigation/native';
import {BottomTabBar, createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {connect} from 'react-redux';
import PopularPage from '../page/PopularPage';
import TrendingPage from '../page/TrendingPage';
import FavoritePage from '../page/FavoritePage';
import MyPage from '../page/MyPage';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import Ionicons from 'react-native-vector-icons/Ionicons';
import Entypo from 'react-native-vector-icons/Entypo';
import EventTypes from '../util/EventTypes';
import EventBus from 'react-native-event-bus';

const Tab = createBottomTabNavigator();

type Props = {};

const TABS = {//在这里配置页面的路由
    PopularPage: {
        screen: PopularPage,
        navigationOptions: {
            tabBarLabel: '最热',
            tabBarIcon: ({color, focused}) => (
                <MaterialIcons
                    name={'whatshot'}
                    size={26}
                    style=
                />
            ),
        },
    },
    TrendingPage: {
        screen: TrendingPage,
        navigationOptions: {
            tabBarLabel: '趋势',
            tabBarIcon: ({color, focused}) => (
                <Ionicons
                    name={'md-trending-up'}
                    size={26}
                    style=
                />
            ),
        },
    },
    FavoritePage: {
        screen: FavoritePage,
        navigationOptions: {
            tabBarLabel: '收藏',
            tabBarIcon: ({color, focused}) => (
                <MaterialIcons
                    name={'favorite'}
                    size={26}
                    style=
                />
            ),
        },
    },
    MyPage: {
        screen: MyPage,
        navigationOptions: {
            tabBarLabel: '我的',
            tabBarIcon: ({color, focused}) => (
                <Entypo
                    name={'user'}
                    size={26}
                    style=
                />
            ),
        },
    },
};

class DynamicTabNavigator extends Component<Props> {
    constructor(props) {
        super(props);
        console.disableYellowBox = true;
    }

    fireEvent(navigationState) {
        const {index, history, routeNames} = navigationState;
        if (history.length === 1) {
            return;
        }
        let fromIndex;
        let key = history[history.length - 2].key;
        for (let i = 0; i < routeNames.length; i++) {
            if (key.startsWith(routeNames[i])) {
                fromIndex = i;
                break;
            }
        }

        EventBus.getInstance().fireEvent(EventTypes.bottom_tab_select, {//发送底部tab切换的事件
            from: fromIndex,
            to: index,
        });
    }

    _tabNavigator() {
        if (this.Tabs) {
            return this.Tabs;
        }
        const {PopularPage, TrendingPage, FavoritePage, MyPage} = TABS;
        const tabs = {PopularPage, TrendingPage, FavoritePage, MyPage};//根据需要定制显示的tab
        PopularPage.navigationOptions.tabBarLabel = '最热';//动态配置Tab属性
        return this.Tabs = <NavigationContainer
            independent={true}
        >
            <Tab.Navigator
                tabBar={props => {
                    this.fireEvent(props.state);
                    return <TabBarComponent theme={this.props.theme} {...props}/>;
                }}
            >
                {
                    Object.entries(tabs).map(item => {
                        return <Tab.Screen
                            name={item[0]}
                            component={item[1].screen}
                            options={item[1].navigationOptions}
                        />;
                    })

                }
            </Tab.Navigator>
        </NavigationContainer>;
    }

    render() {
        const Tab = this._tabNavigator();
        return Tab;
    }
}

class TabBarComponent extends React.Component {
    constructor(props) {
        super(props);
        this.theme = {
            tintColor: props.activeTintColor,
            updateTime: new Date().getTime(),
        };
    }

    render() {
        return <BottomTabBar
            {...this.props}
            activeTintColor={this.props.theme.themeColor}
        />;
    }
}

const mapStateToProps = state => ({
    theme: state.theme.theme,
});

export default connect(mapStateToProps)(DynamicTabNavigator);

上述代码大家直接替换你的对应文件即可,但要注意以下几个经验技巧:

  • 关于tabBarIcon的使用;
  • tabBar的使用;
  • 关于在5x中动态创建底部导航器的技巧;
  • 关于在5x中监听tab切换的技巧;

关于tabBarIcon的使用

4x的tabBarIcon中tintColor在5x中叫color,使用时需要注意;

tabBar的使用

4x的中的tabBarComponent在5x中叫tabBar,使用时需要注意;

关于在5x中动态创建底部导航器的技巧

在4x中我们通过createBottomTabNavigator(tabs,...的方式动态创建了一个底部导航器,这种方式在5x中就不奏效了,在5x中推荐大家借助ES7的新特性Object.entries来创建动态导航器:

···
 <Tab.Navigator
    tabBar={props => {
        this.fireEvent(props.state);
        return <TabBarComponent theme={this.props.theme} {...props}/>;
    }}
>
    {
        Object.entries(tabs).map(item => {
            return <Tab.Screen
                name={item[0]}
                component={item[1].screen}
                options={item[1].navigationOptions}
            />;
        })

    }
</Tab.Navigator>
···

其中name表示页面对应的路由名,component是对应的该页面所展示的对应组件,options对应4x的navigationOptions

关于在5x中监听tab切换的技巧

上文中我们讲到4x的onNavigationStateChange在5x中被改成了onStateChange,使用它我们可以监听NavigationContainer节点下一级页面的切换,然后可以通过onStateChange中的回调参数来判断切换到的页面,但5x存在一个bug也就是当你在APP中存在NavigationContainer嵌套时,里层NavigationContaineronStateChange在回调时会缺失参数,从而无法判断页面切换。

为解决这个问题,有个小技巧就是我们在tabBar的回调方法中进行页面切换的判断,没有切换tab时tabBar都会被回调,同时会携带index, history, routeNames等参数,我们可以从这些参数中获取到当前切换到的页面,以及上一个页面对应的索引:

<Tab.Navigator
    tabBar={props => {
        this.fireEvent(props.state);
        return <TabBarComponent theme={this.props.theme} {...props}/>;
    }}
>
...
 fireEvent(navigationState) {
    const {index, history, routeNames} = navigationState;
    if (history.length === 1) {
        return;
    }
    let fromIndex;
    let key = history[history.length - 2].key;
    for (let i = 0; i < routeNames.length; i++) {
        if (key.startsWith(routeNames[i])) {
            fromIndex = i;
            break;
        }
    }

    EventBus.getInstance().fireEvent(EventTypes.bottom_tab_select, {//发送底部tab切换的事件
        from: fromIndex,
        to: index,
    });
}

NavigationUtil.js修改的修改只有一处,主要是为了弥补5x不支持Switch Navigator的问题:

static resetToHomPage(params) {
    const {navigation} = params;
    navigation.navigate('Main');
}

修改成:

static resetToHomPage(params) {
    const {navigation} = params;
    // navigation.navigate("HomePage");

    navigation.dispatch(
        StackActions.replace('HomePage', {}),
    );
}

在上述代码中我们通过StackActions.replaceAPI来实现了Switch Navigator的效果。

PopularPage.js修改

我们在PopularPage.js中用到了createAppContainercreateMaterialTopTabNavigator所以也需要进行相应的修改:

主要修改是将:

render() {
 ...
const TabNavigator = keys.length ? createAppContainer(createMaterialTopTabNavigator(
        this._genTabs(), {
            tabBarOptions: {
                tabStyle: styles.tabStyle,
                upperCaseLabel: false,//是否使标签大写,默认为true
                scrollEnabled: true,//是否支持 选项卡滚动,默认false
                style: {
                    backgroundColor: theme.themeColor,//TabBar 的背景颜色
                    // 移除以适配react-navigation4x
                    // height: 30//fix 开启scrollEnabled后再Android上初次加载时闪烁问题
                },
                indicatorStyle: styles.indicatorStyle,//标签指示器的样式
                labelStyle: styles.labelStyle,//文字的样式
            },
            lazy: true,
        },
    )) : null;
    return <View style={styles.container}>
        {navigationBar}
        {TabNavigator && <TabNavigator/>}
    </View>;
}

修改为:

render() {
...
    const TabNavigator = keys.length ? <NavigationContainer
        independent={true}
    >
        <Tab.Navigator
            lazy={true}
            tabBarOptions={
                {
                    tabStyle: styles.tabStyle,
                    // upperCaseLabel: false,//5x 暂不支持标签大写控制
                    scrollEnabled: true,//是否支持 选项卡滚动,默认false
                    activeTintColor: 'white',
                    style: {
                        backgroundColor: theme.themeColor,//TabBar 的背景颜色
                        // 移除以适配react-navigation4x
                        // height: 30//fix 开启scrollEnabled后再Android上初次加载时闪烁问题
                    },
                    indicatorStyle: styles.indicatorStyle,//标签指示器的样式
                    labelStyle: styles.labelStyle,//文字的样式
                }
            }
        >
            {
                Object.entries(this._genTabs()).map(item => {
                    return <Tab.Screen
                        name={item[0]}
                        component={item[1].screen}
                        options={item[1].navigationOptions}
                    />;
                })
            }
        </Tab.Navigator>
    </NavigationContainer> : null;


    return <View style={styles.container}>
        {navigationBar}
        {TabNavigator}
    </View>;
}

上述思路和上文中在DynamicTabNavigator.js修改中进行动态底部导航器所做的修改是一样的,同样道理我们在TrendingPage.js,FavoritePage.js中需要做的修改也是一样的思路,具体可以参考我们仓库中的源码。

其它文件修改

接下来其它文件的修改思路类似,详情参考4x迁移到5x的代码变更对比

5x还不支持的功能

  • 不支持Switch Navigator导航器
  • ….

最后

本教程主要针对课程源码迁移过程中的经验和新的的分享,无法做到所有地方都面面俱到,对于考虑不到的地方大家也可以参考下react-navigation官方提供的迁移文档upgrading-from-4.x