发布于

React Native移动开发实战:跨平台应用开发与性能优化

作者

React Native移动开发实战:跨平台应用开发与性能优化

React Native为移动应用开发提供了高效的跨平台解决方案。本文将分享React Native开发的实战经验和最佳实践。

项目架构与配置

项目初始化与配置

# 创建新项目
npx react-native@latest init MyApp --template react-native-template-typescript

# 项目结构
MyApp/
├── src/
│   ├── components/
│   │   ├── common/
│   │   ├── forms/
│   │   └── ui/
│   ├── screens/
│   │   ├── auth/
│   │   ├── home/
│   │   └── profile/
│   ├── navigation/
│   ├── services/
│   │   ├── api/
│   │   ├── storage/
│   │   └── notifications/
│   ├── store/
│   │   ├── slices/
│   │   └── middleware/
│   ├── utils/
│   ├── hooks/
│   ├── types/
│   └── constants/
├── android/
├── ios/
└── __tests__/

# package.json - 依赖配置
{
  "name": "MyApp",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "type-check": "tsc --noEmit"
  },
  "dependencies": {
    "react": "18.2.0",
    "react-native": "0.72.6",
    "@react-navigation/native": "^6.1.9",
    "@react-navigation/stack": "^6.3.20",
    "@react-navigation/bottom-tabs": "^6.5.11",
    "@reduxjs/toolkit": "^1.9.7",
    "react-redux": "^8.1.3",
    "react-native-screens": "^3.27.0",
    "react-native-safe-area-context": "^4.7.4",
    "react-native-gesture-handler": "^2.13.4",
    "react-native-reanimated": "^3.5.4",
    "react-native-vector-icons": "^10.0.2",
    "react-native-async-storage": "^1.19.5",
    "react-native-keychain": "^8.1.3",
    "react-native-permissions": "^3.10.1",
    "react-native-image-picker": "^7.0.3",
    "react-native-push-notification": "^8.1.1"
  },
  "devDependencies": {
    "@types/react": "^18.2.6",
    "@types/react-native": "^0.72.2",
    "@typescript-eslint/eslint-plugin": "^6.7.4",
    "@typescript-eslint/parser": "^6.7.4",
    "eslint": "^8.51.0",
    "jest": "^29.6.3",
    "metro-react-native-babel-preset": "0.76.8",
    "prettier": "^3.0.3",
    "typescript": "5.0.4"
  }
}

# metro.config.js - Metro配置
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

const defaultConfig = getDefaultConfig(__dirname);

const config = {
  transformer: {
    babelTransformerPath: require.resolve('react-native-svg-transformer'),
  },
  resolver: {
    assetExts: defaultConfig.resolver.assetExts.filter(ext => ext !== 'svg'),
    sourceExts: [...defaultConfig.resolver.sourceExts, 'svg'],
  },
};

module.exports = mergeConfig(defaultConfig, config);

# babel.config.js - Babel配置
module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    'react-native-reanimated/plugin',
    [
      'module-resolver',
      {
        root: ['./src'],
        extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
        alias: {
          '@': './src',
          '@components': './src/components',
          '@screens': './src/screens',
          '@navigation': './src/navigation',
          '@services': './src/services',
          '@store': './src/store',
          '@utils': './src/utils',
          '@hooks': './src/hooks',
          '@types': './src/types',
          '@constants': './src/constants',
        },
      },
    ],
  ],
};

TypeScript配置

// src/types/index.ts - 类型定义
export interface User {
  id: string
  email: string
  name: string
  avatar?: string
  phone?: string
  createdAt: string
  updatedAt: string
}

export interface Post {
  id: string
  title: string
  content: string
  author: User
  images: string[]
  likes: number
  comments: number
  createdAt: string
  updatedAt: string
}

export interface ApiResponse<T> {
  success: boolean
  data: T
  message?: string
  errors?: string[]
}

export interface PaginatedResponse<T> {
  data: T[]
  pagination: {
    page: number
    limit: number
    total: number
    pages: number
  }
}

