设计可扩展的 React Native + Expo Router 文件夹结构
Source: Dev.to
文件夹概览
src/
├─ app
├─ components
├─ config
├─ hooks
├─ lib
├─ providers
├─ screens
└─ utils
app/ — 将路由视为第一类公民
Expo Router 在路由反映用户流程,而不是技术捷径时表现最佳。
app/
├─ (authenticated)
├─ (home-tabs)
├─ (unauthenticated)
├─ _layout.tsx
└─ index.tsx
(unauthenticated)
- 登录、OTP、引导页
- 没有标签页,没有干扰
- 为认证守卫提供清晰的边界
(authenticated)
- 登录后的入口点
- 处理应用级布局、重定向和全局状态
(home-tabs)
- 只放真正属于底部标签页的屏幕
- 其他(模态框、流程、详情页)放在标签页之外
流程: 未认证 → 已认证 → 基于标签页的首页 → 非标签页流程
没有猜测,没有意外的标签页嵌套,没有路由乱七八糟。
components/ — 设计系统,而非随意复用
结构遵循原子化设计,并以务实方式应用:
components/
├─ atoms
├─ molecules
├─ organisms
└─ templates
- Atoms – 纯粹、可复用、可测试的 UI 基元
- Molecules – 带有意图的小组合
- Organisms – 具备特性意识的 UI 块
- Templates – 布局模式(不是完整屏幕)
好处
- 应用整体 UI 一致性
- 当设计系统演进时,重构更轻松
- 组件保持可复用,而不会沦为通用的垃圾抽屉
lib/ — 应用的大脑(而非随意堆放)
lib/ 有意进行结构化:
lib/
├─ auth
├─ backend
├─ implementation
├─ interface
└─ vector-icon
backend/
- API 客户端(Axios / fetch 包装)
- TanStack Query 客户端设置
- 服务器状态 Hook
- 后端数据模型
- 明确的契约(interface / implementation)
- 与平台无关的抽象——易于 mock、测试或替换
示例
// lib/backend/_models/...
// lib/backend/server-state/queries/useGetThoughtOfDayApi.ts
// lib/backend/server-state/query-client.ts
// lib/backend/supabase/supabase-client.ts
// lib/backend/supabase/supabase-safe-call.ts
// lib/backend/supabase-db/fetch-thought-of-day.ts
auth/
- 认证状态、提供者和边界集中在一起
- 没有认证逻辑泄漏到 UI 中
当 API 变化、切换后端提供商或需要离线测试时,这种分离会带来极大收益。
screens/ — 屏幕不是路由
screens/
├─ authenticated
└─ unauthenticated
- 屏幕包含 UI + 屏幕级状态。
- 路由(
app/)只决定 何时 显示某个屏幕。
结果:屏幕可迁移;路由是声明式的。
utils/, hooks/, providers/ — 支撑规模化
| 目录 | 目的 |
|---|---|
| utils | 纯逻辑,无 React 依赖——易于测试和信任。 |
| hooks | 应用特定行为——不是伪装成 Hook 的通用工具。 |
| providers | 主题、查询客户端、安全区、全局应用上下文——为全局关注点提供唯一可信来源。 |
为什么此结构具备可扩展性
- 适用于多个团队——所有权清晰。
- 降低新工程师的认知负担。
- 支持基于特性的增长,无需重写。
- 同样适用于 React Native 和 Web(Expo)。
最重要的是,它映射的是用户在应用中的移动路径,而不是框架内部的组织方式。
最后思考
- 框架会演进。
- 产品需求会变化。
- 团队会壮大。
一个好的文件夹结构不会与之对抗——它会吸收这些变化。
专业提示
保持屏幕组件精简。让屏幕专注于组合和导航,而让各个子组件负责自己的状态、自定义 Hook,以及靠近使用位置的 API/TanStack Query 逻辑。
希望这能帮助任何在设计生产级 Expo Router 应用的人。欢迎分享你们的项目结构!