Skip to content

Expo 完整指南

expo中文地址:https://expo.nodejs.cn/ expo 部署地址:https://expo.dev/

目录

  1. Expo 简介
  2. Expo vs React Native CLI
  3. 环境搭建
  4. Expo SDK 核心概念
  5. 项目结构
  6. 核心 API 详解
  7. 导航系统
  8. 状态管理
  9. 样式与主题
  10. Expo Router
  11. 开发工具
  12. 构建与发布
  13. 常用第三方库集成
  14. 最佳实践
  15. 常见问题与解决方案

Expo 简介

什么是 Expo?

Expo 是一个围绕 React Native 构建的框架和平台,它提供了一整套工具和服务来简化 React Native 应用的开发、构建和部署流程。Expo 由 Expo 团队开发和维护,是 React Native 生态系统中最流行的开发框架之一。

Expo 的核心优势

  1. 快速启动:无需配置原生开发环境,几分钟内即可开始开发
  2. 跨平台开发:一套代码同时支持 iOS、Android 和 Web
  3. 丰富的 SDK:提供了大量预构建的模块和 API
  4. OTA 更新:支持无线更新,无需重新发布应用
  5. 开发工具链:Expo CLI、Expo Go、EAS Build 等完善的工具

Expo 版本历史

版本React Native 版本发布时间主要特性
Expo 540.81.52025最新稳定版
Expo 520.76.x2024新架构支持增强
Expo 500.73.x2024性能优化
Expo 490.72.x2023Fabric 架构支持

Expo vs React Native CLI

对比表格

特性ExpoReact 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@latest

3. 创建新项目

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-router

4. 安装 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=settings

expo-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-context

Stack 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-redux

Store 配置

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.dispatch

Hooks

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> = useSelector

Slice 示例

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 --clean

Expo Go 调试

  1. 安装 Expo Go 应用
  2. 运行 npx expo start
  3. 扫描二维码或手动输入 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-devtools

Flipper (高级调试)

对于使用 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 ios

EAS 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 install

2. iOS 模拟器问题

bash
# 重置模拟器
xcrun simctl shutdown all
xcrun simctl erase all

# 清理 Xcode 派生数据
rm -rf ~/Library/Developer/Xcode/DerivedData

3. Android 模拟器问题

bash
# 冷启动模拟器
adb -e emu kill
adb -e emu avd <avd_name>

# 清理 Gradle 缓存
cd android
./gradlew clean

4. 依赖版本冲突

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>

参考资源

基于 VitePress 的本地知识库