导航与手势
Screen nav 与 deep link。
导航与手势
这一课你将能做到
- 用 React Navigation 搭建「外层 Tab + 内层 Stack」的标准 App 骨架
- 在页面间跳转、传递参数、回传数据,并自定义导航栏
- 配置深链接让外部 URL 直达 App 内指定页面,理解 gesture-handler 在手势体系中的作用
为什么要学
App 的本质就是「页面 + 页面之间的流转」。没有导航,你的 App 永远只有一屏。React Navigation 是 RN 事实上的标准导航库——打开任意一个 RN 项目的 package.json,十有八九能看到它。这一课学的不只是 API,更是「一个正经 App 的骨架应该怎么搭」。
人话:Stack 导航像一叠卡片,新页面盖在旧页面上,返回就是抽走最上面那张;Tab 导航像电视遥控器下方的频道按钮,点哪个切哪个,各频道互不干扰、各自保留进度。真实 App 几乎都是「Tab 定骨架、Stack 管层级」的嵌套结构。
先建立图像
NavigationContainer(唯一根容器)
└── Tab.Navigator(底部标签:首页 / 我的)
├── HomeStack(首页频道内部的页面栈)
│ ├── HomeMain(首页列表)
│ └── Detail(详情页)
└── ProfileStack(我的频道内部的页面栈)
├── ProfileMain(个人主页)
└── Settings(设置页)
每个频道是一个独立的 Stack,切 Tab 不会丢失各自的浏览进度——你在「首页」点到第 3 层详情,切到「我的」再切回来,还在第 3 层。这就是嵌套导航的意义。
跟着做
步骤 1:安装依赖
npx expo install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs react-native-screens react-native-safe-area-context react-native-gesture-handler
一次装 6 个包别慌:@react-navigation/native 是核心,native-stack 和 bottom-tabs 是两种导航器,react-native-screens 和 react-native-gesture-handler 是把页面切换和手势放到原生线程执行的底座,safe-area-context 负责导航栏高度适配。它们是一个套装,缺一不可。
步骤 2:最小 Stack 导航
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Button, Text, View } from 'react-native';
const Stack = createNativeStackNavigator();
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>首页</Text>
<Button title="去详情页" onPress={() => navigation.navigate('Detail', { id: 42 })} />
</View>
);
}
function DetailScreen({ route }) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>详情页,收到 id = {route.params.id}</Text>
</View>
);
}
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} options={{ title: '首页' }} />
<Stack.Screen name="Detail" component={DetailScreen} options={{ title: '详情' }} />
</Stack.Navigator>
</NavigationContainer>
);
}
被 Stack.Screen 注册的组件会自动收到两个 props:navigation(管跳转)和 route(收参数,route.params 里就是传过来的对象)。不用写任何样式,顶部导航栏、标题、iOS 返回箭头和右滑返回手势全都自带。
步骤 3:跳转的四种方式
navigation.navigate('Detail', { id: 1 }); // 常规跳转:目标页已在栈中则回到它
navigation.push('Detail', { id: 2 }); // 强制再压一层新实例
navigation.goBack(); // 返回上一页
navigation.popToTop(); // 一路退回到栈底(首页)
navigate 和 push 的区别是最容易踩的坑:从商品 A 的详情页点相关推荐跳商品 B 的详情页,用 navigate 会复用 A 的实例(数据不更新),必须用 push 压一个新层。自己跳自己的场景一律 push。
步骤 4:Tab + Stack 嵌套(真实 App 骨架)
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
const HomeStack = createNativeStackNavigator();
function HomeStackScreen() {
return (
<HomeStack.Navigator>
<HomeStack.Screen name="HomeMain" component={HomeScreen} options={{ title: '首页' }} />
<HomeStack.Screen name="Detail" component={DetailScreen} options={{ title: '详情' }} />
</HomeStack.Navigator>
);
}
export default function App() {
return (
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="HomeTab" component={HomeStackScreen} options={{ title: '首页' }} />
<Tab.Screen name="ProfileTab" component={ProfileScreen} options={{ title: '我的' }} />
</Tab.Navigator>
</NavigationContainer>
);
}
结构口诀:Tab 在最外层定骨架,每个 Tab 内部各包一个 Stack 管自己的页面层级。详情页想全屏(不显示底部 Tab 栏),就把它放到外层 Stack 而不是 Tab 内部。
步骤 5:回传数据给上一页
// 详情页:返回前把结果合并到上一页的 params
navigation.navigate({ name: 'HomeMain', params: { updated: true }, merge: true });
navigation.goBack();
// 列表页:监听 params 变化触发刷新
useEffect(() => {
if (route.params?.updated) {
fetchList();
navigation.setParams({ updated: false }); // 消费掉,避免重复刷新
}
}, [route.params?.updated]);
merge: true 表示把新参数合并进目标页已有的 params,而不是整个替换。注意这适合「通知一下」的轻量场景;如果是复杂数据共享,该上 Context 或状态库(第 6 课)。
步骤 6:子组件里拿导航 + 自定义导航栏
没有被 Screen 直接注册的深层组件,用 Hook 拿导航对象:
import { useNavigation, useRoute } from '@react-navigation/native';
function ShareButton() {
const navigation = useNavigation();
return <Button title="去设置" onPress={() => navigation.navigate('Settings')} />;
}
<Stack.Screen
name="Detail"
component={DetailScreen}
options={{
title: '商品详情',
headerRight: () => <Button title="分享" onPress={() => {}} />,
headerTintColor: '#fff', // 标题和返回按钮颜色
headerStyle: { backgroundColor: '#007bff' }, // 导航栏背景
headerShown: true, // false 则完全隐藏,自己画
}}
/>
步骤 7:深链接——外部 URL 直达内页
用户点了短信里的 myapp://product/42,希望直接打开商品 42 的详情页:
const linking = {
prefixes: ['myapp://', 'https://myapp.example.com'],
config: {
screens: {
HomeTab: { screens: { Detail: 'product/:id' } }, // 嵌套路径按导航层级写
ProfileTab: 'profile',
},
},
};
<NavigationContainer linking={linking}>...</NavigationContainer>
URL 里的 :id 会自动变成 route.params.id。这正是「params 必须可序列化」的原因——深链接进来时,params 是从 URL 字符串解析出来的。
讲清楚要点
- NavigationContainer 是唯一根:整个 App 只能有一个,包在最外层,深链接配置也挂在它身上。
- navigate vs push:navigate 对已在栈中的页面会复用旧实例,push 永远新开一层。详情跳详情必须用 push。
- 参数必须可序列化:route.params 里别传函数、类实例、Date 对象。深链接和状态恢复都要求 params 能 JSON 化,回调类需求用全局状态或事件。
- native-stack vs stack:
native-stack 映射到系统的 UINavigationController / Fragment,转场原生、性能好;@react-navigation/stack 是纯 JS 实现,自定义上限高。默认用 native-stack。
- gesture-handler 是底座:iOS 右滑返回、抽屉手势、卡片转场全建立在它之上,手势识别跑在原生线程所以丝滑。它基本不用你直接写代码,但必须装、必须在入口正确引入。
- Tab 内页面想跳别的 Tab 的内页:支持嵌套寻址
navigation.navigate('ProfileTab', { screen: 'Settings' })。
你可能会懵的地方
-
为什么我的页面拿不到 navigation?
只有被 Stack.Screen / Tab.Screen 直接注册的组件才自动注入 props。它内部的子组件要用 useNavigation() / useRoute() Hook 获取。
-
Tab 页面跳详情时想隐藏底部 Tab 栏?
首选方案:把详情页放到外层 Stack(Tab 的上一层),它天然盖住 Tab 栏。次选:按路由动态设 tabBarStyle: { display: 'none' }。
-
goBack 不生效?
当前已经在栈底时无处可退。先用 navigation.canGoBack() 判断,不能退就给个替代动作(比如跳首页)。
-
返回时想传一个回调函数给上一页?
别传函数(违反可序列化原则)。用步骤 5 的 params + merge 回传数据,或用 Context / 事件发射器。
-
页面从栈里退出后,它的 state 还在吗?
不在了。出栈即卸载,state 全部销毁。需要保留的(草稿、滚动位置)要自己存到上一层或全局。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 忘记包 NavigationContainer | 白屏崩溃 | 最外层包一个且仅一个 |
| params 里传函数回调 | 深链接/状态恢复时报错 | 传可序列化数据,回通用 params merge 或全局状态 |
| 详情页用 navigate 跳详情页 | 复用旧实例,数据不更新 | 用 push 压新层 |
| 子组件层层转发 navigation props | 中间组件全是样板代码 | 深层用 useNavigation / useRoute |
| 没装 react-native-screens | 安卓转场异常或白屏 | 按官方清单装齐依赖 |
| 隐藏导航栏后没留返回入口 | 用户被困死在页面 | 自绘返回按钮,且别禁用 gestureEnabled |
| 回传数据后没消费标记 | 每次回到列表页都重复刷新 | 用完 setParams 清掉标记 |
| 手写硬件返回键监听和导航打架 | 返回行为混乱 | 优先用导航自带的 beforeRemove |
卡点急救
- 跳转白屏无报错:检查 Screen 的
name 与 navigate 的字符串是否完全一致(区分大小写);确认依赖装齐后 npx expo start -c 清缓存重启。
- iOS 返回手势失效:多半自定义 header 时误设了
gestureEnabled: false,或手势被全屏自定义组件吃掉,逐层排查。
- Tab 切换页面重挂载丢状态:Tab.Navigator 设
lazy: false 让页面常驻;或把要保留的状态提升到 Context / 全局。
- 嵌套 navigate 报了「找不到 screen」:嵌套寻址的 name 路径要和导航层级严格对应,用
navigation.getState() 打印当前路由树排查。
面试常问
-
React Navigation 的 Stack 原理?
内部维护一个路由栈数组(state),navigate/push 入栈、goBack 出栈,UI 由这个 state 驱动渲染。native-stack 变体把这个栈映射到 iOS 的 UINavigationController 和 Android 的 Fragment 事务,所以转场动画、返回手势都是系统原生的,不是 JS 模拟的。
-
页面间通信有哪些方式?怎么选?
四种:route.params 正向传参(跳转时带数据)、navigate + merge 回传(编辑完成通知上一页)、Context / Zustand 全局状态(多页面共享的数据,如登录用户)、事件总线(解耦的广播通知)。简单父子页面用 params,跨分支共享用全局状态。
-
深链接(Deep Linking)怎么实现?
在 NavigationContainer 配 linking 字段:prefixes 声明 scheme/域名,config.screens 把 URL 路径映射到嵌套路由和参数。系统层 iOS 配 associated domains(Universal Links)、Android 配 intent-filter(App Links),Expo 里在 app.json 声明 scheme 即可开始联调。这也是 params 必须可序列化的根本原因。
自测三问
- navigate 和 push 的区别是什么?什么场景必须用 push?
- 为什么 route.params 不能传函数?想回调上一页该怎么办?
- 画出一个「首页/我的」双 Tab、首页内含列表和详情两层的导航结构树。
小结一句话
导航就是管理一个页面栈:Tab 定骨架、Stack 管层级、params 传参数、linking 接外部入口,再配上原生手势底座,App 的骨架就立起来了。
延伸