// Navigation types
export type RootStackParamList = {
  Auth: undefined
  Main: undefined
  PostDetail: { postId: string }
  Profile: { userId: string }
  EditProfile: undefined
}

export type MainTabParamList = {
  Home: undefined
  Search: undefined
  Create: undefined
  Notifications: undefined
  Profile: undefined
}

export type AuthStackParamList = {
  Login: undefined
  Register: undefined
  ForgotPassword: undefined
}

// src/constants/index.ts - 常量定义
export const COLORS = {
  primary: '#007AFF',
  secondary: '#5856D6',
  success: '#34C759',
  warning: '#FF9500',
  error: '#FF3B30',
  background: '#F2F2F7',
  surface: '#FFFFFF',
  text: '#000000',
  textSecondary: '#8E8E93',
  border: '#C6C6C8',
  placeholder: '#C7C7CD',
} as const

export const SIZES = {
  xs: 4,
  sm: 8,
  md: 16,
  lg: 24,
  xl: 32,
  xxl: 48,
} as const

export const FONTS = {
  regular: 'System',
  medium: 'System',
  bold: 'System',
  sizes: {
    xs: 12,
    sm: 14,
    md: 16,
    lg: 18,
    xl: 20,
    xxl: 24,
    xxxl: 32,
  },
} as const

export const API_ENDPOINTS = {
  BASE_URL: __DEV__ ? 'http://localhost:3000/api' : 'https://api.myapp.com',
  AUTH: {
    LOGIN: '/auth/login',
    REGISTER: '/auth/register',
    REFRESH: '/auth/refresh',
    LOGOUT: '/auth/logout',
  },
  USERS: {
    PROFILE: '/users/profile',
    UPDATE: '/users/update',
    UPLOAD_AVATAR: '/users/avatar',
  },
  POSTS: {
    LIST: '/posts',
    CREATE: '/posts',
    DETAIL: (id: string) => `/posts/${id}`,
    UPDATE: (id: string) => `/posts/${id}`,
    DELETE: (id: string) => `/posts/${id}`,
    LIKE: (id: string) => `/posts/${id}/like`,
  },
} as const

组件开发

基础UI组件

// src/components/ui/Button.tsx - 按钮组件
import React from 'react'
import {
  TouchableOpacity,
  Text,
  StyleSheet,
  ActivityIndicator,
  ViewStyle,
  TextStyle,
} from 'react-native'
import { COLORS, SIZES, FONTS } from '@/constants'

interface ButtonProps {
  title: string
  onPress: () => void
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  loading?: boolean
  style?: ViewStyle
  textStyle?: TextStyle
  icon?: React.ReactNode
}

export const Button: React.FC<ButtonProps> = ({
  title,
  onPress,
  variant = 'primary',
  size = 'md',
  disabled = false,
  loading = false,
  style,
  textStyle,
  icon,
}) => {
  const buttonStyle = [
    styles.button,
    styles[variant],
    styles[size],
    disabled && styles.disabled,
    style,
  ]

  const buttonTextStyle = [
    styles.text,
    styles[`${variant}Text`],
    styles[`${size}Text`],
    disabled && styles.disabledText,
    textStyle,
  ]

  return (
    <TouchableOpacity
      style={buttonStyle}
      onPress={onPress}
      disabled={disabled || loading}
      activeOpacity={0.7}
    >
      {loading ? (
        <ActivityIndicator
          color={variant === 'primary' ? COLORS.surface : COLORS.primary}
          size="small"
        />
      ) : (
        <>
          {icon}
          <Text style={buttonTextStyle}>{title}</Text>
        </>
      )}
    </TouchableOpacity>
  )
}

