Expo SDK 54+ 提供托管式 React Native 开发环境,包含基于文件的路由(Expo Router)、原生模块访问以及简化的构建工具链。本技能覆盖应用配置、根布局 Provider 模式以及关键 Expo/React Native 库的使用方式。
前置条件:使用 npx create-expo-app 创建项目,或在 package.json 中声明 Expo SDK 54+。
工作流
搭建新的 Expo 演示项目:
- 创建项目:npx create-expo-app [demo-name] --template blank-typescript
- 安装核心依赖:pnpm add expo-router expo-image expo-haptics react-native-reanimated react-native-gesture-handler react-native-safe-area-context @gorhom/bottom-sheet @shopify/flash-list lucide-react-native nativewind tailwindcss@3
- 配置 NativeWind(参见 nativewind 技能)
- 使用 Provider 栈设置根布局
- 在 app.json 中配置 scheme、name、splash
- 添加路由组和页面
- 运行:pnpm start(Expo 开发服务器)
新增库时:
- 使用 pnpm 安装:pnpm add [library]
- 检查是否需要在 app.json 中配置 Expo config plugin
- 若添加了原生模块,需重新构建开发客户端:npx expo prebuild
app.json 配置 演示应用的关键字段:
- expo.name:显示名称
- expo.slug:URL 安全标识符
- expo.scheme:深链 scheme(例如 myapp)
- expo.orientation:portrait(演示应用的默认值)
- expo.splash:启动屏配置
- expo.ios.bundleIdentifier:iOS bundle ID
- expo.android.package:Android 包名
- expo.plugins:Expo 配置插件(例如 expo-router)
根布局 Provider 模式 根 app/_layout.tsx 使用 Provider 包裹整个应用。标准嵌套顺序: GestureHandlerRootView (flex: 1) └── SafeAreaProvider └── ThemeProvider / Context └── Stack (Expo Router)
- GestureHandlerRootView 必须位于最外层(手势处理与底部弹层的要求)
- SafeAreaProvider 为所有后代提供安全区域 inset
- 应用级 Context Provider 放在 SafeAreaProvider 与 Stack 之间
- 自定义头部时使用 <Stack screenOptions={{ headerShown: false }} />
expo-image(替代 RN Image) 所有图片渲染都应使用 expo-image,它提供缓存、blurhash 占位符、内容适配模式和动画过渡。关键 props:
- source:URI 字符串或本地图片的 require()
- placeholder:加载状态使用的 blurhash 字符串
- contentFit:'cover' | 'contain' | 'fill'
- transition:淡入时长(毫秒,例如 300)
expo-haptics 为交互提供触觉反馈:
- Haptics.selectionAsync():用于选择、开关的轻触
- Haptics.impactAsync(ImpactFeedbackStyle.Medium):按钮按下、卡片点击
- Haptics.notificationAsync(NotificationFeedbackType.Success):操作完成 应克制使用——每次触摸都触发触觉反馈会令人反感。
安全区域 inset 处理设备刘海、状态栏和 Home 指示器:
- useSafeAreaInsets():返回以点为单位的 { top, bottom, left, right }
- 应用于屏幕容器:paddingTop: insets.top
- NativeWind 类名:使用 pt-[${insets.top}px] 或用 SafeAreaView 包裹
@gorhom/bottom-sheet 替代 Radix Dialog 的移动端模态模式:
- 用于详情视图、选择、筛选、表单
- 定义吸附点:snapPoints={['25%', '50%', '90%']}
- 背景遮罩:backdropComponent 配合点击关闭
- 弹层内可滚动内容使用 BottomSheetScrollView
- 需要 GestureHandlerRootView 作为祖先节点
FlashList(替代 FlatList) 来自 @shopify/flash-list 的高性能列表渲染:
- 可直接替换 FlatList,但必须提供 estimatedItemSize
- estimatedItemSize={80}:每项估算高度(点)
- 复用架构以实现流畅的 60fps 滚动
- 使用 contentContainerClassName 进行 NativeWind 样式设置
lucide-react-native React Native 图标库(与 Web 端 lucide-react 对应):
- 按需导入图标:import { Home, Settings, ChevronRight } from 'lucide-react-native'
- Props:size、color、strokeWidth
- 在移动端和 Web 代码库之间保持图标集一致
StatusBar 按屏幕配置状态栏外观:
- 浅色背景使用 <StatusBar style="dark" />
- 深色背景使用 <StatusBar style="light" />
- 从 expo-status-bar 导入
最佳实践
- 用 style={{ flex: 1 }} 的 GestureHandlerRootView 包裹根布局
- 所有图片使用 expo-image(缓存、blurhash、性能)
- 仅为主要操作添加触觉反馈(按钮、重要选择),而非每次触摸
- 为所有 FlashList 组件设置 estimatedItemSize
- Provider 放在根 _layout.tsx,而非各个屏幕中
- 手动内边距使用 useSafeAreaInsets(),简单包裹使用 SafeAreaView
- 在真机上测试触觉反馈和性能
反模式
- 使用 React Native Image 而非 expo-image
- 大数据集使用 FlatList 而非 FlashList
- 忘记 GestureHandlerRootView(会导致底部弹层和手势崩溃)
- 每次交互都过度使用触觉反馈
- 硬编码状态栏高度而非使用安全区域 inset
- FlashList 缺少 estimatedItemSize(必填 prop,会有控制台警告)
- 将 SafeAreaView 放在 ScrollView 内部(导致布局问题)
- 未在 app.json 的 plugins 数组中包含 expo-router 插件