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...

相关推荐
恋猫de小郭7 小时前
React Native 前瞻式重大更新 Skia & WebGPU & ThreeJS,未来可期
android·javascript·flutter·react native·react.js·ios
zwjapple1 天前
“ES7+ React/Redux/React-Native snippets“常用快捷前缀
javascript·react native·react.js
程序猿阿伟2 天前
《探索React Native社交应用中WebRTC实现低延迟音视频通话的奥秘》
react native·音视频·webrtc
十步杀一人_千里不留行3 天前
【实战教程】React Native项目集成Google ML Kit实现离线水表OCR识别
react native·react.js·ocr
程序猿阿伟3 天前
《社交应用架构生存战:React Native与Flutter的部署容灾决胜法则》
flutter·react native·架构
流星雨在线4 天前
react naive 网络框架源码解析
网络·react native
老猿阿浪5 天前
突破测试环境文件上传带宽瓶颈!React Native 阿里云 OSS 直传文件格式问题攻克一
react native·阿里云
小妖怪的夏天7 天前
React Native 动态切换主题
javascript·react native·react.js
zhangguo20028 天前
react native和react在跨端架构上有什么区别?
javascript·react native·react.js
阿珊和她的猫8 天前
React Native 开发环境搭建:从零开始
javascript·react native·react.js