路由组织的经验
app/
├── _layout.tsx ← 全局 Provider + 守卫
├── +not-found.tsx
├── +native-intent.ts ← 自定义 deep link 解析(可选)
│
├── (auth)/ ← 未登录区
│ ├── _layout.tsx
│ ├── login.tsx
│ ├── register.tsx
│ └── forgot.tsx
│
├── (app)/ ← 登录态区
│ ├── _layout.tsx
│ ├── (tabs)/ ← 底部栏
│ │ ├── _layout.tsx
│ │ ├── home/
│ │ │ ├── _layout.tsx
│ │ │ ├── index.tsx
│ │ │ └── [id].tsx
│ │ ├── search/
│ │ └── profile.tsx
│ ├── settings/ ← tab 外的深层页(带 header)
│ │ ├── _layout.tsx
│ │ ├── index.tsx
│ │ ├── account.tsx
│ │ └── billing.tsx
│ └── product/
│ └── [id].tsx ← 公共详情(多入口跳转)
│
└── api/ ← Server Routes(可选)
└── webhook+api.ts
经验:
- 共享页(多入口跳转)放 (app) 根:比如商品详情从首页、搜索都能跳,就不要放
(tabs)/home/里,放(app)/product/[id].tsx让它从任意 tab 推出。 - 把 header 样式的共性提到 _layout.tsx:每屏都设一遍
headerTintColor是噪音。 - modal 放 Root Stack:全局登录、权限请求这类 modal 不在 (app) 子栈里,放顶层让它盖过 tab 栏。
埋点:监听导航事件
// app/_layout.tsx import { useSegments, usePathname } from 'expo-router'; import { useEffect } from 'react'; import analytics from '../lib/analytics'; function Analytics() { const pathname = usePathname(); const segments = useSegments(); useEffect(() => { analytics.track('screen_view', { pathname, group: segments[0], // (auth) / (app) }); }, [pathname]); return null; }
所有页面切换会自动触发——无需在每个屏手写。Amplitude、Mixpanel、PostHog 都能这样接。
错误边界
Expo Router 内置 ErrorBoundary——在 _layout.tsx 同级 export 一个叫 ErrorBoundary 的组件,路由内的错误会被它兜住:
// app/_layout.tsx export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) { return ( <View> <Text>出错了:{error.message}</Text> <Pressable onPress={retry}><Text>重试</Text></Pressable> </View> ); } export default function RootLayout() { ... }
Sentry / 错误上报
npx expo install sentry-expo @sentry/react-native
// app/_layout.tsx import * as Sentry from 'sentry-expo'; Sentry.init({ dsn: 'https://xxx@sentry.io/yyy', enableInExpoDevelopment: false, debug: __DEV__, tracesSampleRate: 0.1, }); export default Sentry.Native.wrap(RootLayout);
Sentry 会自动关联 OTA update-id、设备信息、breadcrumb(路由切换、fetch 请求),崩溃能还原到具体路由。
A/B 与 Feature Flag
import { useFeatureFlag } from '../lib/flags'; export default function Home() { const enabled = useFeatureFlag('new-home-layout'); return enabled ? <NewHome /> : <OldHome />; }
推荐 GrowthBook / Statsig / LaunchDarkly 客户端 SDK——分桶、实验结果和 Mixpanel 打通。
性能:避免不必要的重渲染
@shopify/flash-list 回收更聪明,长列表滑动 60fps 不卡。API 兼容。Image 后滚动 jank 显著下降。启动时间优化
// app/_layout.tsx —— lazy 加载重组件 import { lazy, Suspense } from 'react'; const Chart = lazy(() => import('../components/Chart')); <Suspense fallback={<ActivityIndicator />}> <Chart /> </Suspense>
Expo Router v4 会自动按路由切分 bundle,冷启动不加载全部屏幕代码。你要做的是把路由内的「大组件/重依赖」再 lazy 一次。
可访问性(a11y)
<Pressable accessibilityRole="button" accessibilityLabel="购买,价格 99 元" accessibilityHint="双击打开结算页" onPress={buy} > <Text>立即购买</Text> </Pressable>
iOS VoiceOver、Android TalkBack 会读这些。图标按钮必须给 label;动态状态(已收藏 vs 未收藏)用 accessibilityState。
国际化
pnpm add i18next react-i18next expo-localization
import i18n from 'i18next'; import { initReactI18next, useTranslation } from 'react-i18next'; import * as Localization from 'expo-localization'; i18n.use(initReactI18next).init({ lng: Localization.getLocales()[0].languageCode, fallbackLng: 'en', resources: { zh: { ... }, en: { ... } }, }); const { t } = useTranslation(); <Text>{t('home.greeting', { name: user.name })}</Text>
测试
@testing-library/react-native。路由部分:expo-router/testing-library 提供 renderRouter。常见大型 App 问题
router.replace 限深;或深层用 router.dismissTo('/home') 一次弹多层。router.back。根屏按返回默认退出 App,如需「确认退出」,用 BackHandler + Alert。fullScreenGestureEnabled: false,或屏内 GestureHandler 的 simultaneousHandlers 协调。KeyboardAwareScrollView 或 react-native-keyboard-controller 让页面自动滚。EAS Update 灰度
# 把 10% 的生产用户切到 experiment branch eas channel:edit production --branch-mapping \ '[{"branchId":"experiment-id","rolloutPercentage":10},{"branchId":"stable-id"}]'
出问题直接回到 100% stable,几分钟见效。
发布清单
- ✅ app.json 的 version / buildNumber 确认递增
- ✅ Sentry DSN、Analytics 写到
extra,通过Constants.expoConfig?.extra读 - ✅ Universal Links 两个 .well-known 文件在生产域名可访问
- ✅ 商店信息(截图、描述、关键词)按地区准备
- ✅ 隐私政策 / 用户协议链接在 App 内可达(苹果硬要求)
- ✅ iOS ITMS-90078(缺 NSCameraUsageDescription 等)提前检查
- ✅ Android Data Safety 声明(Google 要)
- ✅ 崩溃率、启动时长基线埋好,OTA 后 A/B 对比
Expo Router 的未来
- Server Functions GA:写
'use server'函数,前端直接调,打通 BFF - React Server Components:数据 fetching 挪到服务端,Native 端也能享有
- Module Federation:多 App 共用一套 bundle,Super-App 场景
- 视觉开发工具:Expo 有持续投入的 Figma → code 工具链
结语
十章读完,你应该能从 npx create-expo-app 造出一个自带 Stack/Tabs/Drawer、支持 Universal Links、三端运行、EAS 打包 + OTA 发布的完整产品。
Expo Router 做的其实只是一件事:让 Native 开发拥有 Web 级别的开发速度。配合 Expo 全家桶,一两个人做出一个有模有样的跨端 App,这在 2020 年还是不可想象的事。
写代码的乐趣之一,就是见证工具持续化繁为简。2026 年的 RN 生态,真的不再是那个「iOS 一套 Android 一套心智分裂」的时代了。