React Native 路由导航:React Navigation

在上一篇介绍了 React Native 项目的初始化方式选择后,我选择了Cli的方式创建了项目。接下来就是实现 React Native 的页面跳转。

使用Cli创建项目

bash 复制代码
npx @react-native-community/cli@latest init AwesomeProject

按照 React Native 官方文档安装所需依赖

reactnative.dev/docs/naviga...

bash 复制代码
yarn add @react-navigation/native @react-navigation/native-stack
bash 复制代码
yarn add react-native-screens react-native-safe-area-context

运行 ios 前,需要先执行

bash 复制代码
cd ios
pod install
cd ..

修改 App.tsx,创建导航容器

tsx 复制代码
import React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import RootStack from '@/navigation';

function App(): React.JSX.Element {
  return (
    <NavigationContainer>
      <RootStack />
    </NavigationContainer>
  );
}

export default App;

这里我把 RootStack 放到 navigation 目录下,为了方便管理。

为了使用@作为 src 目录别名,需要安装下面的依赖并做一下配置。

bash 复制代码
yarn add --dev babel-plugin-module-resolver

在 babel.config.js 文件中添加下面 plugins 配置

javascript 复制代码
module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    [
      'module-resolver',
      {
        root: ['./src'],
        alias: {
          '@': './src',
        },
      },
    ],
  ],
};

为了让 vscode 能够友好的提示,在 tsconfig.json 文件中新增 compilerOptions 配置

json 复制代码
{
  "extends": "@react-native/typescript-config/tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

继续完成 RootStack

创建 navigation 模块,导出 RootStack

tsx 复制代码
import React from 'react';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import HomeScreen from '@/screens/HomeScreen';
import ProfileScreen from '@/screens/ProfileScreen';

const Stack = createNativeStackNavigator();

function RootStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options={{title: 'Welcome'}}
      />
      <Stack.Screen name="Profile" component={ProfileScreen} />
    </Stack.Navigator>
  );
}

export default RootStack;

创建 HomeScreen、ProfileScreen

tsx 复制代码
import React from 'react';
import {Button, Text, View} from 'react-native';

const HomeScreen = ({navigation}) => {
  return (
    <View>
      <Text> HomeScreen </Text>
      <Button
        title="Go to Jane's profile"
        onPress={() => {
          navigation.navigate('Profile', {name: 'Jane'});
        }}
      />
    </View>
  );
};

export default HomeScreen;
tsx 复制代码
import React from 'react';
import {Text} from 'react-native';

const ProfileScreen = ({route}) => {
  return <Text>This is {route.params.name}'s profile</Text>;
};

export default ProfileScreen;

运行查看效果:

如果运行 ios,如果安装了原生相关的依赖,都需要再重新运行前,执行一下 ios 相关的命令

bash 复制代码
npx pod-install

bash 复制代码
cd ios
pod install
cd ..

然后运行 React Native 启动命令

bash 复制代码
yarn start

建议再开一个终端窗口,执行启动模拟器

bash 复制代码
yarn ios
# 或
yarn android

演示效果:
到此已在 App 中完成最基本的路由导航

路由导航的 TS 类型

在上面的例子中,发现部分代码的 ts 类型报错

下面继续完善路由导航,添加类型文件

在 navigation 模块下创建 type.ts,导出 RootStackParamList 类型

ts 复制代码
// src/navigation/type.ts
import {NativeStackScreenProps} from '@react-navigation/native-stack';

export type RootStackParamList = {
  Home: undefined;
  Profile: {name: string};
};

export type HomeScreenProps = NativeStackScreenProps<
  RootStackParamList,
  'Home'
>;

export type ProfileScreenProps = NativeStackScreenProps<
  RootStackParamList,
  'Profile'
>;

declare global {
  namespace ReactNavigation {
    /**
     * 全局导航参数列表
     */
    interface RootParamList extends RootStackParamList {}
  }
}

修改 navigation/index.tsx ,给 createNativeStackNavigator 方法添加 RootStackParamList 类型

ts 复制代码
const Stack = createNativeStackNavigator<RootStackParamList>();

修改 HomeScreen、ProfileScreen,添加各自的类型

tsx 复制代码
const HomeScreen = ({navigation}: HomeScreenProps) => {
  ...
};
tsx 复制代码
const ProfileScreen = ({route}: ProfileScreenProps) => {
  ...
};

