何时使用
- 用 Expo Router 构建有「原生质感」的 React Native 应用,需要导航(Stack/原生 Tabs)、原生控件、动画、Header/搜索栏、表单 Sheet、模糊/液态玻璃等特效的现行写法。
- 需要判断「Expo Go 够不够,还是必须自定义原生构建」。
- 触发词:Expo、Expo Router、React Native、NativeTabs、expo-router、_layout.tsx、formSheet。
不该用的边界:
- 纯原生 iOS(Swift/SwiftUI)→ 用
ios-swiftui-developer/swiftui-best-practices;纯原生 Android(Kotlin/Compose)。 - 与 Expo 无关的 React Native 工程架构/状态管理整体决策 → 用
react-native-architecture。 - 纯 Web/H5、纯后端、与 UI 无关的任务。
- 本技能给结构与代码模式,不替代真机/模拟器运行、测试与专家评审;缺少必要输入时先停下澄清。
步骤
- 先跑 Expo Go,别急着自定义构建(关键约束):
npx expo start扫码进 Expo Go 充分测试。绝大多数expo-*包、Expo Router、reanimated、推送、深链都开箱即用。 - 仅在这些情况才需要自定义构建(
npx expo run:ios/android或eas build):本地 Expo 模块(modules/自定义原生代码)、Apple targets(小组件/扩展,@bacons/apple-targets)、Expo Go 不含的第三方原生模块、app.json表达不了的原生配置。自定义构建更慢、要 Xcode/Android Studio,不确定就先 Expo Go。 - 搭路由:路由放
app/目录,用_layout.tsx定义 Stack/Tabs;保证存在匹配/的路由(可在 group 内)。绝不在app/里同放组件/类型/工具(反模式);移动路由时删旧文件。 - 写 UI:根组件套 ScrollView 保证响应式;遵循 Apple HIG;用内联样式(不支持 CSS/Tailwind)。
- 加原生细节:导航标题用 Stack 而非页面里的自定义文本;多用
Link.Preview与上下文菜单;iOS 上条件性用expo-haptics。
指令
库选型(用新弃旧):
expo-audio/expo-video(不用expo-av);expo-image的source="sf:name"渲染 SF Symbols(不用expo-symbols/@expo/vector-icons)。react-native-safe-area-context(不用 RN 的SafeAreaView);expo-glass-effect做液态玻璃。process.env.EXPO_OS(不用Platform.OS);React.use(不用React.useContext)。- 绝不用已从 RN 移除的
Picker/WebView/SafeAreaView/AsyncStorage与 legacyexpo-permissions。 - 绝不用
img/div等内在元素(除非在 WebView 或 Expo DOM 组件里);图片用expo-image的Image。
响应式与安全区:
- 用
<ScrollView contentInsetAdjustmentBehavior="automatic" />替代SafeAreaView,FlatList/SectionList 同样加该属性;同时兼顾上下安全区。 - 布局用 flexbox,不用 Dimensions API;量屏幕用
useWindowDimensions()(不用Dimensions.get())。 - Stack 下的路由,第一个子元素几乎总应是带
contentInsetAdjustmentBehavior="automatic"的 ScrollView,且 ScrollView 几乎总是路由组件内第一个组件。
样式规则:
- 优先 flex
gap而非 margin/padding;能 padding 就别 margin;圆角用{ borderCurve: 'continuous' }(胶囊形除外)。 - 内联样式优先于
StyleSheet.create(除非复用更省);状态变化加 entering/exiting 动画。 - 给 ScrollView 加内边距用
contentContainerStyle的 padding/gap(减少裁剪),别加在 ScrollView 本身。 - 阴影用 CSS
boxShadow属性,绝不用 legacy RN shadow/elevation;支持inset。 - 重要数据/错误文本加
selectable;计数器用{ fontVariant: 'tabular-nums' };大数字格式化为 1.4M、38k。
代码风格: 文件名 kebab-case(如 comment-card.tsx)、无特殊字符;import 置顶;注意转义嵌套反引号与引号;tsconfig.json 配路径别名并优先用别名。
导航: 路由间用 expo-router 的 <Link href="/path" />,包裹自定义组件用 asChild;Stack 用 expo-router/stack,总是用 _layout.tsx 定义;标题用 <Stack.Screen options={{ title: "Home" }} />;搜索栏优先 Stack.Screen 的 headerSearchBarOptions。
示例
带原生 Tabs + 共享 group 的标准布局:
// app/_layout.tsx
import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs";
import { Theme } from "../components/theme";
export default function Layout() {
return (
<Theme>
<NativeTabs>
<NativeTabs.Trigger name="(index)">
<Icon sf="list.dash" />
<Label>Items</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(search)" role="search" />
</NativeTabs>
</Theme>
);
}
// app/(index,search)/_layout.tsx —— 两个 tab 共享、可各自 push 公共屏
import { Stack } from "expo-router/stack";
import { PlatformColor } from "react-native";
export default function Layout({ segment }) {
const screen = segment.match(/\((.*)\)/)?.[1]!;
const titles: Record<string, string> = { index: "Items", search: "Search" };
return (
<Stack screenOptions={{
headerTransparent: true, headerShadowVisible: false,
headerLargeStyle: { backgroundColor: "transparent" },
headerTitleStyle: { color: PlatformColor("label") },
headerLargeTitle: true, headerBlurEffect: "none",
headerBackButtonDisplayMode: "minimal",
}}>
<Stack.Screen name={screen} options={{ title: titles[screen] }} />
<Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} />
</Stack>
);
}
模态 / 表单 Sheet:
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
<Stack.Screen name="sheet" options={{
presentation: "formSheet",
sheetGrabberVisible: true,
sheetAllowedDetents: [0.5, 1.0],
contentStyle: { backgroundColor: "transparent" }, // iOS 26+ 背景变液态玻璃
}} />
优先用上述原生 modal/sheet,而非手写自定义弹窗组件。
Link 上下文菜单 + 预览:
<Link href="/settings" asChild>
<Link.Trigger><Pressable><Card /></Pressable></Link.Trigger>
<Link.Menu>
<Link.MenuAction title="Share" icon="square.and.arrow.up" />
<Link.MenuAction title="Block" icon="nosign" destructive />
</Link.Menu>
<Link.Preview />
</Link>
阴影(CSS boxShadow,非 legacy):
<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />
源仓库还附 references/ 专题(按需查阅):animations / controls / form-sheet / gradients / icons / media / route-structure / search / storage / tabs / toolbar-and-headers / visual-effects / webgpu-three / zoom-transitions。
注意事项
- 构建复杂度:自定义构建(run:ios/android、eas build)显著增加复杂度、拖慢迭代、需配 Xcode/Android Studio;不确定一律先 Expo Go。
- 不支持 CSS/Tailwind,统一内联样式;阴影只用
boxShadow,安全区只走contentInsetAdjustmentBehavior/Header/Tabs,别用SafeAreaView。 - iOS 优先:原生 Tabs、
headerSearchBarOptions、Link.Preview、液态玻璃、PlatformColor("label")等多为 iOS 体验,Android 需验证降级。 - 弃用 API 漂移快:动手前先核对现行包名与写法(用新弃旧表)。
- 本技能给结构与模式,仅当任务清晰匹配范围时使用;不替代环境内的运行验证、测试与专家评审,缺输入/权限/成功标准时先停下澄清。
互见
- requires:无。
- related:
react-native-architecture(不绑 Expo 的 RN 工程架构与状态管理整体决策,本技能是其 Expo Router UI 子领域);ios-swiftui-developer、swiftui-best-practices(纯原生 iOS 替代路线,跨平台 Expo 之外的另一选择)。 - combines_with:
react-state-management(Expo Router 页面内的 React 状态/数据流方案,与本技能的路由/UI 结构叠加);react-native-architecture(先用本技能搭 Expo Router 的导航与原生 UI,再用它定大盘架构与分层)。
采编自 sickn33/antigravity-awesome-skills(MIT 许可)。
适配提示:原任务给的
domain=研发/misc中的misc不在taxonomy.json中 02-engineering(研发)的受控类目(frontend/backend/mobile/review/testing/devops/observability/architecture),且仓库已归一为「0 misc」。落盘 frontmatter 时建议domain: 研发/mobile(与同源 ios-swiftui-developer / flutter-expert 一致),否则scripts/build-index.mjs的 class 级校验会报 error。