React Native —— 2. 路由

社区今后主推的方案是一个单独的导航库react-navigation,它的使用十分简单。React Navigation 中的视图是原生组件,同时用到了运行在原生线程上的Animated动画库,因而性能表现十分流畅。此外其动画形式和手势都非常便于定制。

安装

首先执行以下命令安装 react-navigation所需要的依赖

bash 复制代码
yarn add @react-navigation/native

npx expo install react-native-screens react-native-safe-area-context

yarn add @react-navigation/native-stack @react-navigation/bottom-tabs
  • @react-natigation/native是路由的核心库
  • react-native-screens通过使用原生的导航控制器来替代 React Native 的默认导航器,从而提高屏幕切换的性能
  • react-native-safe-area-context用于处理不同设备上的安全区域(safearea)问题
  • @react-navigation/native-stack用于创建堆栈导航器
  • @react-navigation/bottom-tabs用于创建底部标签导航器

需要更多导航方式的可以查看 react-navigation 官网,里面还提供了 drawermodal等多种导航方式

编写 Screen

接下来我们想要实现一个有着四个页面的项目,其中 homemy是项目的 tabbar,在 home 页面可以跳转到 news/index 资讯列表页,在 news/index 点击一条资讯可以跳转到 news/detail资讯详情页面。

首先,我们按照下图新增四个页面。

接下来我们按步骤来完善我们的项目代码

一、组件基础代码编写

  1. screens/tabbar/home.tsx
tsx 复制代码
import { Button, Text, TouchableOpacity, View } from "react-native";

type HomeProps = {};

const Home: React.FC<HomeProps> = ({
  navigation
}) => {
  return (
    <View
      style={{
        flex: 1,
        alignItems: "center",
        justifyContent: "center",
        gap: 16,
      }}
    >
      <Text>这里是 Home</Text>
      <Button title="跳转到资讯列表" onPress={() => {
        navigation.navigate("News", {
          screen: "Index",
        });
      }}/>
    </View>
  );
};

export default Home;
  1. screens/tabbar/my.tsx
tsx 复制代码
import { Text, View } from "react-native";

type MyProps = {};

const My: React.FC<MyProps> = (props) => {
  return (
    <View
      style={{
        flex: 1,
        alignItems: "center",
        justifyContent: "center",
        gap: 16,
      }}
    >
      <Text>这里是 My</Text>
    </View>
  )
};

export default My;
  1. screens/news/index.tsx
tsx 复制代码
import { ScrollView, Text, TouchableOpacity } from "react-native";

type NewsIndexProps = {};

const NewsIndex: React.FC<NewsIndexProps> = ({ navigation }) => {
  const newsList = new Array(100).fill(0).map((_, index) => ({
    id: index,
    title: `新闻标题${index}`,
    content: `新闻内容${index}`,
  }));
  return (
    <ScrollView>
      {newsList.map((news) => (
        <TouchableOpacity
          key={news.id}
          style={{
            padding: 16,
            borderBottomColor: "#ccc",
            borderBottomWidth: 1,
          }}
          onPress={() => {
            navigation.navigate("Detail", {
              id: news.id,
              title: news.title,
              content: news.content,
            });
          }}
        >
          <Text>{news.title}</Text>
        </TouchableOpacity>
      ))}
    </ScrollView>
  );
};

export default NewsIndex;
  1. screens/news/detail
tsx 复制代码
import { Text, View } from "react-native";

type NewsDetailProps = {};

const NewsDetail: React.FC<NewsDetailProps> = ({ route, navigation }) => {
  return (
    <View
      style={{
        flex: 1,
        alignItems: "center",
        justifyContent: "center",
        gap: 16,
      }}
    >
      <Text>{route.params.id}</Text>
      <Text>{route.params.title}</Text>
      <Text>{route.params.content}</Text>
    </View>
  );
};

export default NewsDetail;

二、组装路由

navigations目录下,我们新建下图所示的三个 Stack,并根据步骤完善代码

这里我们新增了几个文件,用来组装我们的路由

  • RootStackScreen.tsx 根 Stack
  • TabScreen.tsx tabbar 页面
  • stack/NewsStackScreen.tsx 新闻类 Stack,这里新建了一个 stacks文件夹,如果有其他类的路由,也都可以放到这个文件夹里,用于和 Tabbar 的区分

接下来我们给这三个文件补充代码

  1. stacks/NewsStackScreen.tsx
tsx 复制代码
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import NewsIndex from "@/screens/news";
import NewsDetail from "@/screens/news/detail";

export type NewsParamList = {
  Index: undefined;
  Detail: {
    id: number;
    title: string;
    content: string;
  };
};

const News = createNativeStackNavigator<NewsParamList>();

function NewsScreen() {
  return (
    <News.Navigator>
      <News.Screen name="Index" component={NewsIndex} />
      <News.Screen name="Detail" component={NewsDetail} />
    </News.Navigator>
  );
}

export default NewsScreen;
  1. TabbarScreen.tsx