现在已经没有了 screen 的 ts 类型报错了,并且在使用导航器的 navigation 和 route 的时候,还会有友好的代码提示。

Tabs 导航 和 Drawer 导航

一般 app 首页都会有底部 Tabs 导航和 Drawer 导航,下面通过修改前面的代码,将 HomeScreen 和 ProfileScreen 改为 Tabs 导航页面,并添加 Drawer

安装 tabs 导航依赖

bash 复制代码
yarn add @react-navigation/bottom-tabs @react-navigation/drawer

要使用 @react-navigation/drawer,还需要额外安装下面的依赖

bash 复制代码
yarn add react-native-gesture-handler react-native-reanimated

创建首页底部 Tabs 导航

创建下面页面,并把之前的 HomeScreen 和 ProfileScreen 移动到 HomeTabs 目录下

tsx 复制代码
// src/screen/HomeTabs/index.tsx
import React from 'react';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import HomeScreen from './HomeScreen';
import ProfileScreen from './ProfileScreen';
import {HomeTabsParamList} from '@/navigation/types';

const BottomTabNavigator = createBottomTabNavigator<HomeTabsParamList>();

const HomeTabs = () => {
  return (
    <BottomTabNavigator.Navigator initialRouteName="Home">
      <BottomTabNavigator.Screen
        name="Home"
        component={HomeScreen}
        options={{title: '首页'}}
      />
      <BottomTabNavigator.Screen
        name="Profile"
        component={ProfileScreen}
        options={{title: '我的'}}
      />
    </BottomTabNavigator.Navigator>
  );
};

export default HomeTabs;

修改导航根节点

jsx 复制代码
import React from 'react';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {RootStackParamList, RoowDrawerStackParamList} from './types';
import HomeTabs from '../screens/HomeTabs/index';
import {createDrawerNavigator} from '@react-navigation/drawer';
import AboutScreen from '@/screens/Settings/AboutScreen';

const Stack = createNativeStackNavigator<RootStackParamList>();
const RootDrawerStack = createDrawerNavigator<RoowDrawerStackParamList>();

function RootStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="HomeTabs" component={HomeTabs} />
      <Stack.Group>
        <Stack.Screen name="About" component={AboutScreen} />
      </Stack.Group>
    </Stack.Navigator>
  );
}

function RootDrawerWrapper() {
  return (
    <RootDrawerStack.Navigator>
      <RootDrawerStack.Screen name="MainDrawer" component={RootStack} />
    </RootDrawerStack.Navigator>
  );
}

export default RootDrawerWrapper;

此处新增了一个 About 页面

jsx 复制代码
import {AboutScreenProps} from '@/navigation/types';
import {Text, View, Button} from 'react-native';

const AboutScreen = ({navigation}: AboutScreenProps) => {
  return (
    <View>
      <Text> AboutScreen </Text>

      <Button
        title="open main drawer"
        onPress={() => {
          navigation.openDrawer();
        }}
      />
    </View>
  );
};

export default AboutScreen;

修改 navigation 的类型文件

ts 复制代码
import {BottomTabScreenProps} from '@react-navigation/bottom-tabs';
import {DrawerScreenProps} from '@react-navigation/drawer';
import {
  CompositeScreenProps,
  NavigatorScreenParams,
} from '@react-navigation/native';
import {NativeStackScreenProps} from '@react-navigation/native-stack';

/**
 * 根堆栈导航的参数列表类型
 * @typedef RootStackParamList
 * @property {NavigatorScreenParams<HomeTabsParamList>} HomeTabs - 主页标签页导航参数
 * @property {undefined} About - 关于页面
 */
export type RootStackParamList = {
  HomeTabs: NavigatorScreenParams<HomeTabsParamList>;
  About: undefined;
};

/**
 * 根抽屉导航的参数列表类型
 * @typedef RoowDrawerStackParamList
 * @property {undefined} MainDrawer - 主抽屉页面
 */
export type RoowDrawerStackParamList = {
  MainDrawer: undefined;
};

/**
 * 根堆栈屏幕属性类型
 * 组合了原生堆栈导航属性和抽屉导航属性
 * @template T - 根堆栈参数列表中的键
 */
export type RootStackScreenProps<T extends keyof RootStackParamList> =
  CompositeScreenProps<
    NativeStackScreenProps<RootStackParamList, T>,
    DrawerScreenProps<RoowDrawerStackParamList>
  >;

