@[toc]
如果你回顾一下 RN 项目里所有"导航相关的事故",你会发现它们几乎都长得一样:
- 返回行为不可预测
- 某些页面"看得见但回不去"
- Modal、Tab、Stack 混在一起后,没人敢再改
- 新人一加页面,就把导航结构搞乱
但这些问题,其实不是因为 React Navigation 难用,而是因为:
大多数 RN 项目,一开始就没有"导航分层模型"。
而没有模型的系统,规模一上来,一定失控。
一、为什么"能跑"不等于"可扩展"
我们先说一个 RN 项目最常见的起点。
tsx
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" />
<Stack.Screen name="Detail" />
<Stack.Screen name="Profile" />
</Stack.Navigator>
</NavigationContainer>
这个结构的问题不在于"写错",而在于:
- 所有页面地位是一样的
- 所有页面返回权责是一样的
- 所有页面生命周期语义是一样的
一开始你感觉不到问题,但只要加上:
- 登录态
- Tab
- Modal
- 半屏页
- 全屏流程页
混乱是必然的,不是概率事件。
二、一个关键认知:导航不是页面的集合,而是"层级系统"
我们先把一个错误认知彻底掰正:
Navigation ≠ 页面列表
正确的理解应该是:
Navigation = 层级 + 流程 + 状态机
这句话如果你真的理解了,后面所有设计都会变得非常自然。
三、不会失控的核心原则:先分"层",再谈"页"
我在多个中大型 RN 项目里总结了一套非常稳定的分层模型,你可以直接套用。
四个必分的导航层级
text
App Root Layer 应用级
│
├─ Flow Layer 业务流程级
│
├─ Section Layer 功能区级
│
└─ Page Layer 页面级
我们一层一层拆。
四、App Root Layer:应用级导航,只做一件事
职责定义
App Root 层只负责一件事:
当前 App 处于哪种"全局状态"
典型只有几种:
- 未登录
- 已登录
- 强制更新
- 冷启动引导
正确结构示例
tsx
function RootNavigator() {
const isLogin = useAuthStore(state => state.isLogin)
return (
<NavigationContainer>
{isLogin ? <MainNavigator /> : <AuthNavigator />}
</NavigationContainer>
)
}
注意一个非常重要的点:
Root 层不要用
navigate切换状态而是用 条件渲染
这是 RN 和 Web 路由思维最大的分水岭之一。
五、Flow Layer:最容易被忽略,但最重要的一层
Flow 层解决的是:
"这一组页面,是不是一个不可随意打断的流程?"
比如:
- 登录流程
- 支付流程
- 新手引导
- 多步表单
错误做法(90% 项目都这么干)
tsx
navigate('Login')
navigate('Verify')
navigate('SetPassword')
问题是:
- 用户可以返回中途状态
- 流程被随意打断
- 状态恢复极其痛苦
正确做法:Flow 独立成 Stack
tsx
const AuthStack = createNativeStackNavigator()
function AuthNavigator() {
return (
<AuthStack.Navigator>
<AuthStack.Screen name="Login" />
<AuthStack.Screen name="Verify" />
<AuthStack.Screen name="SetPassword" />
</AuthStack.Navigator>
)
}
然后在 Flow 完成时:
ts
setIsLogin(true)
直接切换 Root 层,而不是"返回首页"。
这一步非常关键。
六、Section Layer:Tab、Drawer 的正确定位
Section 层解决的是:
用户当前在"哪一块功能区域"
典型就是:
- 首页
- 消息
- 我的
正确的模型
tsx
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeStack} />
<Tab.Screen name="Message" component={MessageStack} />
<Tab.Screen name="Profile" component={ProfileStack} />
</Tab.Navigator>
这里有一个非常重要的工程规范:
Tab 下必须是 Stack,而不是页面
为什么?
因为:
- 每个功能区一定会"越走越深"
- 返回语义要限定在"当前区块"
七、Page Layer:页面不应该知道"全局导航"
页面这一层,反而是最简单的。
页面应该知道的
- 自己的入参
- 自己的副作用
- 自己是否需要拦截返回
页面不应该知道的
- 自己属于哪个 Tab
- 上一页是谁
- 返回后去哪
正确的页面返回声明方式
ts
useFocusEffect(
React.useCallback(() => {
const onBack = () => {
if (hasUnsavedData) {
Alert.alert('未保存')
return true
}
return false
}
BackHandler.addEventListener('hardwareBackPress', onBack)
return () =>
BackHandler.removeEventListener('hardwareBackPress', onBack)
}, [hasUnsavedData])
)
页面只做一件事:
声明:我是否允许被返回
而不是:
决定:返回要怎么走
八、Modal / Overlay:不要污染主栈
一个非常常见的错误是:
tsx
navigate('ModalPage')
然后 Modal 就被塞进了业务栈。
正确做法:单独的 Overlay 层
tsx
const RootStack = createNativeStackNavigator()
<RootStack.Navigator>
<RootStack.Screen name="Main" component={MainTabs} />
<RootStack.Screen
name="Modal"
component={ModalPage}
options={{ presentation: 'modal' }}
/>
</RootStack.Navigator>
这样做的好处是:
- Modal 生命周期清晰
- 返回逻辑天然正确
- 不污染业务栈深度
九、Demo:一个"不会失控"的完整导航结构
结构图(文字版)
text
Root
├─ AuthFlow
│ └─ Login → Verify → SetPassword
└─ MainTabs
├─ HomeStack
│ └─ List → Detail
├─ MessageStack
└─ ProfileStack
返回行为会自动满足:
- Android 返回优先退出当前 Page
- Page 结束 → Stack pop
- Stack 空 → 留在当前 Tab
- Tab 切换 ≠ 返回
- Flow 完成 → Root 切换
几乎不需要写额外返回逻辑。
十、工程级检查清单(非常重要)
你可以直接用这个 checklist 来 review 项目:
- 是否存在"页面直接跳 Root"的代码?
- 是否有页面能 navigate 到不属于自己层级的页面?
- Tab 下是否存在直接挂页面的情况?
- Modal 是否混进了业务 Stack?
- Android BackHandler 是否集中管理?
如果有任意一条是 Yes,项目后期一定会疼。
最后一句总结
导航设计不是 API 使用问题,而是"页面模型"的工程能力。
当你把:
- 层级
- 职责
- 生命周期
- 返回权责
一次性想清楚,
RN 的导航复杂度会直接下降一个数量级。