Chapter 03

Stack:最基础的一堆屏

Stack 就是原生 iOS UINavigationController / Android Fragment 的映射——新屏推上栈,返回弹出。Expo Router 的 Stack 底层是 @react-navigation/native-stack,完全原生动画。

最简 Stack

// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return <Stack />;
}

就这一行。所有同级 .tsx 自动变成 Stack 的 Screen,title 默认取文件名。

配置单个 Screen

// app/product/[id].tsx
import { Stack } from 'expo-router';
import { View, Text } from 'react-native';

export default function Product() {
  return (
    <>
      <Stack.Screen
        options={{
          title: '商品详情',
          headerStyle: { backgroundColor: '#000020' },
          headerTintColor: '#fff',
          presentation: 'modal',  // modal / card / transparentModal
        }}
      />
      <View><Text>...</Text></View>
    </>
  );
}
Stack.Screen 放哪里
两个等效位置:屏幕组件里面(方便就近配置),或 _layout.tsx 里(集中管理)。实际项目推荐「共性在 _layout,个性在屏幕内」。

screenOptions 全局默认

export default function RootLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: '#fff' },
        headerTitleStyle: { fontWeight: '600' },
        headerBackTitle: '',              // iOS 去掉返回文字
        animation: 'slide_from_right',
        contentStyle: { backgroundColor: '#f5f5f5' },
      }}
    />
  );
}

常用 options

option说明
titleHeader 标题
headerShown是否显示 Header(默认 true)
headerLeft / headerRight自定义左/右按钮,支持函数返回组件
headerTransparent沉浸式 Header(内容会到状态栏下)
headerLargeTitleiOS 大标题(滚动收起)
presentationcard / modal / transparentModal / containedModal
animationdefault / slide_from_right / slide_from_bottom / fade / none
gestureEnabled是否允许手势返回
fullScreenGestureEnablediOS 全屏手势返回

自定义 Header 按钮

<Stack.Screen
  options={{
    title: '编辑',
    headerRight: () => (
      <Pressable onPress={handleSave}>
        <Text style={{ color: '#000020', fontWeight: '600' }}>保存</Text>
      </Pressable
    ),
    headerLeft: () => (
      <Pressable onPress={() => router.back()}>
        <Text>取消</Text>
      </Pressable
    ),
  }}
/>

Modal 呈现

app/
├── _layout.tsx
├── index.tsx
└── login.tsx         ← 这屏当作 modal 呈现
// app/_layout.tsx 集中配置
<Stack>
  <Stack.Screen name="index" options={{ title: '首页' }} />
  <Stack.Screen
    name="login"
    options={{
      presentation: 'modal',
      animation: 'slide_from_bottom',
      title: '登录',
    }}
  />
</Stack>

// 用 Link 或 router 触发
<Link href="/login">登录</Link>

嵌套 Stack

app/
├── _layout.tsx              ← Root Stack(管理全局模态)
├── index.tsx
└── account/
    ├── _layout.tsx          ← Account Stack(账户中心子栈)
    ├── index.tsx
    ├── profile.tsx
    └── security.tsx
// app/account/_layout.tsx
import { Stack } from 'expo-router';

export default function AccountLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: '#0F0A2E' },
        headerTintColor: '#fff',
      }}
    />
  );
}

账户中心的头部样式只作用于 /account/* 下面的屏,外面仍然是 Root 的样式。

动态标题

import { useLocalSearchParams, Stack } from 'expo-router';

export default function Product() {
  const { id } = useLocalSearchParams();
  const product = useProduct(id as string);

  return (
    <>
      <Stack.Screen options={{ title: product?.name ?? '加载中' }} />
      ...
    </>
  );
}

去掉 header,自己画

<Stack.Screen options={{ headerShown: false }} />

// 自己的 SafeAreaView + 顶部栏
<SafeAreaView style={{ flex: 1 }}>
  <MyHeader title="..." onBack={router.back} />
  ...
</SafeAreaView>

拦截返回

import { useFocusEffect } from 'expo-router';
import { BackHandler, Alert } from 'react-native';
import { useCallback } from 'react';

useFocusEffect(
  useCallback(() => {
    const sub = BackHandler.addEventListener('hardwareBackPress', () => {
      Alert.alert('确认退出?');
      return true;  // 拦截
    });
    return () => sub.remove();
  }, [])
);

路由命名与分组(preview)

文件名用 (group) 包裹表示「路由分组」——URL 路径不含它,但布局可以独享:

app/
├── _layout.tsx                 ← Root Stack
├── (marketing)/
│   ├── _layout.tsx             ← 营销区 Stack(大标题)
│   ├── home.tsx                ← /home(不是 /marketing/home)
│   └── about.tsx               ← /about
└── (app)/
    ├── _layout.tsx             ← App 区 Stack(小标题)
    ├── feed.tsx                ← /feed
    └── profile.tsx             ← /profile

下一章讲 Tabs/Drawer 会深入用到分组。

本章小结