/**
 * 主页标签页导航的参数列表类型
 * @typedef HomeTabsParamList
 * @property {undefined} Home - 主页
 * @property {undefined} Profile - 个人资料页
 */
export type HomeTabsParamList = {
  Home: undefined;
  Profile: undefined;
};

/**
 * 主页屏幕属性类型
 * 组合了底部标签页导航属性和根堆栈导航属性
 */
export type HomeScreenProps = CompositeScreenProps<
  BottomTabScreenProps<HomeTabsParamList, 'Home'>,
  RootStackScreenProps<'HomeTabs'>
>;

/**
 * 个人资料页屏幕属性类型
 * 组合了底部标签页导航属性和根堆栈导航属性
 */
export type ProfileScreenProps = CompositeScreenProps<
  BottomTabScreenProps<HomeTabsParamList, 'Profile'>,
  RootStackScreenProps<'HomeTabs'>
>;

/**
 * 关于页面的屏幕属性类型
 */
export type AboutScreenProps = RootStackScreenProps<'About'>;

declare global {
  namespace ReactNavigation {
    /**
     * 全局导航参数列表
     */
    interface RootParamList extends RootStackParamList {}
  }
}

演示效果:

业务场景中场景的模态框,比如登录的弹窗

在 react-navigation 中,模态框的创建跟普通页面一致

只需要在 screenOptions 里设置 presentation 属性

tsx 复制代码
<Stack.Group screenOptions={{presentation: 'modal'}}>
  <Stack.Screen name="MyModal" component={ModalScreen} />
</Stack.Group>

下面我们来创建一个登录的模态框效果

tsx 复制代码
// screens/Login/OnePressLoginModal.tsx
import {OnePressLoginModalProps} from '@/navigation/types';
import {Button, StyleSheet, TouchableWithoutFeedback, View} from 'react-native';

const OnePressLoginModal = ({navigation}: OnePressLoginModalProps) => {
  return (
    <View style={styles.container}>
      <TouchableWithoutFeedback
        onPress={() => {
          navigation.goBack();
        }}>
        <View style={styles.mask} />
      </TouchableWithoutFeedback>

      <View style={styles.modal}>
        <Button
          title="一键登录"
          onPress={() => {
            console.log('一键登录');
          }}
        />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-end',
  },
  mask: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
  },
  modal: {
    backgroundColor: 'white',
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    padding: 16,
    height: '40%',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default OnePressLoginModal;

在根节点导航器内添加 OnePressLoginModal

tsx 复制代码
function RootStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="HomeTabs" component={HomeTabs} />
      <Stack.Group>
        <Stack.Screen name="About" component={AboutScreen} />
      </Stack.Group>
      <Stack.Group
        screenOptions={{
          presentation: 'transparentModal',
          headerShown: false,
          animation: 'fade_from_bottom',
        }}>
        <Stack.Screen
          name="OnePressLoginModal"
          component={OnePressLoginModal}
          options={{
            contentStyle: {backgroundColor: 'transparent'},
          }}
        />
      </Stack.Group>
    </Stack.Navigator>
  );
}

在首页添加按钮,触发跳转

tsx 复制代码
<Button
  title="open one press login modal"
  onPress={() => {
    navigation.navigate('OnePressLoginModal');
  }}
/>

演示效果:

美化导航

使用 iconfont 图标库,为底部 tab 导航添加图标

安装必要的依赖:

bash 复制代码
yarn add react-native-svg

图标转换工具:

bash 复制代码
yarn add -D react-native-iconfont-cli-2

react-native-iconfont-cli-2 是我 fork react-native-iconfont-cli 做的修改

react-native-iconfont-cli 好像已经没维护了,当前版本会出现 defaultProps 的报错

所以自己 fork 做了修复并发布为 react-native-iconfont-cli-2

初始化和使用方式与 react-native-iconfont-cli 一致。

修改底部 tab 导航图标与样式

在之前的代码中修改 HomeTabs 代码,添加 screenOptions 参数

tsx 复制代码
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import HomeScreen from './HomeScreen';
import ProfileScreen from './ProfileScreen';
import {HomeTabsParamList} from '@/navigation/types';
import {RouteProp} from '@react-navigation/native';
import {IconHome, IconProfile} from '@/components/Iconfont';

const BottomTabNavigator = createBottomTabNavigator<HomeTabsParamList>();