const styles = StyleSheet.create({
  button: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: SIZES.sm,
    paddingHorizontal: SIZES.md,
  },
  // Variants
  primary: {
    backgroundColor: COLORS.primary,
  },
  secondary: {
    backgroundColor: COLORS.secondary,
  },
  outline: {
    backgroundColor: 'transparent',
    borderWidth: 1,
    borderColor: COLORS.primary,
  },
  ghost: {
    backgroundColor: 'transparent',
  },
  // Sizes
  sm: {
    paddingVertical: SIZES.xs,
    minHeight: 32,
  },
  md: {
    paddingVertical: SIZES.sm,
    minHeight: 44,
  },
  lg: {
    paddingVertical: SIZES.md,
    minHeight: 52,
  },
  // States
  disabled: {
    opacity: 0.5,
  },
  // Text styles
  text: {
    fontFamily: FONTS.medium,
    textAlign: 'center',
  },
  primaryText: {
    color: COLORS.surface,
  },
  secondaryText: {
    color: COLORS.surface,
  },
  outlineText: {
    color: COLORS.primary,
  },
  ghostText: {
    color: COLORS.primary,
  },
  smText: {
    fontSize: FONTS.sizes.sm,
  },
  mdText: {
    fontSize: FONTS.sizes.md,
  },
  lgText: {
    fontSize: FONTS.sizes.lg,
  },
  disabledText: {
    opacity: 0.7,
  },
})

// src/components/ui/Input.tsx - 输入框组件
import React, { useState } from 'react'
import {
  View,
  TextInput,
  Text,
  StyleSheet,
  TextInputProps,
  ViewStyle,
} from 'react-native'
import { COLORS, SIZES, FONTS } from '@/constants'

interface InputProps extends TextInputProps {
  label?: string
  error?: string
  containerStyle?: ViewStyle
  leftIcon?: React.ReactNode
  rightIcon?: React.ReactNode
}

export const Input: React.FC<InputProps> = ({
  label,
  error,
  containerStyle,
  leftIcon,
  rightIcon,
  style,
  ...props
}) => {
  const [isFocused, setIsFocused] = useState(false)

  return (
    <View style={[styles.container, containerStyle]}>
      {label && <Text style={styles.label}>{label}</Text>}
      
      <View style={[
        styles.inputContainer,
        isFocused && styles.focused,
        error && styles.error,
      ]}>
        {leftIcon && <View style={styles.leftIcon}>{leftIcon}</View>}
        
        <TextInput
          style={[styles.input, style]}
          onFocus={() => setIsFocused(true)}
          onBlur={() => setIsFocused(false)}
          placeholderTextColor={COLORS.placeholder}
          {...props}
        />
        
        {rightIcon && <View style={styles.rightIcon}>{rightIcon}</View>}
      </View>
      
      {error && <Text style={styles.errorText}>{error}</Text>}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    marginBottom: SIZES.md,
  },
  label: {
    fontSize: FONTS.sizes.sm,
    fontFamily: FONTS.medium,
    color: COLORS.text,
    marginBottom: SIZES.xs,
  },
  inputContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    borderWidth: 1,
    borderColor: COLORS.border,
    borderRadius: SIZES.sm,
    backgroundColor: COLORS.surface,
  },
  focused: {
    borderColor: COLORS.primary,
  },
  error: {
    borderColor: COLORS.error,
  },
  input: {
    flex: 1,
    paddingVertical: SIZES.sm,
    paddingHorizontal: SIZES.md,
    fontSize: FONTS.sizes.md,
    fontFamily: FONTS.regular,
    color: COLORS.text,
  },
  leftIcon: {
    paddingLeft: SIZES.md,
  },
  rightIcon: {
    paddingRight: SIZES.md,
  },
  errorText: {
    fontSize: FONTS.sizes.xs,
    fontFamily: FONTS.regular,
    color: COLORS.error,
    marginTop: SIZES.xs,
  },
})

// src/components/ui/Card.tsx - 卡片组件
import React from 'react'
import { View, StyleSheet, ViewStyle } from 'react-native'
import { COLORS, SIZES } from '@/constants'

interface CardProps {
  children: React.ReactNode
  style?: ViewStyle
  padding?: keyof typeof SIZES
  shadow?: boolean
}

