欢迎来到React Native Expo开发完整教程!
本教程将带您从零基础开始,通过4个精心设计的实战案例,逐步掌握React Native Expo开发的精髓。无论您是前端开发者、移动端开发者,还是完全的编程新手,都能在这里找到适合的学习路径。
访问 nodejs.org 下载并安装最新LTS版本。
# 验证安装
node --version
npm --version
npm install -g @expo/cli
在Google Play商店搜索"Expo Go"并安装
在App Store搜索"Expo Go"并安装
# 创建新项目,选择一个空白模板
npx create-expo-app my-first-app
cd my-first-app
# 启动开发服务器
npx expo start
启动后,您会看到一个二维码。使用Expo Go应用扫描二维码,即可在真机上预览应用效果。
用于包装其他组件,支持布局和样式
import { View, Text } from 'react-native';
<View style={{ padding: 20, backgroundColor: 'lightblue' }}>
<Text>Hello World!</Text>
</View>
显示文本内容,支持嵌套样式
import { Text } from 'react-native';
<Text style={{ fontSize: 18, color: 'darkblue' }}>
欢迎学习 <Text style={{ fontWeight: 'bold' }}>React Native</Text>
</Text>
在函数组件中添加状态管理的核心工具
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
export default function Counter() {
const [count, setCount] = useState(0); // 定义状态变量 count 及更新函数 setCount
return (
<View style={{ padding: 20 }}>
<Text>当前计数:{count}</Text>
<Button
title="增加"
onPress={() => setCount(count + 1)} // 点击按钮更新 count
/>
</View>
);
}
推荐使用 StyleSheet.create 创建样式对象,提高性能和可读性。
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f0f0f0'
},
text: {
fontSize: 24,
fontWeight: 'bold',
color: '#333'
},
button: {
backgroundColor: '#007bff',
padding: 15,
borderRadius: 5,
marginTop: 10
}
});
以下4个案例难度递增,建议按顺序完成。每个案例都包含完整的项目开发流程,从需求分析到最终发布。
counter-app/
├── App.js # 主应用组件,包含计数器逻辑和UI
└── package.json
import React, { useState } from 'react';
import {
SafeAreaView, // 确保内容在安全区域内显示
StyleSheet,
Text,
View,
TouchableOpacity, // 用于创建自定义按钮
Alert // 用于弹出确认框
} from 'react-native';
export default function App() {
const [count, setCount] = useState(0); // 初始化计数器状态为0
// 增加计数
const increment = () => setCount(prev => prev + 1);
// 减少计数,确保不小于0
const decrement = () => setCount(prev => (prev > 0 ? prev - 1 : 0));
// 重置计数器,带有确认弹窗
const reset = () => {
Alert.alert(
'重置确认',
'确定要重置计数器吗?',
[
{ text: '取消', style: 'cancel' },
{ text: '确定', onPress: () => setCount(0) }
]
);
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>计数器应用</Text>
<View style={styles.counterDisplay}>
<Text style={styles.countText}>{count}</Text>
</View>
<View style={styles.buttonContainer}>
<TouchableOpacity style={[styles.button, styles.decrementButton]} onPress={decrement}>
<Text style={styles.buttonText}>-</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, styles.resetButton]} onPress={reset}>
<Text style={styles.buttonText}>重置</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, styles.incrementButton]} onPress={increment}>
<Text style={styles.buttonText}>+</Text>
</TouchableOpacity>
</View>
<Text style={styles.subtitle}>使用 Expo 开发</Text>
</View>
</SafeAreaView>
);
}
// 样式定义
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 30,
textAlign: 'center',
},
counterDisplay: {
backgroundColor: 'white',
borderRadius: 15,
padding: 30,
marginBottom: 30,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
countText: {
fontSize: 48,
fontWeight: 'bold',
color: '#3498db',
textAlign: 'center',
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '80%',
marginBottom: 20,
},
button: {
padding: 20,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
minWidth: 60,
},
incrementButton: {
backgroundColor: '#27ae60', // 绿色
},
decrementButton: {
backgroundColor: '#e74c3c', // 红色
},
resetButton: {
backgroundColor: '#f39c12', // 黄色
paddingHorizontal: 20,
},
buttonText: {
color: 'white',
fontSize: 24,
fontWeight: 'bold',
},
subtitle: {
fontSize: 16,
color: '#7f8c8d',
textAlign: 'center',
},
});
todo-app/
├── App.js # 主应用入口,可能包含导航
├── components/ # 可复用的UI组件
│ ├── TodoInput.js # 输入待办事项组件
│ ├── TodoItem.js # 单个待办事项组件
│ └── FilterButtons.js # 筛选按钮组件
├── screens/ # 屏幕/页面组件
│ └── TodoScreen.js # 待办事项主屏幕
└── utils/ # 工具函数
└── storage.js # 本地存储工具
import AsyncStorage from '@react-native-async-storage/async-storage'; // 导入AsyncStorage
const STORAGE_KEY = '@todo_app_data';
export const storage = {
// 保存数据
async save(todos) {
try {
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
} catch (error) {
console.error('保存数据失败:', error);
}
},
// 读取数据
async load() {
try {
const data = await AsyncStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : []; // 如果没有数据,返回空数组
} catch (error) {
console.error('读取数据失败:', error);
return [];
}
},
// 清空数据 (可选)
async clear() {
try {
await AsyncStorage.removeItem(STORAGE_KEY);
} catch (error) {
console.error('清空数据失败:', error);
}
}
};
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
FlatList, // 用于高效渲染列表
KeyboardAvoidingView, // 避免键盘遮挡输入框
Platform, // 检测平台
Alert,
TextInput, // 输入框
TouchableOpacity // 自定义按钮
} from 'react-native';
// import TodoInput from '../components/TodoInput'; // 假设这些组件已定义
// import TodoItem from '../components/TodoItem';
// import FilterButtons from '../components/FilterButtons';
import { storage } from '../utils/storage'; // 导入本地存储工具
// 模拟的 TodoInput, TodoItem, FilterButtons
const TodoInput = ({ onAddTodo }) => {
const [text, setText] = useState('');
const handleAdd = () => {
onAddTodo(text);
setText('');
};
return (
<View style={todoInputStyles.container}>
<TextInput
style={todoInputStyles.input}
placeholder="添加新的待办事项..."
value={text}
onChangeText={setText}
onSubmitEditing={handleAdd}
/>
<TouchableOpacity style={todoInputStyles.button} onPress={handleAdd}>
<Text style={todoInputStyles.buttonText}>添加</Text>
</TouchableOpacity>
</View>
);
};
const todoInputStyles = StyleSheet.create({ /* ... */ }); // 简化,实际代码中应有样式
const TodoItem = ({ todo, onToggle, onDelete, onEdit }) => (
<View style={todoItemStyles.container}>
<TouchableOpacity onPress={onToggle} style={todoItemStyles.checkbox}>
<Text>{todo.completed ? '✅' : '⬜'}</Text>
</TouchableOpacity>
<Text
style={[
todoItemStyles.text,
todo.completed && todoItemStyles.completedText
]}
>
{todo.text}
</Text>
<TouchableOpacity onPress={onDelete} style={todoItemStyles.deleteButton}>
<Text style={todoItemStyles.deleteButtonText}>删除</Text>
</TouchableOpacity>
</View>
);
const todoItemStyles = StyleSheet.create({ /* ... */ }); // 简化
const FilterButtons = ({ currentFilter, onFilterChange, stats }) => (
<View style={filterButtonsStyles.container}>
{['all', 'active', 'completed'].map(filter => (
<TouchableOpacity
key={filter}
style={[
filterButtonsStyles.button,
currentFilter === filter && filterButtonsStyles.activeButton
]}
onPress={() => onFilterChange(filter)}
>
<Text style={filterButtonsStyles.buttonText}>{filter.toUpperCase()}</Text>
</TouchableOpacity>
))}
</View>
);
const filterButtonsStyles = StyleSheet.create({ /* ... */ }); // 简化
const FILTER_OPTIONS = {
ALL: 'all',
ACTIVE: 'active',
COMPLETED: 'completed'
};
export default function TodoScreen() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState(FILTER_OPTIONS.ALL);
// 初次加载时从本地存储读取待办事项
useEffect(() => {
loadTodos();
}, []);
// 待办事项列表更新时,保存到本地存储
useEffect(() => {
if (todos.length >= 0) { // 确保即使空数组也保存
storage.save(todos);
}
}, [todos]);
// 从本地存储加载待办事项
const loadTodos = async () => {
const savedTodos = await storage.load();
setTodos(savedTodos);
};
// 添加待办事项
const addTodo = useCallback((text) => {
if (text.trim()) {
const newTodo = {
id: Date.now().toString(), // 使用时间戳作为唯一ID
text: text.trim(),
completed: false,
createdAt: new Date().toISOString()
};
setTodos(prev => [newTodo, ...prev]); // 新事项添加到列表顶部
}
}, []);
// 切换待办事项的完成状态
const toggleTodo = useCallback((id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
// 删除待办事项,带有确认弹窗
const deleteTodo = useCallback((id) => {
Alert.alert(
'确认删除',
'确定要删除这个待办事项吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '删除',
style: 'destructive',
onPress: () => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}
}
]
);
}, []);
// 编辑待办事项文本
const editTodo = useCallback((id, newText) => {
if (newText.trim()) {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, text: newText.trim() } : todo
)
);
}
}, []);
// 根据当前筛选条件过滤待办事项
const filteredTodos = todos.filter(todo => {
switch (filter) {
case FILTER_OPTIONS.ACTIVE:
return !todo.completed;
case FILTER_OPTIONS.COMPLETED:
return todo.completed;
default: // ALL
return true;
}
});
// 计算待办事项统计数据
const getStats = () => {
const total = todos.length;
const completed = todos.filter(todo => todo.completed).length;
const active = total - completed;
return { total, completed, active };
};
const stats = getStats();
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} // iOS使用'padding',Android使用'height'
>
<View style={styles.content}>
<Text style={styles.title}>我的待办事项</Text>
{/* 统计信息 */}
<View style={styles.statsContainer}>
<Text style={styles.statsText}>
总计: {stats.total} | 进行中: {stats.active} | 已完成: {stats.completed}
</Text>
</View>
{/* 输入框 */}
<TodoInput onAddTodo={addTodo} />
{/* 筛选按钮 */}
<FilterButtons
currentFilter={filter}
onFilterChange={setFilter}
stats={stats}
/>
{/* 待办事项列表 */}
<FlatList
style={styles.list}
data={filteredTodos}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TodoItem
todo={item}
onToggle={() => toggleTodo(item.id)}
onDelete={() => deleteTodo(item.id)}
onEdit={(newText) => editTodo(item.id, newText)}
/>
)}
showsVerticalScrollIndicator={false}
ListEmptyComponent={ // 列表为空时的提示
<Text style={styles.emptyListText}>暂无待办事项,快来添加吧!</Text>
}
/>
</View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f9fa',
},
content: {
flex: 1,
padding: 20,
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 20, // 适配Android刘海屏
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 20,
},
statsContainer: {
backgroundColor: 'white',
padding: 15,
borderRadius: 10,
marginBottom: 15,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
statsText: {
textAlign: 'center',
color: '#7f8c8d',
fontSize: 14,
},
list: {
flex: 1,
},
emptyListText: {
textAlign: 'center',
marginTop: 50,
fontSize: 16,
color: '#7f8c8d',
},
// 简化的 TodoInput 样式
...StyleSheet.create({
container: {
flexDirection: 'row',
marginBottom: 15,
height: 50,
},
input: {
flex: 1,
borderColor: '#ccc',
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 10,
marginRight: 10,
backgroundColor: 'white',
fontSize: 16,
},
button: {
backgroundColor: '#3498db',
paddingHorizontal: 20,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
}).todoInput,
// 简化的 TodoItem 样式
...StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
padding: 15,
borderRadius: 10,
marginBottom: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
checkbox: {
marginRight: 10,
padding: 5,
},
text: {
flex: 1,
fontSize: 17,
color: '#333',
},
completedText: {
textDecorationLine: 'line-through',
color: '#aaa',
},
deleteButton: {
backgroundColor: '#e74c3c',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 5,
marginLeft: 10,
},
deleteButtonText: {
color: 'white',
fontSize: 14,
},
}).todoItem,
// 简化的 FilterButtons 样式
...StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 20,
backgroundColor: 'white',
borderRadius: 10,
padding: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
button: {
flex: 1,
paddingVertical: 10,
borderRadius: 8,
alignItems: 'center',
},
activeButton: {
backgroundColor: '#3498db',
},
buttonText: {
color: '#555',
fontWeight: 'bold',
},
activeButtonText: {
color: 'white',
},
}).filterButtons,
});
使用 AsyncStorage 实现数据持久化
实现多种过滤条件的数据展示
支持待办事项的即时编辑功能
使用 useState 和 useCallback 优化性能
weather-app/
├── App.js
├── components/
│ ├── WeatherCard.js # 当前天气卡片
│ ├── SearchBar.js # 城市搜索栏
│ ├── ForecastList.js # 7天预报列表
│ └── LoadingSpinner.js # 加载指示器
├── services/
│ ├── weatherAPI.js # OpenWeatherMap API服务
│ └── locationService.js # Expo Location 服务
└── screens/
└── WeatherScreen.js # 主天气显示屏幕
import { WeatherAPI_KEY } from '@env'; // 假设您使用@env或类似库管理环境变量
const BASE_URL = 'https://api.openweathermap.org/data/2.5';
export const weatherAPI = {
// 获取当前位置天气
async getCurrentWeather(lat, lon) {
try {
const response = await fetch(
`${BASE_URL}/weather?lat=${lat}&lon=${lon}&appid=${WeatherAPI_KEY}&units=metric&lang=zh_cn`
);
const data = await response.json();
if (data.cod !== 200) throw new Error(data.message || '获取当前天气失败');
return this.formatWeatherData(data);
} catch (error) {
console.error('获取天气数据失败:', error);
throw error;
}
},
// 搜索城市天气
async searchCityWeather(cityName) {
try {
const response = await fetch(
`${BASE_URL}/weather?q=${encodeURIComponent(cityName)}&appid=${WeatherAPI_KEY}&units=metric&lang=zh_cn`
);
const data = await response.json();
if (data.cod !== 200) throw new Error(data.message || '搜索城市天气失败');
return this.formatWeatherData(data);
} catch (error) {
console.error('搜索城市天气失败:', error);
throw error;
}
},
// 获取7天天气预报 (需要One Call API,该API返回当前、小时、日预报)
async getForecast(lat, lon) {
try {
const response = await fetch(
`${BASE_URL}/onecall?lat=${lat}&lon=${lon}&exclude=minutely,hourly,alerts&appid=${WeatherAPI_KEY}&units=metric&lang=zh_cn`
);
const data = await response.json();
if (data.cod) throw new Error(data.message || '获取预报失败'); // One Call API没有cod字段,但以防万一
return this.formatForecastData(data);
} catch (error) {
console.error('获取预报数据失败:', error);
throw error;
}
},
// 格式化当前天气数据
formatWeatherData(data) {
return {
city: data.name,
country: data.sys.country,
temperature: Math.round(data.main.temp),
description: data.weather[0].description,
icon: data.weather[0].icon,
humidity: data.main.humidity,
windSpeed: data.wind.speed,
pressure: data.main.pressure,
feelsLike: Math.round(data.main.feels_like),
visibility: data.visibility / 1000, // 转换为公里
lat: data.coord.lat, // 用于获取预报
lon: data.coord.lon, // 用于获取预报
};
},
// 格式化预报数据
formatForecastData(data) {
return data.daily.slice(0, 7).map(day => ({
date: new Date(day.dt * 1000).toLocaleDateString('zh-CN', {
weekday: 'long',
month: 'short',
day: 'numeric'
}),
high: Math.round(day.temp.max),
low: Math.round(day.temp.min),
description: day.weather[0].description,
icon: day.weather[0].icon,
humidity: day.humidity,
windSpeed: day.wind_speed,
}));
}
};
import * as Location from 'expo-location'; // 导入Expo位置服务
export const locationService = {
// 请求位置权限
async requestLocationPermission() {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
throw new Error('位置权限被拒绝,请在设置中允许应用获取位置信息。');
}
return status;
},
// 获取当前位置
async getCurrentPosition() {
try {
const status = await this.requestLocationPermission();
if (status !== 'granted') {
throw new Error('需要位置权限才能获取天气');
}
// 获取当前位置,精度高
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High,
});
return {
latitude: location.coords.latitude,
longitude: location.coords.longitude,
};
} catch (error) {
console.error('获取位置失败:', error);
throw error;
}
},
// 反向地理编码(坐标转地址,可选功能)
async reverseGeocode(latitude, longitude) {
try {
const addresses = await Location.reverseGeocodeAsync({
latitude,
longitude,
});
if (addresses.length > 0) {
const address = addresses[0];
return {
city: address.city || address.region, // 优先城市,其次是区域
region: address.region,
country: address.country,
};
}
return null;
} catch (error) {
console.error('反向地理编码失败:', error);
return null;
}
}
};
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView, // 用于可滚动内容
RefreshControl, // 下拉刷新
Alert,
StatusBar, // 控制状态栏样式
} from 'react-native';
import { weatherAPI } from '../services/weatherAPI';
import { locationService } from '../services/locationService';
// import WeatherCard from '../components/WeatherCard'; // 假设已定义
// import SearchBar from '../components/SearchBar';
// import ForecastList from '../components/ForecastList';
// import LoadingSpinner from '../components/LoadingSpinner';
// 模拟的 WeatherCard, SearchBar, ForecastList, LoadingSpinner
const WeatherCard = ({ weather }) => (
<View style={weatherCardStyles.container}>
<Text style={weatherCardStyles.city}>{weather.city}, {weather.country}</Text>
<Text style={weatherCardStyles.temperature}>{weather.temperature}°C</Text>
<Text style={weatherCardStyles.description}>{weather.description}</Text>
{/* 更多天气详情 */}
</View>
);
const weatherCardStyles = StyleSheet.create({ /* ... */ });
const SearchBar = ({ onSearch, onLocationPress, searching }) => {
const [query, setQuery] = useState('');
return (
<View style={searchBarStyles.container}>
<TextInput
style={searchBarStyles.input}
placeholder="搜索城市..."
value={query}
onChangeText={setQuery}
onSubmitEditing={() => onSearch(query)}
/>
<TouchableOpacity style={searchBarStyles.button} onPress={() => onSearch(query)}>
<Text style={searchBarStyles.buttonText}>搜索</Text>
</TouchableOpacity>
<TouchableOpacity style={searchBarStyles.button} onPress={onLocationPress}>
<Text style={searchBarStyles.buttonText}>📍</Text>
</TouchableOpacity>
</View>
);
};
const searchBarStyles = StyleSheet.create({ /* ... */ });
const ForecastList = ({ forecast }) => (
<View style={forecastListStyles.container}>
<Text style={forecastListStyles.title}>未来7天预报</Text>
{forecast.map((day, index) => (
<View key={index} style={forecastListStyles.item}>
<Text style={forecastListStyles.date}>{day.date}</Text>
<Text>{day.high}°C / {day.low}°C</Text>
<Text>{day.description}</Text>
</View>
))}
</View>
);
const forecastListStyles = StyleSheet.create({ /* ... */ });
const LoadingSpinner = () => (
<View style={loadingSpinnerStyles.container}>
<ActivityIndicator size="large" color="#0000ff" />
<Text>加载中...</Text>
</View>
);
const loadingSpinnerStyles = StyleSheet.create({ /* ... */ });
export default function WeatherScreen() {
const [weather, setWeather] = useState(null);
const [forecast, setForecast] = useState([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [searching, setSearching] = useState(false);
// 初始化加载天气数据
useEffect(() => {
loadWeatherData(true); // 默认加载当前位置天气
}, []);
// 加载天气数据函数
const loadWeatherData = async (useLocation = true, cityName = null, lat = null, lon = null) => {
try {
setLoading(true);
let currentLat = lat;
let currentLon = lon;
if (useLocation) {
// 使用当前位置
const location = await locationService.getCurrentPosition();
currentLat = location.latitude;
currentLon = location.longitude;
} else if (cityName) {
// 搜索指定城市,需要先获取城市坐标
// OpenWeatherMap的/weather接口会返回coord,可以提取出来用于获取forecast
const searchedWeather = await weatherAPI.searchCityWeather(cityName);
setWeather(searchedWeather); // 立即更新当前天气
currentLat = searchedWeather.lat;
currentLon = searchedWeather.lon;
}
if (currentLat && currentLon) {
// 同时请求当前天气和预报
const [weatherData, forecastData] = await Promise.all([
useLocation ? weatherAPI.getCurrentWeather(currentLat, currentLon) : weatherAPI.searchCityWeather(cityName),
weatherAPI.getForecast(currentLat, currentLon),
]);
setWeather(weatherData);
setForecast(forecastData);
} else {
throw new Error('无法获取地理位置信息');
}
} catch (error) {
Alert.alert('错误', error.message || '获取天气数据失败');
setWeather(null); // 清空天气数据
setForecast([]);
} finally {
setLoading(false);
setRefreshing(false);
setSearching(false);
}
};
// 下拉刷新回调
const onRefresh = useCallback(() => {
setRefreshing(true);
// 刷新当前显示的天气,如果是搜索结果,则刷新搜索结果
if (weather && weather.lat && weather.lon) {
loadWeatherData(false, null, weather.lat, weather.lon);
} else {
loadWeatherData(true); // 否则加载当前位置
}
}, [weather]);
// 处理城市搜索
const handleSearch = useCallback(async (query) => {
if (!query.trim()) return;
setSearching(true);
await loadWeatherData(false, query.trim());
}, []);
// 切换到当前位置
const handleLocationPress = useCallback(() => {
loadWeatherData(true);
}, []);
if (loading) {
return <LoadingSpinner />;
}
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" backgroundColor="#74b9ff" />
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={['#0984e3']}
tintColor="#0984e3" // iOS加载指示器颜色
/>
}
>
{/* 搜索栏 */}
<SearchBar
onSearch={handleSearch}
onLocationPress={handleLocationPress}
searching={searching}
/>
{/* 当前天气卡片 */}
{weather ? (
<WeatherCard
weather={weather}
/>
) : (
<Text style={styles.noDataText}>未能获取天气数据。</Text>
)}
{/* 7天预报 */}
{forecast.length > 0 && (
<ForecastList forecast={forecast} />
)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#74b9ff', // 背景色为蓝色
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingBottom: 20,
alignItems: 'center', // 使内容居中
},
noDataText: {
color: 'white',
fontSize: 18,
marginTop: 50,
},
});
使用 Expo Location 获取用户位置
集成 OpenWeatherMap 天气服务
适配不同屏幕尺寸的布局
添加天气状态动画效果(此教程未详述,作为扩展点)
social-app/
├── App.js # 主入口,可能包含AuthContext提供者
├── config/ # Firebase配置
│ └── firebase.js
├── navigation/ # 导航配置
│ └── AppNavigator.js # 根导航器
├── services/ # 业务逻辑服务
│ ├── authService.js # Firebase认证服务
│ ├── database.js # Firestore数据库操作
│ ├── storageService.js # Firebase Storage图片存储
│ └── notificationService.js # 推送通知服务
├── screens/ # 页面组件
│ ├── AuthScreen.js # 登录/注册界面
│ ├── HomeScreen.js # 动态主页
│ ├── ProfileScreen.js # 个人资料页
│ ├── ChatScreen.js # 聊天界面
│ └── PostScreen.js # 发布动态界面
├── components/ # 可复用UI组件
│ ├── PostCard.js # 动态展示卡片
│ ├── ChatBubble.js # 聊天气泡
│ └── UserAvatar.js # 用户头像组件
└── context/ # React Context API
└── AuthContext.js # 认证状态上下文
import {
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut,
updateProfile, // 更新用户资料
onAuthStateChanged // 监听认证状态
} from 'firebase/auth';
import { auth } from '../config/firebase'; // 导入Firebase认证实例
import { storageService } from './storageService'; // 用于本地存储用户信息
export const authService = {
// 监听认证状态变化
onAuthStateChange(callback) {
return onAuthStateChanged(auth, callback);
},
// 注册新用户
async signUp(email, password, displayName) {
try {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
const user = userCredential.user;
// 更新用户资料,设置显示名称
await updateProfile(user, {
displayName: displayName
});
// 保存用户信息到本地
await storageService.saveUserInfo({
uid: user.uid,
email: user.email,
displayName: user.displayName,
photoURL: user.photoURL,
createdAt: new Date().toISOString()
});
return user;
} catch (error) {
console.error('注册失败:', error);
throw this.handleAuthError(error);
}
},
// 用户登录
async signIn(email, password) {
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
const user = userCredential.user;
// 更新最后登录时间,并保存用户信息
await storageService.saveUserInfo({
uid: user.uid, // 确保保存完整的用户对象或必要字段
email: user.email,
displayName: user.displayName,
photoURL: user.photoURL,
lastLoginAt: new Date().toISOString()
});
return user;
} catch (error) {
console.error('登录失败:', error);
throw this.handleAuthError(error);
}
},
// 用户登出
async signOut() {
try {
await signOut(auth);
await storageService.clearUserInfo(); // 清除本地存储的用户信息
} catch (error) {
console.error('登出失败:', error);
throw error;
}
},
// 获取当前用户
getCurrentUser() {
return auth.currentUser;
},
// 统一错误处理
handleAuthError(error) {
const errorMessages = {
'auth/user-not-found': '用户不存在',
'auth/wrong-password': '密码错误',
'auth/email-already-in-use': '邮箱已被使用',
'auth/weak-password': '密码强度不够',
'auth/invalid-email': '邮箱格式无效',
'auth/too-many-requests': '请求过于频繁,请稍后重试'
};
return new Error(errorMessages[error.code] || '认证失败');
}
};
import {
collection,
doc,
addDoc,
updateDoc,
deleteDoc,
onSnapshot, // 实时监听
query,
orderBy,
where, // 查询条件
limit, // 限制数量
startAfter, // 分页加载
getDocs, // 获取一次性快照
serverTimestamp // 服务器时间戳
} from 'firebase/firestore';
import { db } from '../config/firebase'; // 导入Firestore实例
export const databaseService = {
// 动态相关操作
posts: {
// 发布动态
async createPost(userId, content, imageUrl = null) {
try {
const postData = {
userId,
content,
imageUrl,
likes: [], // 点赞用户ID列表
commentsCount: 0, // 评论数量
createdAt: serverTimestamp(),
updatedAt: serverTimestamp()
};
const docRef = await addDoc(collection(db, 'posts'), postData);
return docRef.id;
} catch (error) {
console.error('发布动态失败:', error);
throw error;
}
},
// 获取动态列表 (支持分页)
async getPosts(limitCount = 10, lastPostSnapshot = null) {
try {
let postsQuery = query(
collection(db, 'posts'),
orderBy('createdAt', 'desc'),
limit(limitCount)
);
if (lastPostSnapshot) { // 如果提供了上一个文档快照,则从该文档之后开始加载
postsQuery = query(postsQuery, startAfter(lastPostSnapshot));
}
const snapshot = await getDocs(postsQuery);
return {
posts: snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })),
lastVisible: snapshot.docs[snapshot.docs.length - 1] // 返回最后一个文档快照用于下一页
};
} catch (error) {
console.error('获取动态失败:', error);
throw error;
}
},
// 监听动态实时更新 (用于首页实时展示)
listenToPosts(callback) {
const postsQuery = query(
collection(db, 'posts'),
orderBy('createdAt', 'desc'),
limit(50) // 最多监听50条最新动态
);
return onSnapshot(postsQuery, (snapshot) => {
const posts = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
callback(posts);
}, (error) => {
console.error('监听动态失败:', error);
});
},
// 点赞/取消点赞
async toggleLike(postId, userId) {
try {
const postRef = doc(db, 'posts', postId);
// 首先获取当前点赞列表
const postDoc = await getDocs(query(collection(db, 'posts'), where('__name__', '==', postId)));
if (!postDoc.empty) {
const postData = postDoc.docs[0].data();
const likes = postData.likes || [];
const isLiked = likes.includes(userId);
const updatedLikes = isLiked
? likes.filter(id => id !== userId) // 取消点赞
: [...likes, userId]; // 点赞
await updateDoc(postRef, {
likes: updatedLikes,
updatedAt: serverTimestamp()
});
}
} catch (error) {
console.error('点赞失败:', error);
throw error;
}
}
},
// 评论相关操作
comments: {
// 发表评论
async addComment(postId, userId, content) {
try {
const commentData = {
postId,
userId,
content,
createdAt: serverTimestamp()
};
const docRef = await addDoc(collection(db, 'comments'), commentData);
// 同时更新动态的评论计数
const postRef = doc(db, 'posts', postId);
await updateDoc(postRef, {
commentsCount: (await getPostCommentsCount(postId)) + 1, // 获取当前评论数+1
updatedAt: serverTimestamp()
});
return docRef.id;
} catch (error) {
console.error('发表评论失败:', error);
throw error;
}
},
// 获取某个动态的评论列表
listenToPostComments(postId, callback) {
const commentsQuery = query(
collection(db, 'comments'),
where('postId', '==', postId),
orderBy('createdAt', 'asc')
);
return onSnapshot(commentsQuery, (snapshot) => {
const comments = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
callback(comments);
}, (error) => {
console.error('监听评论失败:', error);
});
},
// 获取评论数量
async getPostCommentsCount(postId) {
const commentsQuery = query(
collection(db, 'comments'),
where('postId', '==', postId)
);
const snapshot = await getDocs(commentsQuery);
return snapshot.size;
}
},
// 聊天消息相关
messages: {
// 发送消息
async sendMessage(conversationId, senderId, content, type = 'text') {
try {
const messageData = {
conversationId,
senderId,
content,
type,
createdAt: serverTimestamp(),
read: false // 消息是否已读
};
const docRef = await addDoc(collection(db, 'messages'), messageData);
return docRef.id;
} catch (error) {
console.error('发送消息失败:', error);
throw error;
}
},
// 监听会话消息
listenToMessages(conversationId, callback) {
const messagesQuery = query(
collection(db, 'messages'),
where('conversationId', '==', conversationId),
orderBy('createdAt', 'asc')
);
return onSnapshot(messagesQuery, (snapshot) => {
const messages = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
callback(messages);
}, (error) => {
console.error('监听消息失败:', error);
});
}
}
};
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { authService } from '../services/authService';
import { storageService } from '../services/storageService'; // 用于本地存储用户session
const AuthContext = createContext(); // 创建认证上下文
// 认证状态的reducer函数
const authReducer = (state, action) => {
switch (action.type) {
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_USER':
return { ...state, user: action.payload, loading: false, error: null }; // 设置用户,清除错误
case 'SIGN_OUT':
return { ...state, user: null, loading: false, error: null }; // 登出,清除用户和错误
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false }; // 设置错误
default:
return state;
}
};
const initialState = {
user: null,
loading: true, // 初始加载状态
error: null
};
export const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, initialState);
// 初始化认证状态:在应用启动时检查用户登录状态
useEffect(() => {
const initializeAuth = async () => {
try {
// 首先尝试从本地存储加载用户信息(可选,用于快速恢复状态)
const savedUser = await storageService.getUserInfo();
if (savedUser) {
// 如果有本地用户,且Firebase当前也有用户,则设置
const currentUser = authService.getCurrentUser();
if (currentUser && currentUser.uid === savedUser.uid) { // 确保是同一个用户
dispatch({ type: 'SET_USER', payload: currentUser });
} else {
// 本地用户失效或不匹配,清除本地数据
await storageService.clearUserInfo();
dispatch({ type: 'SET_USER', payload: null });
}
} else {
dispatch({ type: 'SET_USER', payload: null });
}
} catch (error) {
console.error('初始化认证状态失败:', error);
dispatch({ type: 'SET_USER', payload: null });
dispatch({ type: 'SET_ERROR', payload: error.message });
}
// 监听Firebase认证状态变化,这是最权威的
const unsubscribe = authService.onAuthStateChange((user) => {
if (user) {
dispatch({ type: 'SET_USER', payload: user });
// 每次登录成功也更新本地存储
storageService.saveUserInfo({
uid: user.uid,
email: user.email,
displayName: user.displayName,
photoURL: user.photoURL,
lastLoginAt: new Date().toISOString()
});
} else {
dispatch({ type: 'SIGN_OUT' });
storageService.clearUserInfo(); // 登出时清除本地存储
}
});
return unsubscribe; // 清理订阅
};
initializeAuth();
}, []);
// 登录函数
const signIn = async (email, password) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
dispatch({ type: 'SET_ERROR', payload: null });
const user = await authService.signIn(email, password);
// SET_USER会在onAuthStateChange中触发
return user;
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
throw error;
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
// 注册函数
const signUp = async (email, password, displayName) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
dispatch({ type: 'SET_ERROR', payload: null });
const user = await authService.signUp(email, password, displayName);
// SET_USER会在onAuthStateChange中触发
return user;
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
throw error;
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
// 登出函数
const signOut = async () => {
try {
await authService.signOut();
// SIGN_OUT会在onAuthStateChange中触发
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
throw error;
}
};
// 提供给子组件的上下文值
const value = {
user: state.user,
loading: state.loading,
error: state.error,
signIn,
signUp,
signOut
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
// 自定义Hook,用于在组件中方便地使用认证上下文
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
使用Firebase Authentication实现完整的注册、登录、登出
使用Firestore实现实时数据同步(动态、评论、聊天)
集成Firebase Storage处理图片资源
实现消息推送和系统通知(需要Expo Notifications)
# 安装核心导航组件
npx expo install @react-navigation/native @react-navigation/bottom-tabs @react-navigation/stack
# 安装导航所需的其他依赖
npx expo install react-native-screens react-native-safe-area-context
import React from 'react';
import { NavigationContainer } from '@react-navigation/native'; // 导航容器
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; // 底部Tab导航
import { createStackNavigator } from '@react-navigation/stack'; // 堆栈导航
import { Ionicons } from '@expo/vector-icons'; // 图标库
// 假设的屏幕组件
import HomeScreen from '../screens/HomeScreen';
import ProfileScreen from '../screens/ProfileScreen';
import SettingsScreen from '../screens/SettingsScreen';
import DetailScreen from '../screens/DetailScreen';
const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();
// HomeStack: 一个包含Home和Detail页面的堆栈导航
function HomeStack() {
return (
<Stack.Navigator initialRouteName="HomeContent">
<Stack.Screen name="HomeContent" component={HomeScreen} options={{ title: '首页' }} />
<Stack.Screen name="Detail" component={DetailScreen} options={{ title: '详情' }} />
</Stack.Navigator>
);
}
// AppNavigator: 应用程序的主导航,底部Tab导航
export default function AppNavigator() {
return (
<NavigationContainer>
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
// 根据路由名称设置不同的图标
if (route.name === 'HomeTab') {
iconName = focused ? 'home' : 'home-outline';
} else if (route.name === 'Profile') {
iconName = focused ? 'person' : 'person-outline';
} else if (route.name === 'Settings') {
iconName = focused ? 'settings' : 'settings-outline';
}
// 返回Ionicons组件
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#007AFF', // 选中Tab的颜色
tabBarInactiveTintColor: 'gray', // 未选中Tab的颜色
headerShown: false, // 默认隐藏顶部导航栏,由Stack Navigator控制
})}
>
<Tab.Screen
name="HomeTab"
component={HomeStack} // Tab中嵌套Stack导航
options={{ title: '首页' }} // TabBar中显示的名称
/>
<Tab.Screen name="Profile" component={ProfileScreen} options={{ title: '我的' }} />
<Tab.Screen name="Settings" component={SettingsScreen} options={{ title: '设置' }} />
</Tab.Navigator>
</NavigationContainer>
);
}
import { useNavigation, useRoute } from '@react-navigation/native';
// 在源屏幕中传递参数
const navigation = useNavigation();
navigation.navigate('Detail', {
itemId: 123,
itemTitle: '产品详情'
});
// 在目标屏幕中接收参数
const route = useRoute();
const { itemId, itemTitle } = route.params;
// 返回上一页
navigation.goBack();
// 返回到导航栈的根页面
navigation.popToTop();
// 替换当前屏幕,使返回按钮不会回到当前屏幕
navigation.replace('NewScreen', { data: 'some data' });
使用 expo-camera 和 expo-image-picker
import { Camera } from 'expo-camera';
import * as ImagePicker from 'expo-image-picker';
import React, { useRef, useState, useEffect } from 'react';
import { View, Button, Image, Alert } from 'react-native';
function CameraExample() {
const [hasPermission, setHasPermission] = useState(null);
const cameraRef = useRef(null);
useEffect(() => {
(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === 'granted');
})();
}, []);
const takePicture = async () => {
if (cameraRef.current) {
const photo = await cameraRef.current.takePictureAsync();
console.log(photo.uri); // 照片的URI
// 可以在此处将照片上传或显示
}
};
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.canceled) {
console.log(result.assets[0].uri); // 选择的图片URI
}
};
if (hasPermission === null) return <View />;
if (hasPermission === false) return <Text>无权限访问相机</Text>;
return (
<View style={{ flex: 1 }}>
<Camera style={{ flex: 1 }} type={Camera.Constants.Type.back} ref={cameraRef}>
<View style={{ flex: 1, backgroundColor: 'transparent', flexDirection: 'row' }}>
<Button title="拍照" onPress={takePicture} />
<Button title="选择图片" onPress={pickImage} />
</View>
</Camera>
</View>
);
}
使用 expo-location 获取设备位置
import * as Location from 'expo-location';
import React, { useState, useEffect } from 'react';
import { View, Text, Button, Alert } from 'react-native';
function LocationExample() {
const [location, setLocation] = useState(null);
const [errorMsg, setErrorMsg] = useState(null);
useEffect(() => {
(async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('位置权限被拒绝');
Alert.alert('权限不足', '需要位置权限才能获取您的位置');
return;
}
let currentLocation = await Location.getCurrentPositionAsync({});
setLocation(currentLocation);
})();
}, []);
return (
<View>
<Text>{errorMsg || '等待获取位置...'}</Text>
{location && (
<Text>
纬度: {location.coords.latitude},
经度: {location.coords.longitude}
</Text>
)}
</View>
);
}
使用 expo-notifications 实现本地和远程推送
import * as Notifications from 'expo-notifications';
import React, { useEffect } from 'react';
import { Button, View, Text, Alert } from 'react-native';
// 设置通知处理程序,控制应用在前台时通知的行为
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true, // 是否显示通知横幅
shouldPlaySound: true, // 是否播放声音
shouldSetBadge: false, // 是否更新应用图标上的徽章
}),
});
function NotificationExample() {
useEffect(() => {
// 请求通知权限
(async () => {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
Alert.alert('权限不足', '无法发送推送通知,请在设置中启用通知权限。');
return;
}
// 获取Expo Push Token,用于发送远程通知
const token = (await Notifications.getExpoPushTokenAsync()).data;
console.log('Expo Push Token:', token);
// 您可以将此token发送到您的后端服务器,以便通过Expo的API发送通知
})();
}, []);
// 发送本地通知
const sendLocalNotification = async () => {
await Notifications.scheduleNotificationAsync({
content: {
title: '我的本地通知!',
body: '这是从您的应用内部发送的本地通知。',
data: { someData: 'goes here' }, // 可携带自定义数据
},
trigger: { seconds: 2 }, // 2秒后触发
});
Alert.alert('通知已发送', '您将在2秒后收到本地通知。');
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title="发送本地通知" onPress={sendLocalNotification} />
</View>
);
}
使用 react-native-reanimated 实现高性能声明式动画
import Animated, {
useAnimatedStyle, // 用于创建动画样式
useSharedValue, // 创建共享值
withTiming, // 时间动画
withSpring, // 弹性动画
withDelay, // 延迟动画
} from 'react-native-reanimated';
import { View, Button, StyleSheet } from 'react-native';
import React from 'react';
function AnimatedBox() {
const translateX = useSharedValue(0); // 声明一个共享值,初始为0
const scale = useSharedValue(1); // 声明一个共享值,初始为1
// 创建动画样式,会监听共享值的变化
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ scale: scale.value }
],
}));
const handleAnimate = () => {
// 点击后,translateX值变为100,带有时间动画效果
translateX.value = withTiming(100, { duration: 1000 });
// scale值变为1.5,带有弹性动画效果
scale.value = withSpring(1.5, { damping: 10, stiffness: 100 });
};
const handleReset = () => {
translateX.value = withTiming(0, { duration: 500 });
scale.value = withSpring(1, { damping: 10, stiffness: 100 });
};
return (
<View style={styles.container}>
<Animated.View style={[styles.box, animatedStyle]} />
<Button title="动画一下" onPress={handleAnimate} />
<Button title="重置" onPress={handleReset} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
box: {
width: 100,
height: 100,
backgroundColor: '#3498db',
borderRadius: 10,
marginBottom: 20,
},
});
Managed Workflow 适合大部分初学者和需要快速迭代、不涉及复杂原生功能的项目。
当您的项目需要深度定制原生功能,集成某些Expo SDK不支持的第三方原生库时,可以考虑 Bare Workflow。
您也可以从 Managed Workflow 通过 expo eject 或 EAS Build 的自定义插件功能逐步迁移到 Bare Workflow。
EAS 是 Expo 提供的一套云服务,用于构建、提交和更新您的Expo和React Native应用。
# 安装 EAS CLI (如果尚未安装)
npm install -g eas-cli
# 登录 Expo 账号 (在浏览器中完成认证)
eas login
# 初始化项目配置 (创建 eas.json 文件)
eas build:configure
# 构建 Android 应用 (生成 .apk 或 .aab 文件)
eas build --platform android --profile production
# 构建 iOS 应用 (生成 .ipa 文件,需要配置 Apple 开发者账号)
eas build --platform ios --profile production
# 提交应用到 Google Play Store
eas submit --platform android --latest
# 提交应用到 Apple App Store Connect
eas submit --platform ios --latest
通过 app.json (或 app.config.js) 配置应用程序在不同平台上的行为和外观。
# app.json 配置示例
{
"expo": {
"name": "我的应用", // 应用在设备上显示的名称
"slug": "my-app-slug", // Expo项目唯一标识符,通常用于URL
""version": "1.0.0", // 应用版本号
"orientation": "portrait", // 应用方向:portrait (竖屏) 或 landscape (横屏)
"icon": "./assets/icon.png", // 应用图标路径
"splash": { // 启动画面配置
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": { // 更新配置
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [ // 资产文件打包模式
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.myapp", // iOS 应用唯一Bundle ID
"buildNumber": "1.0.0" // iOS 构建版本号
},
"android": {
"adaptiveIcon": { // Android 自适应图标
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
},
"package": "com.yourcompany.myapp", // Android 应用唯一包名
"versionCode": 1 // Android 版本代码,每次发布递增
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [ // 如果使用Bare Workflow或需要原生功能
"expo-camera"
]
}
}
app.json 中设置 android.package 和 android.versionCode。eas build --platform android 生成 App Bundle。app.json 中设置 ios.bundleIdentifier 和 ios.buildNumber。eas build --platform ios 生成 iOS 应用文件。src/
├── api/ # API 请求相关服务
├── components/ # 可复用UI组件 (原子组件)
├── screens/ # 页面组件 (组织大型UI)
├── navigation/ # 导航配置 (React Navigation)
├── services/ # 业务逻辑服务 (如认证、数据库)
├── utils/ # 通用工具函数
├── hooks/ # 自定义React Hooks
├── context/ # React Context API (全局状态管理)
├── assets/ # 静态资源 (图片、字体)
├── config/ # 配置文件 (如Firebase配置)
└── theme/ # 主题配置 (颜色、字体、间距)
建议使用 StyleSheet.create 结合主题系统。
import { StyleSheet } from 'react-native';
// theme.js
export const theme = {
colors: {
primary: '#3498db',
secondary: '#2ecc71',
text: '#2c3e50',
background: '#f5f5f5',
danger: '#e74c3c'
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32
},
fontSize: {
sm: 14,
md: 16,
lg: 18,
xl: 24
}
};
// MyComponent.js
import { theme } from '../theme';
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
padding: theme.spacing.md
},
title: {
fontSize: theme.fontSize.xl,
color: theme.colors.primary,
marginBottom: theme.spacing.lg
}
});
FlatList / SectionList 代替 ScrollView 处理大量数据,并合理设置 windowSize, maxToRenderPerBatch, removeClippedSubviews。React.memo, useMemo, useCallback 缓存组件、计算结果和回调函数。expo-image 库的缓存能力。react-native-reanimated 进行原生驱动的动画,避免在 JS 线程执行动画逻辑。render 方法中执行复杂计算或创建新对象。InteractionManager.runAfterInteractions 处理非关键的后台任务,确保UI流畅。import React, { useMemo, useCallback } from 'react';
import { FlatList, Text, View, TouchableOpacity, StyleSheet } from 'react-native';
// 假设的 UserItem 组件
const UserItem = React.memo(({ user, onPress }) => (
<TouchableOpacity onPress={onPress} style={itemStyles.container}>
<Text style={itemStyles.text}>{user.name} ({user.email})</Text>
</TouchableOpacity>
));
const ITEM_HEIGHT = 60; // 每个列表项的固定高度
const UserList = ({ users, onUserPress }) => {
// 使用 useMemo 缓存排序后的用户列表,只有当 users 数组变化时才重新计算
const sortedUsers = useMemo(() => {
console.log('Sorting users...');
return users.sort((a, b) => a.name.localeCompare(b.name));
}, [users]);
// 使用 useCallback 缓存 renderItem 回调函数,避免每次渲染都创建新的函数实例
const renderItem = useCallback(({ item }) => (
<UserItem user={item} onPress={() => onUserPress(item)} />
), [onUserPress]); // 依赖 onUserPress 函数
return (
<FlatList
data={sortedUsers}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()} // 确保key是字符串
initialNumToRender={10} // 首次渲染的项数
maxToRenderPerBatch={5} // 每次批量渲染的项数
windowSize={21} // 渲染窗口大小
removeClippedSubviews={true} // 移除屏幕外组件以节省内存 (需谨慎使用)
getItemLayout={(data, index) => ({ // 优化滚动性能,如果列表项高度固定
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
);
};
const itemStyles = StyleSheet.create({
container: {
padding: 15,
borderBottomWidth: 1,
borderBottomColor: '#eee',
height: ITEM_HEIGHT, // 固定高度
justifyContent: 'center',
},
text: {
fontSize: 16,
color: '#333',
},
});
在生产环境中捕获错误,提供更好的用户体验。
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
// 错误回退组件
const ErrorFallback = ({ error }) => (
<View style={fallbackStyles.container}>
<Text style={fallbackStyles.title}>出现问题!</Text>
<Text style={fallbackStyles.text}>{error ? error.message : '未知错误'}</Text>
<Text style={fallbackStyles.subText}>请稍后再试或联系支持。</Text>
</View>
);
// 错误边界组件 (适用于Class Component)
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
// 当子组件抛出错误时,更新状态
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// 捕获错误信息,可用于发送错误报告
componentDidCatch(error, errorInfo) {
console.error('应用错误:', error, errorInfo);
// 可以在此处发送错误报告到Sentry、Bugsnag等服务
// logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 渲染自定义的回退 UI
return <ErrorFallback error={this.state.error} />;
}
return this.props.children; // 正常渲染子组件
}
}
const fallbackStyles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#ffebee', // 浅红色背景
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#c62828', // 深红色
marginBottom: 10,
},
text: {
fontSize: 16,
color: '#d32f2f',
textAlign: 'center',
marginBottom: 5,
},
subText: {
fontSize: 14,
color: '#e57373',
textAlign: 'center',
},
});
export default ErrorBoundary;
// 在App.js中使用
// import ErrorBoundary from './ErrorBoundary';
// <ErrorBoundary>
// <AppNavigator />
// </ErrorBoundary>
单元测试和集成测试是保证应用质量的关键。
测试独立的函数、组件逻辑。使用 Jest 和 React Native Testing Library。
import { render, fireEvent, screen } from '@testing-library/react-native';
import React, { useState } from 'react';
import { Button, Text, View } from 'react-native';
// 假设我们测试的计数器组件
const Counter = () => {
const [count, setCount] = useState(0);
return (
<View>
<Text testID="count-display">{count}</Text>
<Button title="Increment" onPress={() => setCount(count + 1)} />
<Button title="Decrement" onPress={() => setCount(count - 1)} />
</View>
);
};
test('计数器增加功能正常工作', () => {
render(<Counter />);
const countDisplay = screen.getByTestId('count-display');
const incrementButton = screen.getByText('Increment');
// 初始状态为 0
expect(countDisplay.props.children).toBe(0);
// 点击增加按钮
fireEvent.press(incrementButton);
expect(countDisplay.props.children).toBe(1);
fireEvent.press(incrementButton);
expect(countDisplay.props.children).toBe(2);
});
test('计数器减少功能正常工作', () => {
render(<Counter />);
const countDisplay = screen.getByTestId('count-display');
const incrementButton = screen.getByText('Increment');
const decrementButton = screen.getByText('Decrement');
// 先增加两次
fireEvent.press(incrementButton);
fireEvent.press(incrementButton);
expect(countDisplay.props.children).toBe(2);
// 点击减少按钮
fireEvent.press(decrementButton);
expect(countDisplay.props.children).toBe(1);
fireEvent.press(decrementButton);
expect(countDisplay.props.children).toBe(0);
});
测试多个组件协同工作,模拟用户交互流程。
import { render, fireEvent, screen, waitFor } from '@testing-library/react-native';
import React, { useState } from 'react';
import { TextInput, Button, View, Text, FlatList } from 'react-native';
// 模拟的 TodoApp,简化自案例2
const TodoApp = () => {
const [todos, setTodos] = useState([]);
const [text, setText] = useState('');
const addTodo = () => {
if (text.trim()) {
setTodos([{ id: Date.now().toString(), text: text, completed: false }, ...todos]);
setText('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo));
};
return (
<View>
<TextInput
testID="todo-input"
placeholder="输入待办事项..."
value={text}
onChangeText={setText}
/>
<Button title="添加" onPress={addTodo} />
<FlatList
data={todos}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => toggleTodo(item.id)}>
<Text testID={`todo-item-${item.id}`} style={{ textDecorationLine: item.completed ? 'line-through' : 'none' }}>
{item.text}
</Text>
</TouchableOpacity>
)}
/>
</View>
);
};
test('添加和切换待办事项的功能正常工作', async () => {
render(<TodoApp />);
const input = screen.getByTestId('todo-input');
const addButton = screen.getByText('添加');
// 添加第一个待办事项
fireEvent.changeText(input, '学习React Native');
fireEvent.press(addButton);
await waitFor(() => expect(screen.getByText('学习React Native')).toBeTruthy());
const todo1 = screen.getByTestId(/todo-item-.*/); // 匹配任意id的todo项
// 添加第二个待办事项
fireEvent.changeText(input, '完成项目');
fireEvent.press(addButton);
await waitFor(() => expect(screen.getByText('完成项目')).toBeTruthy());
const todo2 = screen.getByText('完成项目');
// 验证第一个待办事项未完成
expect(todo1.props.style.textDecorationLine).toBe('none');
// 切换第一个待办事项为完成状态
fireEvent.press(todo1);
await waitFor(() => expect(todo1.props.style.textDecorationLine).toBe('line-through'));
// 验证第二个待办事项仍未完成
expect(todo2.props.style.textDecorationLine).toBe('none');
});
您已经完成了一份完整的React Native Expo开发教程学习。通过4个实战案例的练习,您应该已经掌握了:
技术学习是一个持续的过程。建议您在掌握基础后,多参与实际项目开发,关注社区动态,不断提升自己的技能水平。记住,最好的学习方法就是动手实践!