const HomeTabs = () => {
  return (
    <BottomTabNavigator.Navigator
      initialRouteName="Home"
      screenOptions={({route}) => {
        return {
          headerShown: false, // 隐藏头部
          tabBarActiveTintColor: '#000', // 选中颜色
          tabBarInactiveTintColor: '#999', // 未选中颜色
          tabBarActiveBackgroundColor: '#fff', // 选中背景色
          tabBarInactiveBackgroundColor: '#fff', // 未选中背景色
          tabBarLabelStyle: {
            fontSize: 14,
          },
          tabBarIcon: getTabBarIcon(route),
        };
      }}>
      <BottomTabNavigator.Screen
        name="Home"
        component={HomeScreen}
        options={{title: '首页'}}
      />
      <BottomTabNavigator.Screen
        name="Profile"
        component={ProfileScreen}
        options={{title: '我的'}}
      />
    </BottomTabNavigator.Navigator>
  );
};

const getTabBarIcon =
  (route: RouteProp<HomeTabsParamList, keyof HomeTabsParamList>) =>
  (props: {focused: boolean; color: string; size: number}) => {
    switch (route.name) {
      case 'Home':
        return <IconHome color={props.color} size={24} />;
      case 'Profile':
        return <IconProfile color={props.color} size={24} />;
      default:
        return null;
    }
  };

export default HomeTabs;

效果:

优化刘海屏和底部小白条

当我们给 screenOptions 设置 headerShown: false 后

我们需要给页面做兼容处理

我们前面已经有安装了所需依赖:

react-native-safe-area-context

先给 app.tsx 加一层 SafeAreaProvider

tsx 复制代码
import React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import RootStack from '@/navigation';
import {SafeAreaProvider} from 'react-native-safe-area-context';

function App(): React.JSX.Element {
  return (
    <SafeAreaProvider>
      <NavigationContainer>
        <RootStack />
      </NavigationContainer>
    </SafeAreaProvider>
  );
}

export default App;

然后在需要兼容的页面,加上一层 SafeAreaView

tsx 复制代码
import {HomeScreenProps} from '@/navigation/types';
import {Button, StyleSheet, Text, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';

const HomeScreen = ({navigation}: HomeScreenProps) => {
  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.content}>
        <Text> HomeScreen </Text>
        <Button
          title="open main drawer"
          onPress={() => {
            navigation.openDrawer();
          }}
        />

        <Button
          title="open AboutScreen"
          onPress={() => {
            navigation.navigate('About');
          }}
        />

        <Button
          title="open one press login modal"
          onPress={() => {
            navigation.navigate('OnePressLoginModal');
          }}
        />
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'red',
  },
  content: {
    backgroundColor: '#fff',
  },
});

export default HomeScreen;

现在看刚刚的首页效果:

我们给容器加上背景后,可以看到上下默认都有做了间距处理

默认是使用 padding,也可以通过 mode 参数修改为 margin

在 tab 页面,底部已经由 tabBar 处理了,所以我们的容器不需要再处理底部的间距

通过 edges 参数,控制需要的方向

tsx 复制代码
<SafeAreaView style={styles.container} edges={['top']}>

效果:


参考资料

目前已完成的模板代码都在这了,有需要可自行查看。github.com/ace0109/rea...

相关推荐
武当王丶也2 天前
React Native 状态管理:用 Jotai 替代 useState
前端·react native
武当王丶也2 天前
React Native 本地缓存:react-native-mmkv
前端·react native
武当王丶也2 天前
React Native 设备屏幕尺寸适配:react-native-size-matters
前端·react native
MshengYang_lazy3 天前
React Native离线级联选择器开发手记:当SQLite遇见小区房号选择
前端·react native·sqlite
No Silver Bullet4 天前
React Native进阶(六十一): WebView 替代方案 react-native-webview 应用详解
javascript·react native·react.js
ThinkPet6 天前
【003安卓开发方案调研】之ReactNative技术开发安卓
android·react native·react.js
消失的旧时光-19437 天前
浅谈跨平台框架的演变(H5混合开发->RN->Flutter)
android·开发语言·flutter·react native·跨平台
wen's7 天前
解决 React Native 0.76 中 com.facebook.react.settings 插件缺失问题
react native·react.js·facebook
努力的搬砖人.8 天前
React相关面试题
react native·react.js·面试·reactjs·reactnative