export const Card: React.FC<CardProps> = ({
  children,
  style,
  padding = 'md',
  shadow = true,
}) => {
  return (
    <View style={[
      styles.card,
      { padding: SIZES[padding] },
      shadow && styles.shadow,
      style,
    ]}>
      {children}
    </View>
  )
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: COLORS.surface,
    borderRadius: SIZES.sm,
    borderWidth: StyleSheet.hairlineWidth,
    borderColor: COLORS.border,
  },
  shadow: {
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.1,
    shadowRadius: 3.84,
    elevation: 5,
  },
})

复杂业务组件

// src/components/PostCard.tsx - 文章卡片组件
import React from 'react'
import {
  View,
  Text,
  Image,
  TouchableOpacity,
  StyleSheet,
  Dimensions,
} from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { Card } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { COLORS, SIZES, FONTS } from '@/constants'
import { Post } from '@/types'
import { formatDate, formatNumber } from '@/utils'

interface PostCardProps {
  post: Post
  onLike?: (postId: string) => void
  onComment?: (postId: string) => void
}

const { width } = Dimensions.get('window')
const cardWidth = width - SIZES.md * 2

export const PostCard: React.FC<PostCardProps> = ({
  post,
  onLike,
  onComment,
}) => {
  const navigation = useNavigation()

  const handlePress = () => {
    navigation.navigate('PostDetail', { postId: post.id })
  }

  const handleLike = () => {
    onLike?.(post.id)
  }

  const handleComment = () => {
    onComment?.(post.id)
  }

  return (
    <Card style={styles.card}>
      <TouchableOpacity onPress={handlePress} activeOpacity={0.7}>
        {/* Header */}
        <View style={styles.header}>
          <Image
            source={{ uri: post.author.avatar || 'https://via.placeholder.com/40' }}
            style={styles.avatar}
          />
          <View style={styles.authorInfo}>
            <Text style={styles.authorName}>{post.author.name}</Text>
            <Text style={styles.date}>{formatDate(post.createdAt)}</Text>
          </View>
        </View>

        {/* Content */}
        <Text style={styles.title}>{post.title}</Text>
        <Text style={styles.content} numberOfLines={3}>
          {post.content}
        </Text>

        {/* Images */}
        {post.images.length > 0 && (
          <View style={styles.imagesContainer}>
            {post.images.slice(0, 3).map((image, index) => (
              <Image
                key={index}
                source={{ uri: image }}
                style={[
                  styles.image,
                  post.images.length === 1 && styles.singleImage,
                ]}
              />
            ))}
            {post.images.length > 3 && (
              <View style={styles.moreImages}>
                <Text style={styles.moreImagesText}>
                  +{post.images.length - 3}
                </Text>
              </View>
            )}
          </View>
        )}
      </TouchableOpacity>

      {/* Actions */}
      <View style={styles.actions}>
        <Button
          title={`${formatNumber(post.likes)} Likes`}
          onPress={handleLike}
          variant="ghost"
          size="sm"
          icon={<HeartIcon />}
        />
        <Button
          title={`${formatNumber(post.comments)} Comments`}
          onPress={handleComment}
          variant="ghost"
          size="sm"
          icon={<CommentIcon />}
        />
        <Button
          title="Share"
          onPress={() => {}}
          variant="ghost"
          size="sm"
          icon={<ShareIcon />}
        />
      </View>
    </Card>
  )
}