tsx 复制代码
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import Home from "@/screens/tabbar/home";
import My from "@/screens/tabbar/my";
import { AntDesign } from "@expo/vector-icons";

export type TabParamList = {
  Home: undefined;
  My: undefined;
};

const Tab = createBottomTabNavigator<TabParamList>();

function TabScreen() {
  return (
    <Tab.Navigator>
      <Tab.Screen
        name="Home"
        component={Home}
        options={{
          tabBarIcon: ({ focused }) => {
            return <AntDesign name="home" size={24} color={focused ? "#000" : "#ccc"} />;
          },
        }}
      />
      <Tab.Screen
        name="My"
        component={My}
        options={{
          tabBarIcon: ({ focused }) => {
            return <AntDesign name="user" size={24} color={focused ? "#000" : "#ccc"} />;
          },
        }}
      />
    </Tab.Navigator>
  );
}

export default TabScreen;
  1. RootStackScreen.tsx
tsx 复制代码
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import TabScreen, { TabParamList } from "./TabScreen";
import NewsScreen, { NewsParamList } from "./stacks/NewsStackScreen";

type StackParamHandler<T> = {
  screen: keyof T;
  params?: T[keyof T];
};

export type RootStackParamList = {
  Tab: StackParamHandler<TabParamList>;
  News: StackParamHandler<NewsParamList>;
};

const RootStack = createNativeStackNavigator<RootStackParamList>();

function RootStackScreen() {
  return (
    <NavigationContainer>
      <RootStack.Navigator screenOptions={{ headerShown: false }}>
        <RootStack.Screen name="Tab" component={TabScreen} />
        <RootStack.Screen name="News" component={NewsScreen} />
      </RootStack.Navigator>
    </NavigationContainer>
  );
}

export default RootStackScreen;

createNativeStackNavigator

createNativeStackNavigator 是一个函数,返回一个包含两个属性的对象:Screen 和 Navigator。它们都是用于配置导航器的 React 组件。Navigator 应该包含 Screen 元素作为其子元素,以定义路由的配置。

createNativeStackNavigator 函数还可以支持泛型,使得可以更灵活地指定导航器的参数类型。通过泛型,你可以指定每个屏幕组件的导航参数类型以及导航器的默认参数类型。

在上面的 stacks/NewsStackScreen.tsx 中,我们传入了 NewsParamList 用来规定该 Stack 中拥有那些页面和页面的参数,类型定义如下:

tsx 复制代码
export type NewsParamList = {
  Index: undefined;
  Detail: {
    id: number;
    title: string;
    content: string;
  };
};
const News = createNativeStackNavigator<NewsParamList>();

这里定义了 news/detail.tsx中接收 id, title, content 三个参数,那么我们在 news/index.tsx中跳转时,就需要传入这三个参数

tsx 复制代码
  onPress={() => {
    navigation.navigate("Detail", {
      id: news.id,
      title: news.title,
      content: news.content,
    });
  }}

路由嵌套

路由嵌套的官方文档地址在此 Nesting navigators | React Navigation

在上面的代码中,我们将 TabScreenstacks/NewsStackScreen.tsx作为子路由嵌套到 RootStackScreen.tsx 内,这里我们处理了一下 RootStackScreen的类型

tsx 复制代码
type StackParamHandler<T> = {
  screen: keyof T;
  params?: T[keyof T];
};

export type RootStackParamList = {
  Tab: StackParamHandler<TabParamList>;
  News: StackParamHandler<NewsParamList>;
};

const RootStack = createNativeStackNavigator<RootStackParamList>();

实际项目中,嵌套路由的组织会比这里的例子更加复杂

路由跳转

此时我们返回到 tabbar/home.tsx 文件,发现我们编写的

tsx 复制代码
  <Button title="跳转到资讯列表" onPress={() => {
    navigation.navigate("News", {
      screen: "Index",
    });
  }}/>

通过给定一个 screen 来确定跳转到对应的嵌套路由的页面。

而打开 news/index.tsx页面,发现我们在同一个 Stack 中,是不需要通过 screen这个参数的

tsx 复制代码
navigation.navigate("Detail", {
  news,
});

如果我们在嵌套路由中也不传入 screen那么就会打开目标栈的第一个页面

具体的页面跳转和传参可以查看官方文档Passing parameters to routes | React Navigation

三、修改 App.tsx

打开 App.tsx,将代码改成下面这样

tsx 复制代码
import { StatusBar } from "expo-status-bar";
import RootStackScreen from "./src/navigations/RootStackScreen";

export default function App() {
  return (
    <>
      <StatusBar style="auto" />
      <RootStackScreen />
    </>
  );
}

四、运行

执行命令 yarn start,运行成功后应该会显示如下,这里录制的 ios,android 可能会有些许差异。如果要抹除系统之间的差异,上方导航栏应该换成自定义导航栏。

类型

打开 news/detail.tsx 文件,可以发现navigationroute 提示找不到类型,因为这里我们给的类型声明是一个空的type

