Appearance
Expo 完整指南
expo中文地址:https://expo.nodejs.cn/ expo 部署地址:https://expo.dev/
目录
- Expo 简介
- Expo vs React Native CLI
- 环境搭建
- Expo SDK 核心概念
- 项目结构
- 核心 API 详解
- 导航系统
- 状态管理
- 样式与主题
- Expo Router
- 开发工具
- 构建与发布
- 常用第三方库集成
- 最佳实践
- 常见问题与解决方案
Expo 简介
什么是 Expo?
Expo 是一个围绕 React Native 构建的框架和平台,它提供了一整套工具和服务来简化 React Native 应用的开发、构建和部署流程。Expo 由 Expo 团队开发和维护,是 React Native 生态系统中最流行的开发框架之一。
Expo 的核心优势
- 快速启动:无需配置原生开发环境,几分钟内即可开始开发
- 跨平台开发:一套代码同时支持 iOS、Android 和 Web
- 丰富的 SDK:提供了大量预构建的模块和 API
- OTA 更新:支持无线更新,无需重新发布应用
- 开发工具链:Expo CLI、Expo Go、EAS Build 等完善的工具
Expo 版本历史
| 版本 | React Native 版本 | 发布时间 | 主要特性 |
|---|---|---|---|
| Expo 54 | 0.81.5 | 2025 | 最新稳定版 |
| Expo 52 | 0.76.x | 2024 | 新架构支持增强 |
| Expo 50 | 0.73.x | 2024 | 性能优化 |
| Expo 49 | 0.72.x | 2023 | Fabric 架构支持 |
Expo vs React Native CLI
对比表格
| 特性 | Expo | React Native CLI |
|---|---|---|
| 环境配置 | 简单,无需原生环境 | 需要配置 Xcode/Android Studio |
| 开发速度 | 快速启动 | 配置繁琐 |
| 原生模块 | 受限于 Expo SDK | 完全自定义 |
| 应用体积 | 较大 | 可优化到最小 |
| 原生代码访问 | 有限(可通过 Config Plugins 扩展) | 完全访问 |
| 构建方式 | EAS Build 或本地 | 本地构建 |
| 更新机制 | OTA 更新支持 | 需要重新发布 |
选择建议
选择 Expo 的场景:
- 快速原型开发
- 中小型应用项目
- 团队缺乏原生开发经验
- 需要快速迭代和 OTA 更新
- 跨平台一致性要求高
选择 React Native CLI 的场景:
- 需要深度原生集成
- 应用体积有严格限制
- 需要使用 Expo 不支持的第三方原生库
- 有原生开发经验的团队
环境搭建
系统要求
yaml
Node.js: >= 18.0.0
npm: >= 9.0.0 或 yarn >= 1.22.0
Git: 最新稳定版
Watchman: macOS/Linux 推荐安装步骤
1. 安装 Node.js
bash
# 使用 nvm 安装(推荐)
nvm install 20
nvm use 20
# 或使用官方安装包
# 下载地址: https://nodejs.org/2. 安装 Expo CLI
bash
# 全局安装(可选,新版推荐使用 npx)
npm install -g expo-cli
# 或使用 npx 直接运行
npx create-expo-app@latest3. 创建新项目
bash
# 创建默认模板项目
npx create-expo-app@latest my-app
# 创建指定模板项目
npx create-expo-app@latest my-app --template blank
npx create-expo-app@latest my-app --template blank-typescript
npx create-expo-app@latest my-app --template tabs
npx create-expo-app@latest my-app --template navigation
# 使用 Expo Router
npx create-expo-app@latest my-app --template tabs --expo-router4. 安装 Expo Go(移动端调试)
- iOS: App Store 搜索 "Expo Go" 下载
- Android: Google Play 或扫描终端二维码下载
项目配置文件
app.json
json
{
"expo": {
"name": "My App",
"slug": "my-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.company.myapp",
"buildNumber": "1.0.0"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.company.myapp",
"versionCode": 1
},
"web": {
"favicon": "./assets/favicon.png",
"bundler": "metro"
},
"plugins": ["expo-router"],
"scheme": "myapp",
"extra": {
"eas": {
"projectId": "your-project-id"
}
}
}
}package.json
json
{
"name": "my-app",
"version": "1.0.0",
"main": "expo-router/entry",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"build:android": "eas build --platform android",
"build:ios": "eas build --platform ios",
"submit": "eas submit"
},
"dependencies": {
"expo": "~54.0.0",
"expo-router": "~4.0.0",
"expo-status-bar": "~3.0.0",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0"
},
"devDependencies": {
"@types/react": "~19.1.0",
"typescript": "~5.9.2"
}
}tsconfig.json
json
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}Expo SDK 核心概念
SDK 版本与兼容性
Expo SDK 采用语义化版本控制,每个 SDK 版本对应特定的 React Native 版本:
javascript
// 检查当前 SDK 版本
import { Constants } from 'expo'
console.log(Constants.expoVersion)
// 或通过 expo-constants
import Constants from 'expo-constants'
console.log(Constants.expoConfig)核心 API 分类
1. 设备与系统 API
typescript
// expo-device - 设备信息
import * as Device from 'expo-device'
const deviceInfo = {
brand: Device.brand, // Apple, Samsung
manufacturer: Device.manufacturer,
modelName: Device.modelName, // iPhone 14 Pro
modelId: Device.modelId,
deviceName: await Device.getDeviceNameAsync(),
totalMemory: Device.totalMemory,
osName: Device.osName, // iOS, Android
osVersion: Device.osVersion,
platformApiLevel: Device.platformApiLevel
}
// expo-platform - 平台检测
import { Platform } from 'react-native'
if (Platform.OS === 'ios') {
// iOS 特定逻辑
}2. 文件系统 API
typescript
// expo-file-system
import * as FileSystem from 'expo-file-system'
// 获取应用文档目录
const docDir = FileSystem.documentDirectory
const cacheDir = FileSystem.cacheDirectory
// 读写文件
await FileSystem.writeAsStringAsync(
`${docDir}config.json`,
JSON.stringify({ theme: 'dark' })
)
const content = await FileSystem.readAsStringAsync(`${docDir}config.json`)
// 下载文件
const downloadResult = await FileSystem.downloadAsync(
'https://example.com/file.pdf',
`${docDir}downloaded.pdf`
)
// 上传文件
const uploadResult = await FileSystem.uploadAsync(
'https://api.example.com/upload',
`${docDir}file.pdf`,
{
httpMethod: 'POST',
uploadType: FileSystem.FileSystemUploadType.MULTIPART,
fieldName: 'file'
}
)
// 删除文件
await FileSystem.deleteAsync(`${docDir}old-file.txt`)
// 获取文件信息
const info = await FileSystem.getInfoAsync(`${docDir}file.pdf`)
console.log(info.exists, info.size, info.modificationTime)
// 创建目录
await FileSystem.makeDirectoryAsync(`${docDir}myDir`, { intermediates: true })
// 列出目录内容
const files = await FileSystem.readDirectoryAsync(docDir)3. 网络与通信 API
typescript
// expo-network - 网络状态
import * as Network from 'expo-network'
const networkState = await Network.getNetworkStateAsync()
console.log(networkState.isConnected, networkState.type)
// 获取 IP 地址
const ip = await Network.getIpAddressAsync()
// expo-intent-launcher - Android Intent
import * as IntentLauncher from 'expo-intent-launcher'
await IntentLauncher.startActivityAsync('android.settings.SETTINGS')4. 多媒体 API
typescript
// expo-image-picker - 图片选择
import * as ImagePicker from 'expo-image-picker';
// 请求权限
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
alert('需要相册权限');
}
// 从相册选择
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 0.8,
allowsMultipleSelection: true,
});
if (!result.canceled) {
console.log(result.assets[0].uri);
}
// 拍照
const cameraResult = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
});
// expo-camera - 相机控制
import { CameraView, CameraType, useCameraPermissions } from 'expo-camera';
function CameraScreen() {
const [facing, setFacing] = useState<CameraType>('back');
const [permission, requestPermission] = useCameraPermissions();
const cameraRef = useRef<CameraView>(null);
const takePicture = async () => {
const photo = await cameraRef.current?.takePictureAsync();
console.log(photo?.uri);
};
const recordVideo = async () => {
const video = await cameraRef.current?.recordAsync();
console.log(video?.uri);
};
if (!permission?.granted) {
return <Button onPress={requestPermission} title="授权相机" />;
}
return (
<CameraView style={styles.camera} facing={facing} ref={cameraRef}>
<Button onPress={takePicture} title="拍照" />
</CameraView>
);
}
// expo-av - 音视频播放
import { Video, Audio, AVPlaybackStatus } from 'expo-av';
// 播放视频
<Video
source={{ uri: 'https://example.com/video.mp4' }}
rate={1.0}
volume={1.0}
isMuted={false}
resizeMode="contain"
shouldPlay
isLooping
onPlaybackStatusUpdate={(status: AVPlaybackStatus) => {
if (status.isLoaded) {
console.log(status.positionMillis, status.durationMillis);
}
}}
/>
// 音频播放
const { sound } = await Audio.Sound.createAsync(
require('./assets/sound.mp3')
);
await sound.playAsync();
await sound.pauseAsync();
await sound.stopAsync();
await sound.unloadAsync();
// 录音
const { recording } = await Audio.Recording.createAsync(
Audio.RecordingOptionsPresets.HIGH_QUALITY
);
await recording.stopAndUnloadAsync();
const uri = recording.getURI();5. 传感器 API
typescript
// expo-sensors - 传感器
import {
Accelerometer,
Gyroscope,
Magnetometer,
Barometer,
Pedometer
} from 'expo-sensors'
// 加速度计
Accelerometer.addListener(accelerometerData => {
console.log(accelerometerData.x, accelerometerData.y, accelerometerData.z)
})
Accelerometer.setUpdateInterval(100)
// 陀螺仪
Gyroscope.addListener(gyroscopeData => {
console.log(gyroscopeData.x, gyroscopeData.y, gyroscopeData.z)
})
// 计步器
const isAvailable = await Pedometer.isAvailableAsync()
const steps = await Pedometer.getStepCountAsync(startDate, endDate)
Pedometer.watchStepCount(result => {
console.log(result.steps)
})
// expo-location - 位置服务
import * as Location from 'expo-location'
// 请求权限
const { status } = await Location.requestForegroundPermissionsAsync()
if (status !== 'granted') {
alert('需要位置权限')
}
// 获取当前位置
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High
})
console.log(location.coords.latitude, location.coords.longitude)
// 持续监听位置
const subscription = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
distanceInterval: 10,
timeInterval: 5000
},
location => {
console.log(location.coords)
}
)
// 地理编码
const geocoded = await Location.reverseGeocodeAsync({
latitude: 39.9042,
longitude: 116.4074
})
console.log(geocoded[0].city, geocoded[0].street)
// expo-brightness - 屏幕亮度
import * as Brightness from 'expo-brightness'
const { status } = await Brightness.requestPermissionsAsync()
const brightness = await Brightness.getBrightnessAsync()
await Brightness.setBrightnessAsync(0.5)6. 安全与认证 API
typescript
// expo-secure-store - 安全存储
import * as SecureStore from 'expo-secure-store'
// 存储敏感数据
await SecureStore.setItemAsync('token', 'user-auth-token')
await SecureStore.setItemAsync(
'userCredentials',
JSON.stringify({
username: 'user',
password: 'pass'
})
)
// 读取数据
const token = await SecureStore.getItemAsync('token')
const credentials = JSON.parse(
await SecureStore.getItemAsync('userCredentials')
)
// 删除数据
await SecureStore.deleteItemAsync('token')
// expo-local-authentication - 生物识别
import * as LocalAuthentication from 'expo-local-authentication'
// 检查设备是否支持生物识别
const compatible = await LocalAuthentication.hasHardwareAsync()
const enrolled = await LocalAuthentication.isEnrolledAsync()
// 生物识别认证
const result = await LocalAuthentication.authenticateAsync({
promptMessage: '请验证身份',
fallbackLabel: '使用密码',
cancelLabel: '取消',
disableDeviceFallback: false
})
if (result.success) {
console.log('认证成功')
}
// expo-crypto - 加密
import * as Crypto from 'expo-crypto'
const digest = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
'password123'
)
console.log(digest)
const randomBytes = await Crypto.getRandomBytesAsync(16)
console.log(randomBytes)7. 通知 API
typescript
// expo-notifications
import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
// 配置通知处理
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true
})
})
// 获取推送令牌
const { data: token } = await Notifications.getExpoPushTokenAsync({
projectId: 'your-project-id'
})
console.log(token)
// 注册原生通知(iOS)
if (Platform.OS === 'ios') {
await Notifications.requestPermissionsAsync({
ios: {
allowAlert: true,
allowBadge: true,
allowSound: true
}
})
}
// 本地通知
await Notifications.scheduleNotificationAsync({
content: {
title: '通知标题',
body: '通知内容',
data: { screen: 'Home' },
sound: 'default',
badge: 1
},
trigger: {
seconds: 5
}
})
// 定时通知
await Notifications.scheduleNotificationAsync({
content: {
title: '每日提醒',
body: '别忘了完成任务!'
},
trigger: {
hour: 9,
minute: 0,
repeats: true
}
})
// 监听通知
const notificationListener = Notifications.addNotificationReceivedListener(
notification => {
console.log('收到通知:', notification)
}
)
const responseListener = Notifications.addNotificationResponseReceivedListener(
response => {
console.log('用户点击通知:', response.notification.request.content.data)
}
)
// 取消通知
await Notifications.cancelAllScheduledNotificationsAsync()
await Notifications.dismissAllNotificationsAsync()
// 清理监听器
notificationListener.remove()
responseListener.remove()8. 分享与社交 API
typescript
// expo-sharing
import * as Sharing from 'expo-sharing'
if (await Sharing.isAvailableAsync()) {
await Sharing.shareAsync('file:///path/to/file.pdf', {
mimeType: 'application/pdf',
dialogTitle: '分享文件',
UTI: 'com.adobe.pdf'
})
}
// expo-sharing 结合 expo-file-system
const downloadPath = `${FileSystem.cacheDirectory}share.pdf`
await FileSystem.downloadAsync(url, downloadPath)
await Sharing.shareAsync(downloadPath)
// expo-intent-launcher (Android 分享)
import * as IntentLauncher from 'expo-intent-launcher'
await IntentLauncher.startActivityAsync('android.intent.action.SEND', {
type: 'text/plain',
extra: {
'android.intent.extra.TEXT': '分享内容'
}
})9. 应用内购买
typescript
// expo-in-app-purchases
import * as InAppPurchases from 'expo-in-app-purchases'
// 初始化
await InAppPurchases.connectAsync()
// 获取产品信息
const { responseCode, results } = await InAppPurchases.getProductsAsync([
'product_id_1',
'product_id_2'
])
// 购买
const purchaseResult = await InAppPurchases.purchaseItemAsync('product_id_1')
// 监听购买结果
InAppPurchases.setPurchaseListener(({ responseCode, results }) => {
if (responseCode === InAppPurchases.IAPResponseCode.OK) {
results.forEach(purchase => {
if (!purchase.acknowledged) {
// 处理购买
InAppPurchases.finishTransactionAsync(purchase, true)
}
})
}
})
// 断开连接
await InAppPurchases.disconnectAsync()项目结构
标准项目结构
my-app/
├── app/ # Expo Router 路由目录
│ ├── (tabs)/ # Tab 布局
│ │ ├── _layout.tsx # Tab 布局配置
│ │ ├── index.tsx # 首页
│ │ └── settings.tsx # 设置页
│ ├── (auth)/ # Auth 布局
│ │ ├── _layout.tsx
│ │ ├── login.tsx
│ │ └── register.tsx
│ ├── modal.tsx # 模态页面
│ └── _layout.tsx # 根布局
├── src/
│ ├── components/ # 可复用组件
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ └── index.ts
│ ├── screens/ # 页面组件(非 Router 模式)
│ │ ├── HomeScreen.tsx
│ │ └── ProfileScreen.tsx
│ ├── navigation/ # 导航配置
│ │ ├── AppNavigator.tsx
│ │ ├── AuthNavigator.tsx
│ │ └── MainNavigator.tsx
│ ├── store/ # 状态管理
│ │ ├── index.ts
│ │ ├── slices/
│ │ │ └── authSlice.ts
│ │ └── hooks.ts
│ ├── services/ # API 服务
│ │ ├── api.ts
│ │ └── auth.ts
│ ├── hooks/ # 自定义 Hooks
│ │ ├── useAuth.ts
│ │ └── useTheme.ts
│ ├── utils/ # 工具函数
│ │ ├── helpers.ts
│ │ └── constants.ts
│ ├── types/ # TypeScript 类型
│ │ └── index.ts
│ ├── constants/ # 常量配置
│ │ └── theme.ts
│ └── assets/ # 静态资源
│ ├── images/
│ ├── fonts/
│ └── icons/
├── assets/ # Expo 默认资源
│ ├── icon.png
│ ├── adaptive-icon.png
│ ├── splash-icon.png
│ └── favicon.png
├── app.json # Expo 配置
├── package.json
├── tsconfig.json
├── babel.config.js
├── metro.config.js
└── eas.json # EAS 构建配置入口文件
index.ts (Expo 默认入口)
typescript
import { registerRootComponent } from 'expo'
import App from './App'
registerRootComponent(App)App.tsx (应用根组件)
typescript
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider } from 'react-redux';
import { store } from './src/store';
import AppNavigator from './src/navigation/AppNavigator';
export default function App() {
return (
<Provider store={store}>
<SafeAreaProvider>
<StatusBar style="auto" />
<AppNavigator />
</SafeAreaProvider>
</Provider>
);
}核心 API 详解
expo-constants
typescript
import Constants from 'expo-constants'
const appConfig = {
// 应用信息
appName: Constants.expoConfig?.name,
appVersion: Constants.expoConfig?.version,
appSlug: Constants.expoConfig?.slug,
// 系统信息
platform: Constants.platform,
systemFonts: Constants.systemFonts,
// 调试信息
isDevice: Constants.isDevice,
debugMode: Constants.isDebugMode,
// 安装信息
installationId: Constants.installationId,
sessionId: Constants.sessionId,
// 其他
manifest: Constants.manifest,
linkingUri: Constants.linkingUri
}expo-font
typescript
import * as Font from 'expo-font';
import { useEffect, useState } from 'react';
// 加载自定义字体
const loadFonts = async () => {
await Font.loadAsync({
'CustomFont': require('./assets/fonts/CustomFont.ttf'),
'CustomFont-Bold': require('./assets/fonts/CustomFont-Bold.ttf'),
});
};
// 在组件中使用
function App() {
const [fontsLoaded, setFontsLoaded] = useState(false);
useEffect(() => {
(async () => {
await Font.loadAsync({
'CustomFont': require('./assets/fonts/CustomFont.ttf'),
});
setFontsLoaded(true);
})();
}, []);
if (!fontsLoaded) {
return null;
}
return (
<Text style={{ fontFamily: 'CustomFont' }}>自定义字体</Text>
);
}
// 使用 useFonts Hook
import { useFonts } from 'expo-font';
function App() {
const [loaded, error] = useFonts({
'CustomFont': require('./assets/fonts/CustomFont.ttf'),
});
if (!loaded && !error) {
return null;
}
return <Text style={{ fontFamily: 'CustomFont' }}>Hello</Text>;
}expo-splash-screen
typescript
import * as SplashScreen from 'expo-splash-screen';
import { useCallback } from 'react';
// 防止启动屏自动隐藏
SplashScreen.preventAutoHideAsync();
function App() {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
// 执行初始化任务
await Font.loadAsync({
'CustomFont': require('./assets/fonts/CustomFont.ttf'),
});
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (e) {
console.warn(e);
} finally {
setAppIsReady(true);
}
}
prepare();
}, []);
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
return (
<View onLayout={onLayoutRootView}>
<Text>App Ready</Text>
</View>
);
}expo-linking
typescript
import * as Linking from 'expo-linking'
// 打开 URL
await Linking.openURL('https://example.com')
await Linking.openURL('tel:+1234567890')
await Linking.openURL('mailto:support@example.com')
await Linking.openURL('maps://app')
// 深度链接
const prefix = Linking.createURL('/path')
// myapp://path
// 处理传入的链接
const url = await Linking.getInitialURL()
console.log('初始 URL:', url)
// 监听链接变化
Linking.addEventListener('url', ({ url }) => {
console.log('收到链接:', url)
})
// 解析 URL
const parsed = Linking.parse('myapp://profile/123?tab=settings')
console.log(parsed)
// { hostname: 'profile', path: '123', queryParams: { tab: 'settings' } }
// 创建 URL
const url = Linking.createURL('profile/123', {
queryParams: { tab: 'settings' }
})
// myapp://profile/123?tab=settingsexpo-screen-orientation
typescript
import * as ScreenOrientation from 'expo-screen-orientation'
// 锁定方向
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT)
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE)
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP)
// 解锁方向
await ScreenOrientation.unlockAsync()
// 获取当前方向
const orientation = await ScreenOrientation.getOrientationAsync()
console.log(orientation)
// 监听方向变化
ScreenOrientation.addOrientationChangeListener(({ orientationInfo }) => {
console.log('方向变化:', orientationInfo.orientation)
})expo-keep-awake
typescript
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
// 保持屏幕常亮
await activateKeepAwakeAsync();
// 取消常亮
deactivateKeepAwake();
// 使用 Hook
import { useKeepAwake } from 'expo-keep-awake';
function VideoPlayer() {
useKeepAwake();
return <Video source={{ uri: videoUrl }} />;
}导航系统
React Navigation 集成
安装依赖
bash
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs
npm install react-native-screens react-native-safe-area-contextStack Navigator
typescript
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Settings: undefined;
Details: { id: number; title?: string };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
function AppNavigator() {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName="Home"
screenOptions={{
headerStyle: { backgroundColor: '#FFD100' },
headerTintColor: '#333',
headerTitleStyle: { fontWeight: 'bold' },
headerBackTitleVisible: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
title: '首页',
headerLargeTitle: true,
}}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={({ route }) => ({ title: route.params.userId })}
/>
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={{
presentation: 'modal',
headerShown: false,
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
}页面导航
typescript
import { NativeStackScreenProps } from '@react-navigation/native-stack';
type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;
function HomeScreen({ navigation, route }: Props) {
// 导航到其他页面
const goToProfile = () => {
navigation.navigate('Profile', { userId: '123' });
};
// 替换当前页面
const replaceWithDetails = () => {
navigation.replace('Details', { id: 1 });
};
// 返回
const goBack = () => {
navigation.goBack();
};
// 返回到指定页面
const popToHome = () => {
navigation.popToTop();
};
// 重置导航栈
const resetStack = () => {
navigation.reset({
index: 0,
routes: [{ name: 'Home' }],
});
};
return (
<View>
<Button title="Go to Profile" onPress={goToProfile} />
</View>
);
}Bottom Tab Navigator
typescript
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
type TabParamList = {
Home: undefined;
Order: undefined;
Message: undefined;
Profile: undefined;
};
const Tab = createBottomTabNavigator<TabParamList>();
function MainNavigator() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
headerShown: false,
tabBarIcon: ({ focused, color, size }) => {
let iconName: keyof typeof Ionicons.glyphMap;
switch (route.name) {
case 'Home':
iconName = focused ? 'home' : 'home-outline';
break;
case 'Order':
iconName = focused ? 'receipt' : 'receipt-outline';
break;
case 'Message':
iconName = focused ? 'chatbubble' : 'chatbubble-outline';
break;
case 'Profile':
iconName = focused ? 'person' : 'person-outline';
break;
default:
iconName = 'home';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#FF6B00',
tabBarInactiveTintColor: '#999999',
tabBarStyle: {
backgroundColor: '#FFFFFF',
borderTopWidth: 1,
borderTopColor: '#EEEEEE',
paddingTop: 8,
paddingBottom: 8,
height: 60,
},
tabBarLabelStyle: {
fontSize: 12,
marginTop: 4,
},
})}
>
<Tab.Screen name="Home" component={HomeScreen} options={{ tabBarLabel: '首页' }} />
<Tab.Screen name="Order" component={OrderScreen} options={{ tabBarLabel: '订单' }} />
<Tab.Screen name="Message" component={MessageScreen} options={{ tabBarLabel: '消息' }} />
<Tab.Screen name="Profile" component={ProfileScreen} options={{ tabBarLabel: '我的' }} />
</Tab.Navigator>
);
}Drawer Navigator
typescript
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function AppNavigator() {
return (
<Drawer.Navigator
initialRouteName="Home"
screenOptions={{
drawerType: 'front',
drawerPosition: 'left',
drawerStyle: {
backgroundColor: '#FFFFFF',
width: 280,
},
drawerActiveTintColor: '#FF6B00',
drawerInactiveTintColor: '#333333',
headerShown: true,
}}
drawerContent={(props) => <CustomDrawerContent {...props} />}
>
<Drawer.Screen
name="Home"
component={HomeScreen}
options={{
drawerLabel: '首页',
drawerIcon: ({ color }) => <Ionicons name="home" size={24} color={color} />,
}}
/>
<Drawer.Screen name="Settings" component={SettingsScreen} />
</Drawer.Navigator>
);
}组合导航
typescript
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
function AuthNavigator() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</Stack.Navigator>
);
}
function MainNavigator() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}
function AppNavigator() {
const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated);
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<Stack.Screen name="Main" component={MainNavigator} />
) : (
<Stack.Screen name="Auth" component={AuthNavigator} />
)}
</Stack.Navigator>
</NavigationContainer>
);
}状态管理
Redux Toolkit 集成
安装依赖
bash
npm install @reduxjs/toolkit react-reduxStore 配置
typescript
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import authReducer from './slices/authSlice'
import userReducer from './slices/userSlice'
export const store = configureStore({
reducer: {
auth: authReducer,
user: userReducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: false,
immutableCheck: false
}),
devTools: __DEV__
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatchHooks
typescript
// src/store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './index'
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelectorSlice 示例
typescript
// src/store/slices/authSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
import { AuthState, User } from '../../types'
const initialState: AuthState = {
isAuthenticated: false,
user: null,
token: null,
isLoading: false,
error: null
}
export const login = createAsyncThunk(
'auth/login',
async (
{ phone, password }: { phone: string; password: string },
{ rejectWithValue }
) => {
try {
const response = await fetch('https://api.example.com/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, password })
})
if (!response.ok) {
throw new Error('登录失败')
}
const data = await response.json()
return data
} catch (error: any) {
return rejectWithValue(error.message)
}
}
)
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout: state => {
state.isAuthenticated = false
state.user = null
state.token = null
state.error = null
},
clearError: state => {
state.error = null
},
setUser: (state, action: PayloadAction<User>) => {
state.user = action.payload
}
},
extraReducers: builder => {
builder
.addCase(login.pending, state => {
state.isLoading = true
state.error = null
})
.addCase(login.fulfilled, (state, action) => {
state.isLoading = false
state.isAuthenticated = true
state.user = action.payload.user
state.token = action.payload.token
})
.addCase(login.rejected, (state, action) => {
state.isLoading = false
state.error = action.payload as string
})
}
})
export const { logout, clearError, setUser } = authSlice.actions
export default authSlice.reducer在组件中使用
typescript
import React, { useState, useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import { login, clearError } from '../store/slices/authSlice';
function LoginScreen() {
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const dispatch = useAppDispatch();
const { isLoading, error, isAuthenticated } = useAppSelector(
(state) => state.auth
);
const handleLogin = () => {
dispatch(login({ phone, password }));
};
useEffect(() => {
if (error) {
Alert.alert('错误', error);
dispatch(clearError());
}
}, [error]);
useEffect(() => {
if (isAuthenticated) {
navigation.navigate('Home');
}
}, [isAuthenticated]);
return (
<View>
<TextInput value={phone} onChangeText={setPhone} />
<TextInput value={password} onChangeText={setPassword} secureTextEntry />
<Button title={isLoading ? '登录中...' : '登录'} onPress={handleLogin} />
</View>
);
}Zustand (轻量级替代方案)
typescript
// src/store/useAuthStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AuthState {
isAuthenticated: boolean;
user: User | null;
token: string | null;
login: (user: User, token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
isAuthenticated: false,
user: null,
token: null,
login: (user, token) =>
set({ isAuthenticated: true, user, token }),
logout: () =>
set({ isAuthenticated: false, user: null, token: null }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
// 在组件中使用
function ProfileScreen() {
const { user, logout } = useAuthStore();
return (
<View>
<Text>{user?.username}</Text>
<Button title="退出登录" onPress={logout} />
</View>
);
}样式与主题
StyleSheet
typescript
import { StyleSheet, Dimensions, Platform } from 'react-native'
const { width, height } = Dimensions.get('window')
const isSmallDevice = width < 375
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
paddingHorizontal: 16,
paddingVertical: Platform.OS === 'ios' ? 20 : 16
},
title: {
fontSize: isSmallDevice ? 20 : 24,
fontWeight: 'bold',
color: '#333333',
textAlign: 'center',
marginBottom: 16
},
card: {
backgroundColor: '#F5F5F5',
borderRadius: 12,
padding: 16,
marginVertical: 8,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4
},
android: {
elevation: 4
}
})
}
})
// 响应式样式
const responsiveStyles = StyleSheet.create({
container: {
width: width > 600 ? '80%' : '100%',
maxWidth: 600,
alignSelf: 'center'
}
})主题系统
typescript
// src/constants/theme.ts
export const COLORS = {
primary: '#FFD100',
primaryDark: '#E5BC00',
secondary: '#FF6B00',
background: '#F5F5F5',
white: '#FFFFFF',
black: '#333333',
gray: '#999999',
lightGray: '#EEEEEE',
textPrimary: '#333333',
textSecondary: '#666666',
textLight: '#999999',
success: '#52C41A',
error: '#FF4D4F',
warning: '#FAAD14'
}
export const FONTS = {
regular: 'System',
medium: 'System',
bold: 'System',
h1: { fontSize: 32, fontWeight: 'bold' as const },
h2: { fontSize: 24, fontWeight: 'bold' as const },
h3: { fontSize: 20, fontWeight: '600' as const },
body: { fontSize: 16, fontWeight: 'normal' as const },
caption: { fontSize: 12, fontWeight: 'normal' as const }
}
export const SIZES = {
base: 8,
font: 14,
radius: 8,
padding: 16,
margin: 16,
width,
height
}
export const SHADOWS = {
light: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2
},
medium: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
elevation: 4
},
dark: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 8
}
}深色模式支持
typescript
import { useColorScheme } from 'react-native';
const lightTheme = {
colors: {
primary: '#FFD100',
background: '#FFFFFF',
text: '#333333',
card: '#F5F5F5',
},
};
const darkTheme = {
colors: {
primary: '#FFD100',
background: '#1A1A1A',
text: '#FFFFFF',
card: '#2A2A2A',
},
};
export const ThemeContext = createContext({
isDark: false,
theme: lightTheme,
toggleTheme: () => {},
});
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const colorScheme = useColorScheme();
const [isDark, setIsDark] = useState(colorScheme === 'dark');
const theme = isDark ? darkTheme : lightTheme;
const toggleTheme = () => {
setIsDark((prev) => !prev);
};
return (
<ThemeContext.Provider value={{ isDark, theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);
// 在组件中使用
function Card({ title }: { title: string }) {
const { theme } = useTheme();
return (
<View style={{ backgroundColor: theme.colors.card }}>
<Text style={{ color: theme.colors.text }}>{title}</Text>
</View>
);
}expo-linear-gradient
typescript
import { LinearGradient } from 'expo-linear-gradient';
function GradientButton() {
return (
<LinearGradient
colors={['#FF6B6B', '#FF8E8E']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.button}
>
<Text style={styles.buttonText}>点击我</Text>
</LinearGradient>
);
}
const styles = StyleSheet.create({
button: {
paddingVertical: 16,
paddingHorizontal: 32,
borderRadius: 25,
alignItems: 'center',
},
buttonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: 'bold',
},
});Expo Router
简介
Expo Router 是 Expo 官方推荐的路由解决方案,基于文件系统的路由,支持深度链接和 Web 端 SEO。
安装
bash
npm install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar配置
app.json
json
{
"expo": {
"scheme": "myapp",
"plugins": ["expo-router"]
}
}package.json
json
{
"main": "expo-router/entry"
}路由结构
app/
├── _layout.tsx # 根布局
├── index.tsx # 首页 (/)
├── about.tsx # 关于页 (/about)
├── (tabs)/ # Tab 组 (路由组,不影响 URL)
│ ├── _layout.tsx
│ ├── index.tsx # / (tabs 首页)
│ ├── profile.tsx # /profile
│ └── settings.tsx # /settings
├── (auth)/
│ ├── _layout.tsx
│ ├── login.tsx # /login
│ └── register.tsx # /register
├── users/
│ ├── index.tsx # /users
│ └── [id].tsx # /users/:id (动态路由)
├── posts/
│ └── [id]/
│ └── comments.tsx # /posts/:id/comments
└── modal.tsx # 模态页面布局文件
根布局 (app/_layout.tsx)
typescript
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack
screenOptions={{
headerStyle: { backgroundColor: '#FFD100' },
headerTintColor: '#333',
}}
>
<Stack.Screen name="index" options={{ title: '首页' }} />
<Stack.Screen name="about" options={{ title: '关于' }} />
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
title: '模态页面',
}}
/>
</Stack>
);
}Tab 布局 (app/(tabs)/_layout.tsx)
typescript
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName: keyof typeof Ionicons.glyphMap;
switch (route.name) {
case 'index':
iconName = focused ? 'home' : 'home-outline';
break;
case 'profile':
iconName = focused ? 'person' : 'person-outline';
break;
default:
iconName = 'home';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#FF6B00',
tabBarInactiveTintColor: '#999999',
})}
>
<Tabs.Screen name="index" options={{ title: '首页' }} />
<Tabs.Screen name="profile" options={{ title: '我的' }} />
</Tabs>
);
}页面导航
typescript
import { useRouter, useLocalSearchParams, Link } from 'expo-router';
function HomeScreen() {
const router = useRouter();
return (
<View>
{/* 使用 Link 组件 */}
<Link href="/about">关于我们</Link>
<Link href={{ pathname: '/users/[id]', params: { id: '123' } }}>
用户详情
</Link>
{/* 编程式导航 */}
<Button
title="Go to Profile"
onPress={() => router.push('/profile')}
/>
<Button
title="Go to User"
onPress={() => router.push(`/users/${userId}`)}
/>
<Button title="Go Back" onPress={() => router.back()} />
<Button title="Replace" onPress={() => router.replace('/login')} />
</View>
);
}
// 动态路由页面
function UserScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return <Text>User ID: {id}</Text>;
}深度链接
typescript
// app.json
{
"expo": {
"scheme": "myapp"
}
}
// 外部链接
// myapp://users/123
// myapp://profile
// Web 链接
// https://myapp.com/users/123开发工具
Expo CLI 命令
bash
# 启动开发服务器
npx expo start
npx expo start --clear # 清除缓存
npx expo start --tunnel # 使用隧道(跨网络调试)
npx expo start --lan # 使用局域网 IP
npx expo start --localhost # 使用本地主机
# 指定平台
npx expo start --android
npx expo start --ios
npx expo start --web
# 安装依赖
npx expo install expo-camera
npx expo install --fix # 修复依赖版本
# 配置
npx expo config --type public
npx expo config --type introspect
# 导出
npx expo export --platform web
npx expo export --platform android
npx expo export --platform ios
# 预构建(生成原生代码)
npx expo prebuild
npx expo prebuild --platform android
npx expo prebuild --cleanExpo Go 调试
- 安装 Expo Go 应用
- 运行
npx expo start - 扫描二维码或手动输入 URL
开发菜单
在 Expo Go 中摇动设备或在模拟器中按 Cmd+D (iOS) / Cmd+M (Android) 打开开发菜单:
- Reload
- Debug Remote JS
- Enable Fast Refresh
- Show Performance Monitor
- Show Element Inspector
React DevTools
bash
# 安装独立版 DevTools
npm install -g react-devtools
# 运行
react-devtoolsFlipper (高级调试)
对于使用 Development Build 的项目,可以使用 Flipper 进行高级调试:
bash
# 安装 Flipper
# 下载地址: https://fbflipper.com/
# 需要预构建
npx expo prebuild
npx expo run:android
npx expo run:ios构建与发布
EAS Build
安装 EAS CLI
bash
npm install -g eas-cli
eas login #账号rememberflyfly@163.com 密码zq...配置 eas.json
执行 eas build:configure 命令(执行后会在项目根目录生成 eas.json, 默认包含 development/preview/production 三个配置档Expo。)
json
{
"cli": {
"version": ">= 10.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
}
},
"preview": {
"distribution": "internal",
// "ios": { "distribution": "internal" }
"android": {
"buildType": "apk"
}
},
"production": {
"ios": {
"autoIncrement": true
},
"android": {
"buildType": "app-bundle"
}
}
},
"submit": {
"production": {}
}
}构建命令
bash
# 构建 Development Build
eas build --profile development --platform android
eas build --profile development --platform ios
# 构建 Preview Build
eas build --profile preview --platform android
eas build --profile preview --platform ios
# 构建生产版本
eas build --profile production --platform android
eas build --profile production --platform ios
eas build --profile production --platform all
# 本地构建(需要原生环境)
eas build --local --platform android
eas build --local --platform iosEAS Submit
bash
# 提交到 App Store
eas submit --platform ios --latest
# 提交到 Google Play
eas submit --platform android --latest
# 手动指定构建
eas submit --platform ios --id <build-id>EAS Update (OTA 更新)
bash
# 配置
eas update:configure
# 发布更新
eas update --branch production --message "修复 Bug"
eas update --branch preview --message "预览版本"
# 查看更新
eas update:list
# 回滚
eas update:rollback代码中检查更新
typescript
import * as Updates from 'expo-updates'
const checkForUpdate = async () => {
try {
const update = await Updates.checkForUpdateAsync()
if (update.isAvailable) {
await Updates.fetchUpdateAsync()
Alert.alert('更新可用', '是否立即重启应用以应用更新?', [
{ text: '稍后', style: 'cancel' },
{ text: '立即重启', onPress: () => Updates.reloadAsync() }
])
}
} catch (error) {
console.error('检查更新失败:', error)
}
}
// 监听更新事件
Updates.addListener(event => {
if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) {
console.log('发现新版本')
}
})本地构建
bash
# 预构建(生成 android/ios 目录)
npx expo prebuild
# Android
cd android
./gradlew assembleRelease
# iOS
cd ios
xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release
# 或使用 Expo 命令
npx expo run:android --variant release
npx expo run:ios --configuration Release常用第三方库集成
网络请求
typescript
// axios
import axios from 'axios'
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
api.interceptors.request.use(
async config => {
const token = await SecureStore.getItemAsync('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
api.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
// Token 过期,跳转登录
}
return Promise.reject(error)
}
)
export default api异步存储
typescript
// @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage'
// 存储
await AsyncStorage.setItem('user', JSON.stringify(userData))
await AsyncStorage.multiSet([
['token', 'abc123'],
['userId', '123']
])
// 读取
const user = JSON.parse(await AsyncStorage.getItem('user'))
const values = await AsyncStorage.multiGet(['token', 'userId'])
// 删除
await AsyncStorage.removeItem('token')
await AsyncStorage.clear()
// 获取所有键
const keys = await AsyncStorage.getAllKeys()图表
typescript
// react-native-chart-kit
import { LineChart, BarChart, PieChart } from 'react-native-chart-kit';
const data = {
labels: ['一月', '二月', '三月', '四月', '五月'],
datasets: [
{
data: [20, 45, 28, 80, 99],
},
],
};
<LineChart
data={data}
width={Dimensions.get('window').width - 32}
height={220}
yAxisLabel="¥"
chartConfig={{
backgroundColor: '#FFFFFF',
backgroundGradientFrom: '#FFFFFF',
backgroundGradientTo: '#FFFFFF',
decimalPlaces: 0,
color: (opacity = 1) => `rgba(255, 107, 0, ${opacity})`,
labelColor: (opacity = 1) => `rgba(51, 51, 51, ${opacity})`,
}}
bezier
style={{ borderRadius: 16 }}
/>动画
typescript
// react-native-reanimated
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
withRepeat,
withSequence,
Easing,
} from 'react-native-reanimated';
function AnimatedComponent() {
const offset = useSharedValue(0);
const scale = useSharedValue(1);
const animatedStyles = useAnimatedStyle(() => ({
transform: [
{ translateX: offset.value },
{ scale: scale.value },
],
}));
const moveRight = () => {
offset.value = withSpring(100);
};
const pulse = () => {
scale.value = withRepeat(
withSequence(
withTiming(1.2, { duration: 200 }),
withTiming(1, { duration: 200 })
),
-1,
true
);
};
return (
<Animated.View style={[styles.box, animatedStyles]}>
<Button title="Move" onPress={moveRight} />
<Button title="Pulse" onPress={pulse} />
</Animated.View>
);
}手势
typescript
// react-native-gesture-handler
import {
GestureHandlerRootView,
PanGestureHandler,
TapGestureHandler,
Swipeable,
} from 'react-native-gesture-handler';
function SwipeableRow({ children, onDelete }) {
const renderRightActions = (progress, dragX) => {
return (
<View style={styles.deleteAction}>
<Text>删除</Text>
</View>
);
};
return (
<Swipeable
renderRightActions={renderRightActions}
onSwipeableWillOpen={onDelete}
>
{children}
</Swipeable>
);
}最佳实践
1. 项目结构规范
src/
├── components/ # 组件按功能分类
│ ├── common/ # 通用组件
│ ├── forms/ # 表单组件
│ └── layout/ # 布局组件
├── hooks/ # 自定义 Hooks
├── services/ # API 服务层
├── store/ # 状态管理
├── types/ # TypeScript 类型定义
├── utils/ # 工具函数
└── constants/ # 常量配置2. 性能优化
typescript
// 使用 memo 避免不必要的重渲染
import React, { memo } from 'react';
const ListItem = memo(({ item, onPress }) => {
return (
<TouchableOpacity onPress={() => onPress(item.id)}>
<Text>{item.title}</Text>
</TouchableOpacity>
);
}, (prevProps, nextProps) => {
return prevProps.item.id === nextProps.item.id &&
prevProps.item.title === nextProps.item.title;
});
// 使用 useCallback 缓存回调
const handlePress = useCallback((id) => {
navigation.navigate('Details', { id });
}, [navigation]);
// 使用 useMemo 缓存计算结果
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// FlatList 优化
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews={true}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>3. 错误处理
typescript
// 全局错误边界
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<View style={styles.errorContainer}>
<Text>出错了,请重启应用</Text>
<Button title="重试" onPress={() => this.setState({ hasError: false })} />
</View>
);
}
return this.props.children;
}
}
// API 错误处理
const handleApiError = (error: any) => {
if (axios.isAxiosError(error)) {
if (error.response) {
switch (error.response.status) {
case 401:
// 未授权
break;
case 403:
// 禁止访问
break;
case 404:
// 未找到
break;
case 500:
// 服务器错误
break;
}
} else if (error.request) {
// 网络错误
}
}
return Promise.reject(error);
};4. 环境配置
typescript
// .env.development
API_URL=https://dev-api.example.com
DEBUG=true
// .env.production
API_URL=https://api.example.com
DEBUG=false
// app.config.ts
import 'dotenv/config';
export default {
expo: {
extra: {
apiUrl: process.env.API_URL,
debug: process.env.DEBUG === 'true',
},
},
};
// 使用
import Constants from 'expo-constants';
const apiUrl = Constants.expoConfig?.extra?.apiUrl;5. 代码规范
typescript
// 使用 TypeScript 严格模式
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}
// 组件 Props 类型定义
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline';
disabled?: boolean;
loading?: boolean;
}
// 使用解构和默认值
const Button: React.FC<ButtonProps> = ({
title,
onPress,
variant = 'primary',
disabled = false,
loading = false,
}) => {
return (
<TouchableOpacity onPress={onPress} disabled={disabled || loading}>
{loading ? <ActivityIndicator /> : <Text>{title}</Text>}
</TouchableOpacity>
);
};常见问题与解决方案
1. Metro Bundler 问题
bash
# 清除缓存
npx expo start --clear
# 重置 Metro 缓存
npx react-native start --reset-cache
# 删除 node_modules 重新安装
rm -rf node_modules
npm install2. iOS 模拟器问题
bash
# 重置模拟器
xcrun simctl shutdown all
xcrun simctl erase all
# 清理 Xcode 派生数据
rm -rf ~/Library/Developer/Xcode/DerivedData3. Android 模拟器问题
bash
# 冷启动模拟器
adb -e emu kill
adb -e emu avd <avd_name>
# 清理 Gradle 缓存
cd android
./gradlew clean4. 依赖版本冲突
bash
# 检查依赖版本
npx expo-doctor
# 修复依赖
npx expo install --fix
# 查看依赖树
npm ls <package-name>5. 网络请求问题
typescript
// Android 9+ 需要 HTTPS
// android/app/src/main/AndroidManifest.xml
<application
android:usesCleartextTraffic="true"
...
>
// iOS ATS 配置
// ios/MyApp/Info.plist
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>6. 内存泄漏
typescript
// 正确清理副作用
useEffect(() => {
const subscription = someEventEmitter.addListener('event', handler)
return () => {
subscription.remove()
}
}, [])
// 清理定时器
useEffect(() => {
const timer = setInterval(() => {}, 1000)
return () => {
clearInterval(timer)
}
}, [])
// 取消异步操作
useEffect(() => {
let isMounted = true
const fetchData = async () => {
const data = await api.getData()
if (isMounted) {
setState(data)
}
}
fetchData()
return () => {
isMounted = false
}
}, [])7. 键盘遮挡问题
typescript
import { KeyboardAvoidingView, Platform } from 'react-native';
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<TextInput />
</KeyboardAvoidingView>
// 或使用 react-native-keyboard-aware-scroll-view
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
<KeyboardAwareScrollView>
<TextInput />
</KeyboardAwareScrollView>