const styles = StyleSheet.create({
  card: {
    marginHorizontal: SIZES.md,
    marginBottom: SIZES.md,
  },
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: SIZES.sm,
  },
  avatar: {
    width: 40,
    height: 40,
    borderRadius: 20,
    marginRight: SIZES.sm,
  },
  authorInfo: {
    flex: 1,
  },
  authorName: {
    fontSize: FONTS.sizes.md,
    fontFamily: FONTS.medium,
    color: COLORS.text,
  },
  date: {
    fontSize: FONTS.sizes.xs,
    fontFamily: FONTS.regular,
    color: COLORS.textSecondary,
    marginTop: 2,
  },
  title: {
    fontSize: FONTS.sizes.lg,
    fontFamily: FONTS.bold,
    color: COLORS.text,
    marginBottom: SIZES.xs,
  },
  content: {
    fontSize: FONTS.sizes.md,
    fontFamily: FONTS.regular,
    color: COLORS.text,
    lineHeight: 20,
    marginBottom: SIZES.sm,
  },
  imagesContainer: {
    flexDirection: 'row',
    marginBottom: SIZES.sm,
  },
  image: {
    width: (cardWidth - SIZES.md * 4) / 3,
    height: 80,
    borderRadius: SIZES.xs,
    marginRight: SIZES.xs,
  },
  singleImage: {
    width: cardWidth - SIZES.md * 2,
    height: 200,
  },
  moreImages: {
    width: (cardWidth - SIZES.md * 4) / 3,
    height: 80,
    borderRadius: SIZES.xs,
    backgroundColor: 'rgba(0,0,0,0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  moreImagesText: {
    color: COLORS.surface,
    fontSize: FONTS.sizes.md,
    fontFamily: FONTS.bold,
  },
  actions: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    paddingTop: SIZES.sm,
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: COLORS.border,
  },
})

// src/components/LoadingSpinner.tsx - 加载组件
import React from 'react'
import {
  View,
  ActivityIndicator,
  Text,
  StyleSheet,
  ViewStyle,
} from 'react-native'
import { COLORS, SIZES, FONTS } from '@/constants'

interface LoadingSpinnerProps {
  size?: 'small' | 'large'
  color?: string
  text?: string
  style?: ViewStyle
}

export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
  size = 'large',
  color = COLORS.primary,
  text,
  style,
}) => {
  return (
    <View style={[styles.container, style]}>
      <ActivityIndicator size={size} color={color} />
      {text && <Text style={styles.text}>{text}</Text>}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: SIZES.lg,
  },
  text: {
    marginTop: SIZES.sm,
    fontSize: FONTS.sizes.md,
    fontFamily: FONTS.regular,
    color: COLORS.textSecondary,
    textAlign: 'center',
  },
})

// src/components/EmptyState.tsx - 空状态组件
import React from 'react'
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
import { Button } from '@/components/ui/Button'
import { COLORS, SIZES, FONTS } from '@/constants'

interface EmptyStateProps {
  title: string
  description?: string
  actionTitle?: string
  onAction?: () => void
  icon?: React.ReactNode
  style?: ViewStyle
}

export const EmptyState: React.FC<EmptyStateProps> = ({
  title,
  description,
  actionTitle,
  onAction,
  icon,
  style,
}) => {
  return (
    <View style={[styles.container, style]}>
      {icon && <View style={styles.icon}>{icon}</View>}
      <Text style={styles.title}>{title}</Text>
      {description && <Text style={styles.description}>{description}</Text>}
      {actionTitle && onAction && (
        <Button
          title={actionTitle}
          onPress={onAction}
          style={styles.action}
        />
      )}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: SIZES.lg,
  },
  icon: {
    marginBottom: SIZES.md,
  },
  title: {
    fontSize: FONTS.sizes.xl,
    fontFamily: FONTS.bold,
    color: COLORS.text,
    textAlign: 'center',
    marginBottom: SIZES.sm,
  },
  description: {
    fontSize: FONTS.sizes.md,
    fontFamily: FONTS.regular,
    color: COLORS.textSecondary,
    textAlign: 'center',
    lineHeight: 20,
    marginBottom: SIZES.lg,
  },
  action: {
    minWidth: 120,
  },
})

导航管理

React Navigation配置

// src/navigation/index.tsx - 导航配置
import React from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import { useSelector } from 'react-redux'
import { AuthNavigator } from './AuthNavigator'
import { MainNavigator } from './MainNavigator'
import { RootStackParamList } from '@/types'
import { RootState } from '@/store'