接下来我们需要解决类型提示的报错,打开官网Type checking with TypeScript | React Navigation章节

这里介绍了嵌套路由的类型写法,可以使用 CompositeScreenProps 来组织我们的类型

我们首先将 Tabbar 页面和其他 Stack 页面区分开来

Tab 页面类型

  1. RootStackScreen.tsx 中新增类型声明
tsx 复制代码
// tab 页面类型
export type CompositeTabScreenProps<T extends keyof TabParamList> = CompositeScreenProps<
  BottomTabScreenProps<TabParamList, T>,
  NativeStackScreenProps<RootStackParamList>
>;
  1. tabbar/home.tsx 页面中修改
tsx 复制代码
// type HomeProps = {};
type HomeProps = CompositeTabScreenProps<"Home">;

此时我们会发现 navigation 的类型提示已经消失,而且 Button onPressnavigation.navigate也可以正确进行类型推导了

stacks 页面

刚才解决了 tab 页面的类型,现在我们处理其他 stacks 页面的类型,这个步骤会比 tab 稍微麻烦一点

  1. RootStackScreen.tsx 中新增类型声明
tsx 复制代码
// 所有除了 tabbar 之外的页面
export type PageStackUnionType = NewsParamList;
  
// 其他页面类型
export type CompositePageScreenProps<
  T extends PageStackUnionType,
  K extends keyof T & string
> = CompositeScreenProps<NativeStackScreenProps<T, K>, NativeStackScreenProps<RootStackParamList>>;

这里我们增加了一个 PageStackUnionType,用处是将所有除了 Tab 的普通 Stack 放到一起。

这里我们只有一个 NewsParamList,如果以后我们新增了一组设置相关的页面,比如取名为 SettingsParamList,则可以这样来修改

tsx 复制代码
export type PageStackUnionType = NewsParamList | SettingsParamList;

如果有更多的页面加进来,只需要继续按照 SettingsParamList的写法来扩展 PageStackUnionType就可以。

  1. news/detail.tsx中修改
tsx 复制代码
// type NewsDetailProps = {};
type NewsDetailProps = CompositePageScreenProps<NewsParamList, "Detail">;

此时发现我们的 route 也可以通过类型来正确推导了

接下来大家在根据上面的例子将剩余的页面也修改掉,那么路由的类型章节就结束了,最后附上修改完类型的 RootStackScreen.tsx完整代码

tsx 复制代码
import { CompositeScreenProps, NavigationContainer } from "@react-navigation/native";
import { NativeStackScreenProps, createNativeStackNavigator } from "@react-navigation/native-stack";
import TabScreen, { TabParamList } from "./TabScreen";
import NewsScreen, { NewsParamList } from "./stacks/NewsStackScreen";
import { BottomTabScreenProps } from "@react-navigation/bottom-tabs";

// tab 页面类型
export type CompositeTabScreenProps<T extends keyof TabParamList> = CompositeScreenProps<
  BottomTabScreenProps<TabParamList, T>,
  NativeStackScreenProps<RootStackParamList>
>;

// 所有除了 tabbar 之外的页面
export type PageStackUnionType = NewsParamList;
  
// 其他页面类型
export type CompositePageScreenProps<
  T extends PageStackUnionType,
  K extends keyof T & string
> = CompositeScreenProps<NativeStackScreenProps<T, K>, NativeStackScreenProps<RootStackParamList>>;


type StackParamHandler<T> = {
  screen: keyof T;
  params?: T[keyof T];
};

export type RootStackParamList = {
  Tab: StackParamHandler<TabParamList>;
  News: StackParamHandler<NewsParamList>;
};

const RootStack = createNativeStackNavigator<RootStackParamList>();

function RootStackScreen() {
  return (
    <NavigationContainer>
      <RootStack.Navigator screenOptions={{ headerShown: false }}>
        <RootStack.Screen name="Tab" component={TabScreen} />
        <RootStack.Screen name="News" component={NewsScreen} />
      </RootStack.Navigator>
    </NavigationContainer>
  );
}

export default RootStackScreen;
相关推荐
Ratten2 小时前
03.TypeScript 常见泛型工具详解
typescript
木西4 小时前
React Native DApp 开发全栈实战·从 0 到 1 系列(eas构建自定义客户端)
react native·web3·app
烛阴6 小时前
TypeScript 函数重载入门:让你的函数签名更精确
前端·javascript·typescript
随笔记7 小时前
react中函数式组件和类组件有什么区别?新建的react项目用函数式组件还是类组件?
前端·react.js·typescript
葡萄城技术团队7 小时前
TypeScript 进阶必备!5 个实用工具类型,帮你写出更健壮的前端代码
typescript
定栓8 小时前
Typescript入门-对象讲解
前端·javascript·typescript
ssshooter19 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Lsx_1 天前
TypeScript 是怎么去查找类型定义的?
前端·javascript·typescript
wayne2141 天前
企业级 RN Android 完整 CI/CD 自动化解决方案
react native