- 发布于
React Native移动开发实战:跨平台应用开发与性能优化
- 作者

- 姓名
- 全能波
- GitHub
- @weicracker
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移动开发的核心要点:
- 项目架构:合理的文件结构、TypeScript配置、依赖管理
- 组件开发:可复用的UI组件、业务组件封装
- 导航管理:React Navigation配置、路由设计
- 状态管理:Redux Toolkit、数据持久化
- 性能优化:图片优化、列表优化、内存管理
React Native为跨平台移动应用开发提供了高效的解决方案,通过合理的架构设计和最佳实践,可以构建出高质量的移动应用。