const Stack = createStackNavigator<RootStackParamList>()

export const RootNavigator: React.FC = () => {
  const { isAuthenticated } = useSelector((state: RootState) => state.auth)

  return (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        {isAuthenticated ? (
          <Stack.Screen name="Main" component={MainNavigator} />
        ) : (
          <Stack.Screen name="Auth" component={AuthNavigator} />
        )}
      </Stack.Navigator>
    </NavigationContainer>
  )
}

// src/navigation/MainNavigator.tsx - 主导航
import React from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { createStackNavigator } from '@react-navigation/stack'
import { HomeScreen } from '@/screens/home/HomeScreen'
import { SearchScreen } from '@/screens/search/SearchScreen'
import { CreateScreen } from '@/screens/create/CreateScreen'
import { NotificationsScreen } from '@/screens/notifications/NotificationsScreen'
import { ProfileScreen } from '@/screens/profile/ProfileScreen'
import { PostDetailScreen } from '@/screens/post/PostDetailScreen'
import { EditProfileScreen } from '@/screens/profile/EditProfileScreen'
import { TabBar } from '@/components/navigation/TabBar'
import { MainTabParamList, RootStackParamList } from '@/types'

const Tab = createBottomTabNavigator<MainTabParamList>()
const Stack = createStackNavigator<RootStackParamList>()

const MainTabs: React.FC = () => {
  return (
    <Tab.Navigator
      tabBar={(props) => <TabBar {...props} />}
      screenOptions={{ headerShown: false }}
    >
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Search" component={SearchScreen} />
      <Tab.Screen name="Create" component={CreateScreen} />
      <Tab.Screen name="Notifications" component={NotificationsScreen} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  )
}

export const MainNavigator: React.FC = () => {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="MainTabs"
        component={MainTabs}
        options={{ headerShown: false }}
      />
      <Stack.Screen
        name="PostDetail"
        component={PostDetailScreen}
        options={{ title: 'Post Detail' }}
      />
      <Stack.Screen
        name="EditProfile"
        component={EditProfileScreen}
        options={{ title: 'Edit Profile' }}
      />
    </Stack.Navigator>
  )
}

// src/navigation/AuthNavigator.tsx - 认证导航
import React from 'react'
import { createStackNavigator } from '@react-navigation/stack'
import { LoginScreen } from '@/screens/auth/LoginScreen'
import { RegisterScreen } from '@/screens/auth/RegisterScreen'
import { ForgotPasswordScreen } from '@/screens/auth/ForgotPasswordScreen'
import { AuthStackParamList } from '@/types'

const Stack = createStackNavigator<AuthStackParamList>()

export const AuthNavigator: React.FC = () => {
  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: false,
        cardStyle: { backgroundColor: 'white' },
      }}
    >
      <Stack.Screen name="Login" component={LoginScreen} />
      <Stack.Screen name="Register" component={RegisterScreen} />
      <Stack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
    </Stack.Navigator>
  )
}

// src/components/navigation/TabBar.tsx - 自定义TabBar
import React from 'react'
import {
  View,
  TouchableOpacity,
  Text,
  StyleSheet,
  SafeAreaView,
} from 'react-native'
import { BottomTabBarProps } from '@react-navigation/bottom-tabs'
import { COLORS, SIZES, FONTS } from '@/constants'

const TabIcons = {
  Home: { active: '🏠', inactive: '🏠' },
  Search: { active: '🔍', inactive: '🔍' },
  Create: { active: '➕', inactive: '➕' },
  Notifications: { active: '🔔', inactive: '🔔' },
  Profile: { active: '👤', inactive: '👤' },
}

