资讯动态

React Native Navigation页面路由实战:从初始化到深链跳转

发布时间:2026/9/9 2:57:17 来源:尧图企业网站定制
我第一次在 React Native 项目里搭 Navigation 页面路由时干过一件很蠢的事用useState控制页面切换。当时觉得页面少一个变量切来切去很爽结果还没上线就被打脸——用户按返回键直接退出 App列表页改完数据回到上一页不会刷新其它模块想跳转到某个页面只能到处传回调。后来老老实实把页面路由换成 React Navigation才理解“路由”这层抽象不是可有可无的。这篇是本系列第 02 篇我把 Navigation 页面路由从初始化、跳转传参到嵌套路由、深链跳转的完整落地经验写出来。适合刚学完 React Native 基础、准备做多页应用的人也适合在 uni-app 或 Flutter 里被“路由”概念绕晕的人参考。示例基于 React Navigation 6.x 和 React Native 0.7xTypeScript 写法。1. Navigation 页面路由到底替我们解决了什么问题1.1 没有路由时页面切换是怎么翻车的很多刚上手 RN 的同学会把页面切换写成这样const [currentPage, setCurrentPage] useStatehome | detail(home); return currentPage home ? ( Home onPress{() setCurrentPage(detail)} / ) : ( Detail onBack{() setCurrentPage(home)} / );页面只有两三个的时候这套写法看起来没什么问题。但一旦页面多起来你会立刻撞到几堵墙第一Android 的物理返回键根本不会管你的currentPage按一下直接退到桌面第二Home和Detail切换时如果Detail里有表单、滚动位置、请求结果切走再切回来就全部丢失第三外部通知想跳到“订单详情页”你只能层层传回调根本没法指定一个页面并带上参数。说白了页面路由解决的不是“怎么把组件渲染出来”而是“页面之间的历史栈、参数传递、生命周期状态、深链入口”这一整套问题。1.2 移动端路由和 Web 路由不是一回事我见过不少人把 Vue Router 的经验直接套到 React Native 上结果怎么想都不对。原因是 Web 路由和移动端 Navigation 有本质区别维度Web 路由移动端 Navigation核心载体URL导航栈Stack刷新页面重新加载整个应用页面组件实例保留在栈中返回行为浏览器 history 回退页面出栈触发 blur/focus 生命周期状态恢复依赖 URL 和服务端状态页面实例自身状态通常保留深链直接改 URL通过 linking 映射到页面名称和参数所以 React Navigation 里的navigate、push、goBack操作的对象是“屏幕实例”不是 URL。理解这一点后面很多 API 就不会觉得玄幻。1.3 动手前把版本和依赖确认好如果你用的是 Expo 管理项目推荐直接npx expo install react-navigation/native react-navigation/native-stack react-native-screens react-native-safe-area-context如果你用的是纯 React Native CLI先装基础依赖npm install react-navigation/native react-navigation/native-stack npm install react-native-screens react-native-safe-area-contextiOS 还要记得cd ios pod install。这里我特意选了native-stack而不是早期教程里常见的react-navigation/stack。前者底层走的是系统原生导航容器页面切换性能和内存占用都更好后者是 JS 实现可定制性更强但动画和性能都不如原生。绝大多数业务用native-stack就够了。2. 初始化 Navigation 容器最小路由骨架长什么样2.1 先注册一个 Stack.NavigatorReact Navigation 6 的页面路由最小结构是这样import { NavigationContainer } from react-navigation/native; import { createNativeStackNavigator } from react-navigation/native-stack; export type RootStackParamList { Home: undefined; Detail: { id: number; title?: string }; }; const Stack createNativeStackNavigatorRootStackParamList(); export default function App() { return ( NavigationContainer Stack.Navigator initialRouteNameHome Stack.Screen nameHome component{HomeScreen} / Stack.Screen nameDetail component{DetailScreen} / /Stack.Navigator /NavigationContainer ); }这段代码有三件事必须做对NavigationContainer必须包在 Navigator 最外层它负责维护全局导航状态每个Stack.Screen通过name在路由表里注册component指向页面组件initialRouteName指定 App 启动后第一个显示的页面。如果NavigationContainer忘包或者某个页面组件没注册就navigate过去最常见的报错就是The action NAVIGATE was not handled by any navigator。2.2 页面注册时的命名规范我踩过比较多的坑是路由命名随意。项目大了以后Detail、DetailPage、detail混在一起找人找半天。我的习惯是路由名用 PascalCase和组件名保持一致比如Home、OrderDetail、Profile。路由名是字符串标识不是显示标题。页面上方显示的标题通过options{{ title: 订单详情 }}单独设置。一个页面只能出现在一个 Navigator 的注册表里。如果同一个页面要在不同导航层级使用通常应该拆分成不同注册名或者在父级做嵌套跳转。Stack.Screen nameOrderDetail component{OrderDetailScreen} options{{ title: 订单详情 }} /2.3 初始路由和执行顺序initialRouteName只决定“首次挂载时显示哪个页面”。如果你之后用navigation.reset或深链跳到了别的页面再切回 App界面停在哪个页面取决于导航栈当前状态而不是这个配置。所以不要在业务代码里依赖initialRouteName做登录态判断。登录态拦截我会在后面的章节讲那里更稳。3. 页面间跳转与参数传递navigate 的完整细节3.1 navigate 不等于 pushStack 导航里有两个 API大家最容易搞混navigate和push。navigation.navigate(Detail, { id: item.id });navigation.push(Detail, { id: item.id });它们的区别很关键navigate会在当前栈里查找同名页面如果已经存在就回到那个页面并更新参数如果不存在才新开一个页面入栈。push不管有没有同名页面都会无条件新开一个页面压到栈顶。实际业务里的区别是这样的你在列表页进入详情页同一个商品被用户点两遍navigate不会叠出两个一模一样的详情页按返回能直接回列表但如果你用push就会叠两层用户要按两次返回才能回去体验很怪。方法行为推荐场景navigate栈里有同名页面则复用并回退没有则 push列表进详情、Tab 切换push无条件新建页面实例详情推荐位跳另一个详情、连续浏览场景3.2 参数传递的三种姿势页面跳转时传参是刚需。我最常用的三种方式基础参数走navigate的第二个参数navigation.navigate(Detail, { id: 1001, title: 商品名称, });当前页面要就地更新参数用setParamsnavigation.setParams({ title: 新的标题 });跨模块、需要全局共享的复杂数据不要塞进路由参数建议用 Context 或全局状态管理。一个红线路由参数必须可序列化。不要往里塞函数、React 组件、Date对象、Map这类东西。原因很简单React Navigation 要做状态持久化、深链拼接、页面恢复遇到不可序列化的数据会直接警告某些场景还会导致状态无法还原。3.3 用 TypeScript 把路由参数变成强类型如果你已经用了 TypeScript别把路由参数写成any。用一个统一的RootStackParamList类型约束之后写跳转代码时会有自动补全少很多低级错误。页面组件侧这样拿参数import type { NativeStackScreenProps } from react-navigation/native-stack; type Props NativeStackScreenPropsRootStackParamList, Detail; export default function DetailScreen({ navigation, route }: Props) { const { id, title } route.params ?? {}; // ... }注意我写了route.params ?? {}因为如果当前页面没有传任何参数route.params可能是undefined。类型定义里如果把某些页面的参数写成可选取参数时一定要做空值兜底否则线上会白屏。4. 返回、刷新与参数读取最常见的三个坎4.1 别总想着“返回时带参数”navigation.goBack()这个 API 是不接受参数的。很多人第一次写“详情页改完数据返回列表页要求刷新”时会下意识想goBack({ refresh: true })结果发现参数根本传不出去。这个坑的根因是返回操作只是把页面从栈顶移出去它并不负责向上一页传递数据。所以正确的思路不是“返回时带参数”而是“让上一页知道自己重新获得了焦点然后重新拉数据”或者把数据放到全局状态里。4.2 用 useFocusEffect 刷新页面数据React Navigation 给每个页面提供了焦点生命周期。页面从 A 推到 BA 会触发blur从 B 返回 AA 会触发focus。useFocusEffect就是在这个时机执行的 hook。import { useFocusEffect } from react-navigation/native; import { useCallback } from react; function OrderListScreen() { useFocusEffect( useCallback(() { fetchOrderList(); }, []) ); return null; }这里有个细节useFocusEffect的回调必须用useCallback包裹。否则每次渲染都会生成新函数导致 effect 反复执行甚至出现死循环。这比在“返回按钮”里写回调要稳得多。因为用户可能用系统返回手势、Android 物理返回键、或者从外部通知跳走再回来这些路径你都没办法一个个手动处理而useFocusEffect能覆盖所有回到当前页面的场景。4.3 用 navigate 回到已有页面并更新参数虽然goBack不能带参数但有一个实用技巧用navigate指向栈里已存在的页面React Navigation 会帮你回到那个页面并同时更新参数。// 详情页保存成功后回到列表页并通知刷新 navigation.navigate(OrderList, { refreshKey: Date.now(), });列表页里监听参数变化useEffect(() { if (route.params?.refreshKey) { fetchOrderList(); } }, [route.params?.refreshKey]);这个模式适合“只需要通知一次”的场景。如果每次返回都需要无脑刷新直接用useFocusEffect更省事。5. 嵌套路由、登录态拦截与深链跳转5.1 Tab 与 Stack 的嵌套层级不要搞反实际项目里很少只有一个 Stack。常见结构是底部 Tab 顶层 Stack用于登录页、全屏弹窗、以及需要隐藏 Tab 的二级页面。我的推荐层级是根 Stack 在外层底部 Tab 作为根 Stack 的一个 Screen具体业务页面如果也需要 Tab就放在 Tab 内部。const Tab createBottomTabNavigator(); function MainTabs() { return ( Tab.Navigator Tab.Screen nameHomeTab component{HomeScreen} / Tab.Screen nameProfileTab component{ProfileScreen} / /Tab.Navigator ); } const RootStack createNativeStackNavigator(); function RootNavigator() { return ( RootStack.Navigator RootStack.Screen nameMain component{MainTabs} / RootStack.Screen nameLogin component{LoginScreen} / RootStack.Screen nameOrderDetail component{OrderDetailScreen} / /RootStack.Navigator ); }为什么要这样嵌套因为登录页和订单详情页不应该看到底部 Tab。如果把它们塞进 Tab 内部你还得在每个页面手动隐藏 Tab很麻烦。5.2 登录态拦截React Navigation 没有“路由守卫”Vue Router 里有beforeEachReact Navigation 里没有等价的全局守卫。但它有navigationRef可以拿到全局导航实例然后监听路由状态变化来做拦截。先建一个全局导出的引用import { createNavigationContainerRef } from react-navigation/native; export const navigationRef createNavigationContainerRef();根组件里把 ref 传给 NavigationContainerNavigationContainer ref{navigationRef} RootNavigator / /NavigationContainer然后在需要拦截的地方监听useEffect(() { const sub navigationRef.addListener(state, () { const currentRoute navigationRef.getCurrentRoute(); if (currentRoute?.name Profile !isLogin) { navigationRef.navigate(Login); } }); return sub; }, [isLogin]);这种写法比“在跳转前检查登录态”更可靠因为它覆盖了所有进入该路由的途径包括深链、通知跳转、Tab 切换。实际使用时要注意别在state监听里做太重的逻辑每次路由变化都会触发。5.3 Deep Link让外部链接能直接打开某个页面Navigation 页面路由的另一个优点是深链支持比较完善。通过linking配置可以让myapp://order/1001这种链接直接打开订单详情页。const linking { prefixes: [myapp://, https://example.com], config: { screens: { Main: main, OrderDetail: order/:id, }, }, }; NavigationContainer linking{linking} RootNavigator / /NavigationContainer配置里order/:id中的:id会自动映射到route.params.id。这样通知推送里带一个业务链接用户点击后就能直接进到对应页面不再需要手动维护一套“根据通知类型跳转”的分发逻辑。6. 我踩过的 Navigation 页面路由相关坑6.1 列表项点击用 push 导致页面叠堆早期我在订单列表的点击事件里用了push结果是用户快速点同一个订单两次页面栈里凭空多出一个完全相同的订单详情。用户按返回时发现要退好几次还以为 App 卡了。后来统一改成navigate就好多了。只有“从详情推荐位再进另一个详情”这种确实需要叠页面的场景才应该用push。这一点适合在一开始就定成团队规范。6.2 从 Tab 内部跳转到根 Stack 页面失败嵌套导航最常见的报错是在HomeTab页面里直接执行navigation.navigate(OrderDetail)结果跳不过去控制台还报无法处理。原因是HomeTab的navigation是 Tab 导航器给的它会先在 Tab 导航器里找OrderDetail找不到就认为无法处理。正确做法是跳转时指定父级路径navigation.navigate(Main, { screen: HomeTab, params: { screen: OrderDetail }, });或者直接用全局navigationRef.navigate(OrderDetail)绕开当前 Tab 的导航器限制。这个坑不踩一次真的很难想起来。6.3 header 配置分散导致样式不统一如果你的每个页面都各自设置headerTitle、headerTintColor、headerStyle后期一旦要改全局主题就得一个页面一个页面改。建议在根 Navigator 上用统一的screenOptionsStack.Navigator screenOptions{{ headerBackTitle: 返回, headerTitleStyle: { fontWeight: 600 }, headerTintColor: #333, headerStyle: { backgroundColor: #fff }, }} 个别页面需要特殊标题再单独覆盖options。这样主题变更只动一处页面路由的可维护性会高很多。Navigation 页面路由这块内容我一直觉得“会用 API”只是开始真正值钱的是理解导航栈的行为和生命周期。等你把嵌套导航、深链、登录态拦截这些场景都过一遍再回头看useState切页面的写法就会发现路由抽象其实是在帮你把“页面流程”从业务代码里解耦出去。下一篇我打算写页面之间复杂状态同步的方案到时再接着聊。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价