要点
栈导航表达进入与返回,标签表达并列入口。路由参数适合传资源 ID,不适合承载整份可变业务对象。
⚠️ 适用范围:本篇 React Navigation 直接用法适用于 bare 工程 / React Navigation 模板工程;Expo 主线工程使用内置 expo-router(等价写法对照见下文),自 Expo SDK 56 起官方不再支持从应用代码直接 import
@react-navigation/*包——与本模块 README 技术基线 一致。
安装
⚠️ 适用范围(与 README 技术基线 一致):本篇直接安装并 import
@react-navigation/*包的写法适用于 bare 工程(RN CLI)与 React Navigation 模板工程。Expo 主线工程(create-expo-app默认模板)的导航由内置的 expo-router 承担——自 Expo SDK 56 起不再支持从应用代码直接 import@react-navigation/*包,两者混装是 expo-doctor 会标记的非预期配对。Expo 工程的学习者请通过本篇理解导航器模型,动手时使用下文的 expo-router 等价写法,不要在本节的安装步骤上操作。
# 核心包(仅 bare 工程 / React Navigation 模板工程需要)
npm install @react-navigation/native
1. 三端原生依赖(bare 工程用 npm install;prebuild 的 Expo 工程可用 npx expo install 对齐版本)
npm install react-native-screens react-native-safe-area-context
1. 按导航器类型追加安装
npm install @react-navigation/native-stack # 原生栈
npm install @react-navigation/bottom-tabs # 底部标签
npm install @react-navigation/drawer # 抽屉(额外依赖 react-native-gesture-handler、react-native-reanimated)
安装完成后在入口文件用 NavigationContainer 包裹整棵应用树:
// App.tsx
import { NavigationContainer } from '@react-navigation/native';
export default function App() {
return <NavigationContainer>{/* 导航器在此嵌套 */}</NavigationContainer>;
}
Expo 主线工程(expo-router)等价写法:expo-router 不需要 NavigationContainer,也无需手工安装导航包,把上述概念映射到文件路由即可:
| 本篇 React Navigation 概念 | expo-router 等价实现 |
|---|---|
NavigationContainer 包裹 | app/_layout.tsx 根布局(内置容器) |
createNativeStackNavigator | app/_layout.tsx 中的 <Stack> |
createBottomTabNavigator | app/(tabs)/_layout.tsx 中的 <Tabs> |
navigation.navigate('Detail', { id }) | router.push('/detail/42') 或 <Link href> |
route.params | useLocalSearchParams() |
完整对照与用法见 Expo 要点 — expo-router 与 生态集成。
核心概念:导航器的嵌套模型
定义: React Navigation 中每个导航器(Navigator)管理一组页面(Screen),导航器之间可以嵌套——这是搭建复杂结构的基本手段。
典型结构:外层原生栈做”启动页 → 主界面 → 详情页”的跳转,主界面内嵌底部标签做一级分区。
NativeStack
├── Splash(启动页)
├── Main(内嵌 BottomTabs)
│ ├── Home
│ ├── Discover
│ └── Profile
└── Detail(详情页,可接收参数)
组合实战:Stack + Tabs
// navigation/RootNavigator.tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Home from '../screens/Home';
import Discover from '../screens/Discover';
import Profile from '../screens/Profile';
import Detail from '../screens/Detail';
type RootStackParamList = {
Main: undefined;
Detail: { itemId: string; from?: string };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator();
// 一级分区:底部标签
function MainTabs() {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="Home" component={Home} />
<Tab.Screen name="Discover" component={Discover} />
<Tab.Screen name="Profile" component={Profile} />
</Tab.Navigator>
);
}
// 外层:原生栈
export default function RootNavigator() {
return (
<Stack.Navigator initialRouteName="Main">
<Stack.Screen name="Main" component={MainTabs} options={{ headerShown: false }} />
<Stack.Screen
name="Detail"
component={Detail}
options={{ title: '详情' }}
/>
</Stack.Navigator>
);
}
抽屉导航同理,把 MainTabs 换成 Drawer.Navigator 的内容即可;抽屉需在入口引入 gesture-handler:
// App.tsx 顶部(drawer 方案必须)
import 'react-native-gesture-handler';
页面传参与回调
// screens/Detail.tsx
import { View, Text, Pressable } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
type Props = NativeStackScreenProps<RootStackParamList, 'Detail'>;
export default function Detail({ route, navigation }: Props) {
const { itemId, from } = route.params;
return (
<View style={{ flex: 1, padding: 16 }}>
<Text>收到参数: {itemId}{from ? `(来自${from})` : ''}</Text>
{/* 2. 跳转传参 */}
<Pressable onPress={() => navigation.push('Detail', { itemId: 'next' })}>
<Text>再压一层 Detail</Text>
</Pressable>
{/* 3. 返回上一页 */}
<Pressable onPress={() => navigation.goBack()}>
<Text>返回</Text>
</Pressable>
</View>
);
}
// 发起跳转的一侧
navigation.navigate('Detail', { itemId: '42', from: 'Home' });
navigate vs push: push 每次都在当前栈压入一个新 screen;navigate 的目标解析会受当前导航树、嵌套 navigator 与已有路由状态影响,通常适合“前往某功能”,但不要用“它一定回到同名页”推断复杂嵌套的行为。详情页套详情页可用 push;需要精确恢复或替换时,写测试或查看当前 navigation state。
三端导航差异
| 差异点 | Android | iOS | 鸿蒙 |
|---|---|---|---|
| 返回手段 | 物理返回键/手势 | 边缘滑动手势 | 侧滑/手势导航 |
| 页面切换动画 | 由导航器配置与系统行为共同决定 | 由导航器配置与系统行为共同决定 | 以选定 RNOH 发行版与 ArkUI 壳工程实际行为为准 |
| 头部样式 | Material 风格 | 大标题/毛玻璃 | 鸿蒙设计规范 |
| 深链 | intent-filter 配置 | Universal Links | want 隐式跳转配置 |
处理 Android 物理返回键:
import { BackHandler } from 'react-native';
import { useEffect } from 'react';
useEffect(() => {
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
// 返回 true 表示已自行处理(拦截),false 交给默认返回逻辑
return false;
});
return () => sub.remove();
}, []);
栈导航器基于 react-native-screens 使用原生页面容器,返回手势/返回键默认已被正确处理;仅自定义”二次返回退出”等逻辑时才需要手动监听。
最佳实践
导航结构应反映任务关系:列表到详情属于返回栈,平行功能可用 tabs,临时任务可用 modal。没有适用于所有应用的三层上限;检查返回键、深链接和状态恢复能否按预期工作,比分层数量更重要。
路由参数尽量传稳定 id 与必要可序列化值,详情从数据层读取,避免携带过期的大对象。导航和动画依赖按安装版本配置;出现双导航栏时检查每层导航器的显示责任,而不是到处关闭 header。
常见问题
Q1: 为什么跳转后页面是空白?
A: 检查 Screen 的 component 是否真的渲染了内容;再检查是否漏装 react-native-screens/safe-area-context;iOS 新工程还需重跑 pod install。
Q2: 从推送点进来直接打开某页面(冷启动深链)怎么做?
A: 给 NavigationContainer 配 linking 映射 URL 与路由,完整 API 见 navigation-essentials。
Q3: 标签栏想中间凸起一个”发布”按钮?
A: 用 Tab.Screen 自定义 tabBarButton 渲染自定义按钮,样式细节参考 02-components-props。
练习与实践
练习一:三层导航骨架
任务要求: 1. 搭建 Stack + BottomTabs 结构,Tabs 含首页/发现/我的三个页面 2. 首页列表点击跳转 Detail 并传递 id,Detail 内再 push 一层 Detail
评估标准: 返回手势/返回键在每一层都表现正确。
练习二:Tab 间传值
任务要求: 1. “我的”页面修改昵称后,切回”首页”能看到最新昵称 2. 通过全局 store 或 Context 实现(路由参数无法跨 Tab)
提示: Tab 页面是否保持挂载、是否冻结由导航器与 screen 配置决定。不要依赖“切换 tab 必然不重跑 effect”;昵称应来自共享状态或数据层,这样无论页面保活、重挂载还是深链进入,首页都能读取最新值。