export const TabBar: React.FC<BottomTabBarProps> = ({
  state,
  descriptors,
  navigation,
}) => {
  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.tabBar}>
        {state.routes.map((route, index) => {
          const { options } = descriptors[route.key]
          const label = options.tabBarLabel || route.name
          const isFocused = state.index === index

          const onPress = () => {
            const event = navigation.emit({
              type: 'tabPress',
              target: route.key,
              canPreventDefault: true,
            })

            if (!isFocused && !event.defaultPrevented) {
              navigation.navigate(route.name)
            }
          }

          return (
            <TouchableOpacity
              key={route.key}
              onPress={onPress}
              style={styles.tab}
              activeOpacity={0.7}
            >
              <Text style={styles.icon}>
                {TabIcons[route.name]?.[isFocused ? 'active' : 'inactive']}
              </Text>
              <Text style={[
                styles.label,
                isFocused && styles.activeLabel,
              ]}>
                {label}
              </Text>
            </TouchableOpacity>
          )
        })}
      </View>
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: COLORS.surface,
  },
  tabBar: {
    flexDirection: 'row',
    backgroundColor: COLORS.surface,
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: COLORS.border,
    paddingVertical: SIZES.xs,
  },
  tab: {
    flex: 1,
    alignItems: 'center',
    paddingVertical: SIZES.xs,
  },
  icon: {
    fontSize: 20,
    marginBottom: 2,
  },
  label: {
    fontSize: FONTS.sizes.xs,
    fontFamily: FONTS.regular,
    color: COLORS.textSecondary,
  },
  activeLabel: {
    color: COLORS.primary,
    fontFamily: FONTS.medium,
  },
})

状态管理

Redux Toolkit配置

// src/store/index.ts - Store配置
import { configureStore } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { authSlice } from './slices/authSlice'
import { postsSlice } from './slices/postsSlice'
import { userSlice } from './slices/userSlice'

const persistConfig = {
  key: 'root',
  storage: AsyncStorage,
  whitelist: ['auth'], // 只持久化auth状态
}

const persistedAuthReducer = persistReducer(persistConfig, authSlice.reducer)

export const store = configureStore({
  reducer: {
    auth: persistedAuthReducer,
    posts: postsSlice.reducer,
    user: userSlice.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
      },
    }),
})

export const persistor = persistStore(store)

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

// src/store/slices/authSlice.ts - 认证状态
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
import { authService } from '@/services/api/authService'
import { User } from '@/types'

interface AuthState {
  user: User | null
  token: string | null
  refreshToken: string | null
  isAuthenticated: boolean
  isLoading: boolean
  error: string | null
}

const initialState: AuthState = {
  user: null,
  token: null,
  refreshToken: null,
  isAuthenticated: false,
  isLoading: false,
  error: null,
}

// 异步actions
export const login = createAsyncThunk(
  'auth/login',
  async (credentials: { email: string; password: string }) => {
    const response = await authService.login(credentials)
    return response.data
  }
)

export const register = createAsyncThunk(
  'auth/register',
  async (userData: { email: string; password: string; name: string }) => {
    const response = await authService.register(userData)
    return response.data
  }
)

export const refreshToken = createAsyncThunk(
  'auth/refreshToken',
  async (_, { getState }) => {
    const { auth } = getState() as { auth: AuthState }
    if (!auth.refreshToken) {
      throw new Error('No refresh token available')
    }
    const response = await authService.refreshToken(auth.refreshToken)
    return response.data
  }
)

export const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    logout: (state) => {
      state.user = null
      state.token = null
      state.refreshToken = null
      state.isAuthenticated = false
      state.error = null
    },
    clearError: (state) => {
      state.error = null
    },
    updateUser: (state, action: PayloadAction<Partial<User>>) => {
      if (state.user) {
        state.user = { ...state.user, ...action.payload }
      }
    },
  },
  extraReducers: (builder) => {
    builder
      // Login
      .addCase(login.pending, (state) => {
        state.isLoading = true
        state.error = null
      })
      .addCase(login.fulfilled, (state, action) => {
        state.isLoading = false
        state.user = action.payload.user
        state.token = action.payload.token
        state.refreshToken = action.payload.refreshToken
        state.isAuthenticated = true
      })
      .addCase(login.rejected, (state, action) => {
        state.isLoading = false
        state.error = action.error.message || 'Login failed'
      })
      // Register
      .addCase(register.pending, (state) => {
        state.isLoading = true
        state.error = null
      })
      .addCase(register.fulfilled, (state, action) => {
        state.isLoading = false
        state.user = action.payload.user
        state.token = action.payload.token
        state.refreshToken = action.payload.refreshToken
        state.isAuthenticated = true
      })
      .addCase(register.rejected, (state, action) => {
        state.isLoading = false
        state.error = action.error.message || 'Registration failed'
      })
      // Refresh Token
      .addCase(refreshToken.fulfilled, (state, action) => {
        state.token = action.payload.token
        state.refreshToken = action.payload.refreshToken
      })
  },
})

export const { logout, clearError, updateUser } = authSlice.actions

// src/store/slices/postsSlice.ts - 文章状态
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
import { postsService } from '@/services/api/postsService'
import { Post, PaginatedResponse } from '@/types'

interface PostsState {
  posts: Post[]
  currentPost: Post | null
  isLoading: boolean
  isRefreshing: boolean
  hasMore: boolean
  page: number
  error: string | null
}

const initialState: PostsState = {
  posts: [],
  currentPost: null,
  isLoading: false,
  isRefreshing: false,
  hasMore: true,
  page: 1,
  error: null,
}

export const fetchPosts = createAsyncThunk(
  'posts/fetchPosts',
  async (params: { page?: number; refresh?: boolean } = {}) => {
    const { page = 1, refresh = false } = params
    const response = await postsService.getPosts({ page, limit: 10 })
    return { ...response.data, refresh }
  }
)

export const fetchPostDetail = createAsyncThunk(
  'posts/fetchPostDetail',
  async (postId: string) => {
    const response = await postsService.getPost(postId)
    return response.data
  }
)

export const likePost = createAsyncThunk(
  'posts/likePost',
  async (postId: string) => {
    await postsService.likePost(postId)
    return postId
  }
)

export const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    clearError: (state) => {
      state.error = null
    },
    resetPosts: (state) => {
      state.posts = []
      state.page = 1
      state.hasMore = true
    },
  },
  extraReducers: (builder) => {
    builder
      // Fetch Posts
      .addCase(fetchPosts.pending, (state, action) => {
        if (action.meta.arg?.refresh) {
          state.isRefreshing = true
        } else {
          state.isLoading = true
        }
        state.error = null
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.isLoading = false
        state.isRefreshing = false
        
        const { data, pagination, refresh } = action.payload
        
        if (refresh) {
          state.posts = data
          state.page = 1
        } else {
          state.posts = [...state.posts, ...data]
        }
        
        state.page = pagination.page + 1
        state.hasMore = pagination.page < pagination.pages
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.isLoading = false
        state.isRefreshing = false
        state.error = action.error.message || 'Failed to fetch posts'
      })
      // Fetch Post Detail
      .addCase(fetchPostDetail.fulfilled, (state, action) => {
        state.currentPost = action.payload
      })
      // Like Post
      .addCase(likePost.fulfilled, (state, action) => {
        const postId = action.payload
        const post = state.posts.find(p => p.id === postId)
        if (post) {
          post.likes += 1
        }
        if (state.currentPost?.id === postId) {
          state.currentPost.likes += 1
        }
      })
  },
})

export const { clearError, resetPosts } = postsSlice.actions

总结

React Native移动开发的核心要点:

  1. 项目架构:合理的文件结构、TypeScript配置、依赖管理
  2. 组件开发:可复用的UI组件、业务组件封装
  3. 导航管理:React Navigation配置、路由设计
  4. 状态管理:Redux Toolkit、数据持久化
  5. 性能优化:图片优化、列表优化、内存管理

React Native为跨平台移动应用开发提供了高效的解决方案,通过合理的架构设计和最佳实践,可以构建出高质量的移动应用。