React Native外卖应用:订餐和配送系统实现
引言:外卖应用开发的痛点与解决方案
你是否曾为构建流畅的外卖应用而烦恼?用户等待时间长、订单状态同步延迟、地图定位不准等问题是否让你头疼?本文将带你从零开始,使用React Native构建一个高效、稳定的外卖订餐和配送系统,解决这些核心痛点。
读完本文,你将能够:
- 掌握React Native核心组件在实际项目中的应用
- 实现用户认证与订单管理功能
- 集成地图服务与实时配送跟踪
- 优化应用性能,提升用户体验
- 解决外卖应用开发中的常见问题
1. 项目架构设计
1.1 系统整体架构
外卖应用通常包含用户端、商家端和配送端三个主要部分。本文将重点介绍用户端的实现,包括用户注册登录、商品浏览、下单支付、订单跟踪等功能。
1.2 技术栈选择
本项目将使用以下技术栈:
| 技术 | 说明 |
|---|---|
| React Native | 跨平台移动应用开发框架 |
| Redux | 状态管理库 |
| React Navigation | 路由管理库 |
| Axios | HTTP请求库 |
| AsyncStorage | 本地数据存储 |
| react-native-maps | 地图组件 |
| react-native-svg | SVG图标支持 |
| Formik + Yup | 表单处理与验证 |
1.3 项目目录结构
外卖应用/
├── src/
│ ├── api/ # API请求
│ ├── assets/ # 静态资源
│ ├── components/ # 自定义组件
│ ├── constants/ # 常量定义
│ ├── hooks/ # 自定义钩子
│ ├── navigation/ # 路由配置
│ ├── screens/ # 屏幕组件
│ │ ├── Auth/ # 认证相关
│ │ ├── Home/ # 首页相关
│ │ ├── Shop/ # 商家相关
│ │ ├── Cart/ # 购物车
│ │ ├── Order/ # 订单相关
│ │ └── Profile/ # 个人中心
│ ├── store/ # Redux状态管理
│ ├── styles/ # 样式定义
│ ├── types/ # TypeScript类型定义
│ └── utils/ # 工具函数
├── App.tsx # 应用入口
└── index.js # 程序入口
2. 环境搭建与项目初始化
2.1 开发环境配置
首先,确保你的开发环境已经配置好React Native开发所需的所有依赖。如果还没有配置,可以按照以下步骤进行:
# 安装React Native CLI
npm install -g react-native-cli
# 创建新项目
react-native init FoodDeliveryApp
# 进入项目目录
cd FoodDeliveryApp
# 安装依赖
npm install redux react-redux redux-thunk axios react-navigation react-native-maps formik yup
2.2 项目初始化
修改App.tsx文件,设置应用的基本结构:
import React from 'react';
import { SafeAreaView, StatusBar, StyleSheet, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { Provider } from 'react-redux';
import store from './src/store';
import MainNavigator from './src/navigation/MainNavigator';
const App = () => {
return (
<Provider store={store}>
<NavigationContainer>
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
<MainNavigator />
</SafeAreaView>
</NavigationContainer>
</Provider>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default App;
3. 核心功能实现
3.1 用户认证模块
用户认证是外卖应用的基础功能,包括注册、登录、密码找回等。我们将使用Formik和Yup进行表单处理和验证。
// src/screens/Auth/LoginScreen.tsx
import React from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import { login } from '../../store/actions/authActions';
const LoginSchema = Yup.object().shape({
phone: Yup.string()
.required('手机号不能为空')
.matches(/^1[3-9]\d{9}$/, '请输入正确的手机号'),
password: Yup.string()
.required('密码不能为空')
.min(6, '密码至少6位'),
});
const LoginScreen = ({ navigation }) => {
const dispatch = useDispatch();
const handleLogin = async (values) => {
try {
await dispatch(login(values.phone, values.password));
navigation.navigate('Main');
} catch (error) {
Alert.alert('登录失败', error.message);
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>欢迎回来</Text>
<Formik
initialValues={{ phone: '', password: '' }}
validationSchema={LoginSchema}
onSubmit={handleLogin}
>
{({ handleChange, handleBlur, handleSubmit, values, errors, touched }) => (
<View>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="请输入手机号"
keyboardType="phone-pad"
onChangeText={handleChange('phone')}
onBlur={handleBlur('phone')}
value={values.phone}
/>
{touched.phone && errors.phone && (
<Text style={styles.errorText}>{errors.phone}</Text>
)}
</View>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="请输入密码"
secureTextEntry
onChangeText={handleChange('password')}
onBlur={handleBlur('password')}
value={values.password}
/>
{touched.password && errors.password && (
<Text style={styles.errorText}>{errors.password}</Text>
)}
</View>
<TouchableOpacity
style={styles.loginButton}
onPress={handleSubmit}
>
<Text style={styles.loginButtonText}>登录</Text>
</TouchableOpacity>
<View style={styles.footer}>
<TouchableOpacity onPress={() => navigation.navigate('ForgotPassword')}>
<Text style={styles.forgotPassword}>忘记密码?</Text>
</TouchableOpacity>
<View style={styles.registerContainer}>
<Text style={styles.registerText}>还没有账号?</Text>
<TouchableOpacity onPress={() => navigation.navigate('Register')}>
<Text style={styles.registerButton}>立即注册</Text>
</TouchableOpacity>
</View>
</View>
</View>
)}
</Formik>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 40,
textAlign: 'center',
},
inputContainer: {
marginBottom: 20,
},
input: {
height: 50,
borderColor: '#ddd',
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 15,
fontSize: 16,
},
errorText: {
color: 'red',
fontSize: 12,
marginTop: 5,
marginLeft: 5,
},
loginButton: {
backgroundColor: '#FF4D4F',
height: 50,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
},
loginButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
footer: {
alignItems: 'center',
},
forgotPassword: {
color: '#1890FF',
marginBottom: 20,
},
registerContainer: {
flexDirection: 'row',
alignItems: 'center',
},
registerText: {
color: '#666',
fontSize: 14,
},
registerButton: {
color: '#FF4D4F',
fontSize: 14,
marginLeft: 5,
fontWeight: 'bold',
},
});
export default LoginScreen;
3.2 商品列表与详情
外卖应用的核心是商品展示和选择。我们将使用FlatList组件来展示商品列表,并实现下拉刷新和上拉加载更多功能。
// src/screens/Home/ShopDetailScreen.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, Image, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
import { useDispatch } from 'react-redux';
import { addToCart } from '../../store/actions/cartActions';
import { useNavigation } from '@react-navigation/native';
const ShopDetailScreen = ({ route }) => {
const { shop } = route.params;
const [goods, setGoods] = useState([]);
const [categories, setCategories] = useState([]);
const [activeCategory, setActiveCategory] = useState('');
const [loading, setLoading] = useState(true);
const dispatch = useDispatch();
const navigation = useNavigation();
useEffect(() => {
// 获取商品数据
fetchGoodsData();
// 设置导航栏标题
navigation.setOptions({ title: shop.name });
}, []);
const fetchGoodsData = async () => {
try {
setLoading(true);
// 模拟API请求
setTimeout(() => {
// 假设从API获取商品数据
const mockCategories = [
{ id: 'all', name: '全部' },
{ id: 'recommended', name: '推荐' },
{ id: 'food', name: '美食' },
{ id: 'drink', name: '饮品' },
{ id: 'dessert', name: '甜点' },
];
const mockGoods = [
{
id: '1',
name: '招牌汉堡',
price: 28,
originalPrice: 35,
image: 'https://example.com/hamburger.jpg',
category: 'food',
sales: 120,
description: '精选牛肉,新鲜蔬菜,特制酱料'
},
{
id: '2',
name: '可乐',
price: 5,
originalPrice: 6,
image: 'https://example.com/cola.jpg',
category: 'drink',
sales: 300,
description: '冰爽可口'
},
// 更多商品...
];
setCategories(mockCategories);
setGoods(mockGoods);
setActiveCategory(mockCategories[0].id);
setLoading(false);
}, 1000);
} catch (error) {
console.error('获取商品数据失败:', error);
setLoading(false);
}
};
const filteredGoods = activeCategory === 'all'
? goods
: goods.filter(item => item.category === activeCategory);
const renderCategoryItem = ({ item }) => (
<TouchableOpacity
style={[
styles.categoryItem,
activeCategory === item.id ? styles.activeCategory : null
]}
onPress={() => setActiveCategory(item.id)}
>
<Text
style={[
styles.categoryText,
activeCategory === item.id ? styles.activeCategoryText : null
]}
>
{item.name}
</Text>
</TouchableOpacity>
);
const renderGoodsItem = ({ item }) => (
<View style={styles.goodsItem}>
<Image source={{ uri: item.image }} style={styles.goodsImage} />
<View style={styles.goodsInfo}>
<Text style={styles.goodsName}>{item.name}</Text>
<Text style={styles.goodsDescription} numberOfLines={1}>{item.description}</Text>
<View style={styles.goodsPriceContainer}>
<Text style={styles.goodsPrice}>¥{item.price.toFixed(2)}</Text>
{item.originalPrice > item.price && (
<Text style={styles.goodsOriginalPrice}>¥{item.originalPrice.toFixed(2)}</Text>
)}
{item.sales > 0 && (
<Text style={styles.goodsSales}>{item.sales}人已买</Text>
)}
</View>
</View>
<TouchableOpacity
style={styles.addButton}
onPress={() => dispatch(addToCart(item, 1))}
>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View>
);
return (
<View style={styles.container}>
{/* 店铺头部信息 */}
<View style={styles.shopHeader}>
<Image source={{ uri: shop.image }} style={styles.shopImage} />
<View style={styles.shopInfo}>
<Text style={styles.shopName}>{shop.name}</Text>
<View style={styles.shopTags}>
<Text style={styles.tag}>外卖</Text>
<Text style={styles.tag}>到店</Text>
</View>
<View style={styles.shopStats}>
<Text style={styles.statItem}>月售{shop.monthSales}</Text>
<Text style={styles.statItem}>配送费¥{shop.deliveryFee}</Text>
<Text style={styles.statItem}>配送{shop.deliveryTime}</Text>
</View>
</View>
</View>
{/* 分类导航 */}
<FlatList
data={categories}
renderItem={renderCategoryItem}
keyExtractor={item => item.id}
horizontal
showsHorizontalScrollIndicator={false}
style={styles.categoryList}
/>
{/* 商品列表 */}
<FlatList
data={filteredGoods}
renderItem={renderGoodsItem}
keyExtractor={item => item.id}
contentContainerStyle={styles.goodsList}
ListHeaderComponent={activeCategory !== 'all' ? (
<Text style={styles.categoryTitle}>
{categories.find(c => c.id === activeCategory)?.name}
</Text>
) : null}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
shopHeader: {
backgroundColor: 'white',
padding: 15,
flexDirection: 'row',
},
shopImage: {
width: 80,
height: 80,
borderRadius: 8,
marginRight: 10,
},
shopInfo: {
flex: 1,
},
shopName: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 5,
},
shopTags: {
flexDirection: 'row',
marginBottom: 5,
},
tag: {
backgroundColor: '#f0f0f0',
paddingHorizontal: 5,
paddingVertical: 2,
borderRadius: 3,
fontSize: 12,
marginRight: 5,
},
shopStats: {
flexDirection: 'row',
flexWrap: 'wrap',
},
statItem: {
fontSize: 12,
color: '#666',
marginRight: 10,
},
categoryList: {
backgroundColor: 'white',
paddingVertical: 10,
},
categoryItem: {
paddingHorizontal: 15,
paddingVertical: 5,
marginHorizontal: 5,
borderRadius: 20,
},
activeCategory: {
backgroundColor: '#FFF0F0',
},
categoryText: {
fontSize: 14,
},
activeCategoryText: {
color: '#FF4D4F',
fontWeight: 'bold',
},
goodsList: {
padding: 10,
},
goodsItem: {
backgroundColor: 'white',
borderRadius: 10,
padding: 10,
marginBottom: 10,
flexDirection: 'row',
alignItems: 'center',
},
goodsImage: {
width: 80,
height: 80,
borderRadius: 8,
},
goodsInfo: {
flex: 1,
marginLeft: 10,
},
goodsName: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 5,
},
goodsDescription: {
fontSize: 12,
color: '#666',
marginBottom: 5,
},
goodsPriceContainer: {
flexDirection: 'row',
alignItems: 'center',
},
goodsPrice: {
fontSize: 16,
color: '#FF4D4F',
fontWeight: 'bold',
},
goodsOriginalPrice: {
fontSize: 12,
color: '#999',
textDecorationLine: 'line-through',
marginLeft: 5,
},
goodsSales: {
fontSize: 12,
color: '#999',
marginLeft: 10,
},
addButton: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#FF4D4F',
justifyContent: 'center',
alignItems: 'center',
},
addButtonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
});
export default ShopDetailScreen;
3.3 购物车功能
购物车是外卖应用的核心功能之一,用于临时存储用户选择的商品,计算总价,并支持修改商品数量、删除商品等操作。
// src/components/Cart/CartItem.tsx
import React from 'react';
import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';
import { useDispatch } from 'react-redux';
import { addToCart, removeFromCart } from '../../store/actions/cartActions';
const CartItem = ({ item }) => {
const dispatch = useDispatch();
const handleIncrease = () => {
dispatch(addToCart(item, 1));
};
const handleDecrease = () => {
if (item.quantity > 1) {
dispatch(addToCart(item, -1));
} else {
dispatch(removeFromCart(item.id));
}
};
return (
<View style={styles.container}>
<Image source={{ uri: item.image }} style={styles.image} />
<View style={styles.infoContainer}>
<Text style={styles.name}>{item.name}</Text>
<Text style={styles.price}>¥{item.price.toFixed(2)}</Text>
</View>
<View style={styles.quantityContainer}>
<TouchableOpacity onPress={handleDecrease} style={styles.quantityButton}>
<Text style={styles.quantityButtonText}>-</Text>
</TouchableOpacity>
<Text style={styles.quantity}>{item.quantity}</Text>
<TouchableOpacity onPress={handleIncrease} style={styles.quantityButton}>
<Text style={styles.quantityButtonText}>+</Text>
</TouchableOpacity>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
image: {
width: 60,
height: 60,
borderRadius: 8,
},
infoContainer: {
flex: 1,
marginLeft: 10,
},
name: {
fontSize: 16,
marginBottom: 5,
},
price: {
fontSize: 14,
color: '#FF4D4F',
fontWeight: 'bold',
},
quantityContainer: {
flexDirection: 'row',
alignItems: 'center',
},
quantityButton: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#f5f5f5',
justifyContent: 'center',
alignItems: 'center',
},
quantityButtonText: {
fontSize: 16,
color: '#333',
},
quantity: {
marginHorizontal: 15,
fontSize: 16,
fontWeight: 'bold',
},
});
export default CartItem;
3.4 订单跟踪功能
订单跟踪功能允许用户实时查看订单状态和配送进度。我们将使用react-native-maps组件来显示配送员位置和配送路线。
// src/screens/Order/OrderTrackingScreen.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Image, TouchableOpacity, Dimensions } from 'react-native';
import MapView, { Marker, Polyline } from 'react-native-maps';
import { useNavigation } from '@react-navigation/native';
const { width, height } = Dimensions.get('window');
const OrderTrackingScreen = ({ route }) => {
const { order } = route.params;
const [orderStatus, setOrderStatus] = useState('delivering');
const [courierLocation, setCourierLocation] = useState({
latitude: 39.908823,
longitude: 116.397470,
});
const [restaurantLocation, setRestaurantLocation] = useState({
latitude: 39.908823,
longitude: 116.397470,
});
const [userLocation, setUserLocation] = useState({
latitude: 39.915823,
longitude: 116.407470,
});
const [polylineCoordinates, setPolylineCoordinates] = useState([
restaurantLocation,
{ latitude: 39.910823, longitude: 116.399470 },
{ latitude: 39.912823, longitude: 116.402470 },
{ latitude: 39.914823, longitude: 116.405470 },
userLocation,
]);
const [estimatedTime, setEstimatedTime] = useState(15); // 预计送达时间(分钟)
const navigation = useNavigation();
useEffect(() => {
// 模拟配送员位置更新
const interval = setInterval(() => {
updateCourierLocation();
// 更新预计送达时间
if (estimatedTime > 0) {
setEstimatedTime(prev => prev - 1);
}
}, 30000); // 每30秒更新一次
return () => clearInterval(interval);
}, [estimatedTime]);
const updateCourierLocation = () => {
// 模拟配送员移动
const stepIndex = Math.max(0, Math.min(
Math.floor((15 - estimatedTime) / 3),
polylineCoordinates.length - 1
));
setCourierLocation({
latitude: polylineCoordinates[stepIndex].latitude,
longitude: polylineCoordinates[stepIndex].longitude,
});
// 当配送员到达时,更新订单状态
if (estimatedTime <= 0 && orderStatus === 'delivering') {
setOrderStatus('arrived');
}
};
const getStatusText = () => {
switch (orderStatus) {
case 'confirmed':
return '商家已确认订单';
case 'preparing':
return '商家正在备餐';
case 'delivering':
return '配送中';
case 'arrived':
return '已送达';
case 'completed':
return '订单完成';
default:
return '订单处理中';
}
};
return (
<View style={styles.container}>
{/* 地图视图 */}
<MapView
style={styles.map}
initialRegion={{
latitude: (restaurantLocation.latitude + userLocation.latitude) / 2,
longitude: (restaurantLocation.longitude + userLocation.longitude) / 2,
latitudeDelta: 0.02,
longitudeDelta: 0.02,
}}
showsUserLocation={false}
>
{/* 配送路线 */}
<Polyline
coordinates={polylineCoordinates}
strokeColor="#FF4D4F"
strokeWidth={4}
/>
{/* 商家标记 */}
<Marker coordinate={restaurantLocation}>
<Image
source={require('../../assets/images/restaurant-marker.png')}
style={styles.markerImage}
/>
</Marker>
{/* 用户标记 */}
<Marker coordinate={userLocation}>
<Image
source={require('../../assets/images/user-marker.png')}
style={styles.markerImage}
/>
</Marker>
{/* 配送员标记 */}
<Marker coordinate={courierLocation}>
<Image
source={require('../../assets/images/courier-marker.png')}
style={styles.courierMarkerImage}
/>
</Marker>
</MapView>
{/* 订单信息卡片 */}
<View style={styles.infoCard}>
<Text style={styles.orderStatus}>{getStatusText()}</Text>
{orderStatus === 'delivering' && (
<View style={styles.deliveryInfo}>
<Text style={styles.estimatedTime}>预计送达时间: {estimatedTime}分钟</Text>
<View style={styles.courierInfo}>
<Image source={{ uri: order.courier.avatar }} style={styles.courierAvatar} />
<View style={styles.courierDetails}>
<Text style={styles.courierName}>{order.courier.name}</Text>
<Text style={styles.courierId}>配送员 #{order.courier.id}</Text>
</View>
<TouchableOpacity style={styles.contactButton}>
<Text style={styles.contactButtonText}>联系骑手</Text>
</TouchableOpacity>
</View>
</View>
)}
{orderStatus === 'arrived' && (
<View style={styles.arrivedSection}>
<Text style={styles.arrivedText}>您的外卖已送达楼下,请及时取餐</Text>
<TouchableOpacity
style={styles.confirmButton}
onPress={() => setOrderStatus('completed')}
>
<Text style={styles.confirmButtonText}>确认收货</Text>
</TouchableOpacity>
</View>
)}
{orderStatus === 'completed' && (
<View style={styles.completedSection}>
<Text style={styles.completedText}>订单已完成,感谢您的使用</Text>
<TouchableOpacity
style={styles.rateButton}
onPress={() => navigation.navigate('OrderRating', { order })}
>
<Text style={styles.rateButtonText}>评价订单</Text>
</TouchableOpacity>
</View>
)}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
map: {
width: width,
height: height * 0.7,
},
infoCard: {
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 20,
height: height * 0.3,
justifyContent: 'space-between',
},
orderStatus: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 15,
textAlign: 'center',
},
deliveryInfo: {
marginBottom: 15,
},
estimatedTime: {
fontSize: 16,
marginBottom: 15,
color: '#666',
},
courierInfo: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 20,
},
courierAvatar: {
width: 50,
height: 50,
borderRadius: 25,
},
courierDetails: {
marginLeft: 15,
flex: 1,
},
courierName: {
fontSize: 16,
fontWeight: 'bold',
},
courierId: {
fontSize: 12,
color: '#666',
},
contactButton: {
backgroundColor: '#FF4D4F',
paddingVertical: 8,
paddingHorizontal: 15,
borderRadius: 20,
},
contactButtonText: {
color: 'white',
fontSize: 14,
},
arrivedSection: {
alignItems: 'center',
marginTop: 20,
},
arrivedText: {
fontSize: 16,
marginBottom: 20,
textAlign: 'center',
},
confirmButton: {
backgroundColor: '#FF4D4F',
paddingVertical: 12,
borderRadius: 8,
width: '100%',
alignItems: 'center',
},
confirmButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
completedSection: {
alignItems: 'center',
marginTop: 20,
},
completedText: {
fontSize: 16,
marginBottom: 20,
textAlign: 'center',
},
rateButton: {
backgroundColor: '#1890FF',
paddingVertical: 12,
borderRadius: 8,
width: '100%',
alignItems: 'center',
},
rateButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
markerImage: {
width: 30,
height: 30,
},
courierMarkerImage: {
width: 40,
height: 40,
},
});
export default OrderTrackingScreen;
4. 性能优化
4.1 列表优化
外卖应用中会有大量的商品列表、订单列表等,使用FlatList的优化特性可以显著提升性能。
// 优化的FlatList使用示例
<FlatList
data={goods}
renderItem={renderGoodsItem}
keyExtractor={item => item.id}
getItemLayout={(data, index) => ({
length: 100, // 每项高度
offset: 100 * index,
index,
})}
initialNumToRender={10} // 初始渲染数量
maxToRenderPerBatch={10} // 每批次渲染数量
windowSize={5} // 可见区域外预渲染数量
removeClippedSubviews={true} // 移除屏幕外视图
ListFooterComponent={loading ? renderFooter : null} // 加载更多指示器
onEndReached={loadMoreData} // 滚动到底部加载更多
onEndReachedThreshold={0.5} // 距离底部多远触发加载更多
refreshing={refreshing} // 下拉刷新状态
onRefresh={refreshData} // 下拉刷新
/>
4.2 图片优化
图片是外卖应用中占用资源较多的部分,优化图片加载可以提升应用性能和用户体验。
// 使用缓存和优化的图片组件
import React from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import FastImage from 'react-native-fast-image';
const OptimizedImage = ({ source, style, resizeMode = FastImage.resizeMode.cover }) => {
return (
<FastImage
source={{
uri: source.uri,
priority: FastImage.priority.normal,
cachePolicy: FastImage.cacheControl.immutable,
}}
style={style}
resizeMode={resizeMode}
onLoadStart={() => console.log('Image loading started')}
onLoadEnd={() => console.log('Image loading finished')}
fallbackSource={require('../assets/images/placeholder.png')}
/>
);
};
export default OptimizedImage;
4.3 状态管理优化
合理设计Redux状态结构,避免不必要的重渲染。
// 使用reselect优化选择器
import { createSelector } from 'reselect';
// 基础选择器
const selectCart = state => state.cart;
// 创建记忆化选择器
export const selectCartItems = createSelector(
[selectCart],
cart => cart.items
);
export const selectCartItemsCount = createSelector(
[selectCartItems],
items => items.reduce((total, item) => total + item.quantity, 0)
);
export const selectCartTotal = createSelector(
[selectCartItems],
items => items.reduce((total, item) => total + (item.price * item.quantity), 0)
);
5. 常见问题解决方案
5.1 表单处理与验证
使用Formik和Yup处理复杂表单,实现表单验证和错误提示。
5.2 网络请求处理
封装Axios请求,处理请求拦截、响应拦截、错误处理等。
// src/api/request.js
import axios from 'axios';
import { Alert } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
// 创建axios实例
const request = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
request.interceptors.request.use(
async (config) => {
// 获取token并添加到请求头
const token = await AsyncStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
// 错误处理
if (error.response) {
// 请求已发出,服务器返回状态码不在2xx范围
const { status, data } = error.response;
// 未授权,需要重新登录
if (status === 401) {
AsyncStorage.removeItem('token');
// 导航到登录页面
navigation.navigate('Login');
Alert.alert('登录已过期,请重新登录');
}
// 服务器返回错误信息
else if (data && data.message) {
Alert.alert('请求失败', data.message);
} else {
Alert.alert('请求失败', `错误码: ${status}`);
}
} else if (error.request) {
// 请求已发出,但没有收到响应
Alert.alert('网络错误', '无法连接到服务器,请检查网络');
} else {
// 请求配置出错
Alert.alert('请求错误', '请求发生错误,请稍后重试');
}
return Promise.reject(error);
}
);
export default request;
5.3 本地数据持久化
使用AsyncStorage存储用户信息、购物车数据等。
// src/utils/storage.js
import AsyncStorage from '@react-native-async-storage/async-storage';
// 存储数据
export const setStorageItem = async (key, value) => {
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(key, jsonValue);
return true;
} catch (e) {
console.error('存储数据失败:', e);
return false;
}
};
// 获取数据
export const getStorageItem = async (key) => {
try {
const jsonValue = await AsyncStorage.getItem(key);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (e) {
console.error('获取数据失败:', e);
return null;
}
};
// 删除数据
export const removeStorageItem = async (key) => {
try {
await AsyncStorage.removeItem(key);
return true;
} catch (e) {
console.error('删除数据失败:', e);
return false;
}
};
// 清空所有数据
export const clearStorage = async () => {
try {
await AsyncStorage.clear();
return true;
} catch (e) {
console.error('清空数据失败:', e);
return false;
}
};
6. 总结与展望
6.1 项目总结
本文详细介绍了使用React Native开发外卖应用的全过程,包括项目架构设计、核心功能实现、性能优化等方面。通过学习本文,你应该能够掌握:
- React Native核心组件(如FlatList, ScrollView, Image等)的实际应用
- Redux状态管理在复杂应用中的使用
- 表单处理与验证
- 网络请求与错误处理
- 地图服务集成与实时位置跟踪
- 应用性能优化技巧
6.2 后续优化方向
外卖应用还有很大的优化空间,未来可以考虑以下方向:
- 引入TypeScript,提升代码质量和开发效率
- 实现离线功能,支持无网络环境下浏览历史订单
- 集成推送通知,及时通知用户订单状态变化
- 优化首屏加载速度,提升用户体验
- 添加单元测试和集成测试,提高代码可靠性
6.3 学习资源推荐
| 资源 | 说明 |
|---|---|
| React Native官方文档 | 最权威的学习资源,包含详细的API说明和示例 |
| React Native社区 | 可以找到大量开源组件和解决方案 |
| Redux官方文档 | 学习状态管理的最佳实践 |
| React Navigation文档 | 掌握React Native路由管理 |
| 《React Native实战》 | 实用的React Native开发指南 |
结语
外卖应用开发涉及多个方面的知识和技术,希望本文能够为你提供一个全面的指导。React Native作为跨平台开发框架,能够帮助开发者高效地构建出高质量的移动应用。通过不断学习和实践,你可以进一步提升自己的开发技能,构建出更加优秀的应用。
如果你觉得本文对你有帮助,请点赞、收藏并关注我的后续文章。如果你有任何问题或建议,欢迎在评论区留言讨论。
祝你的外卖应用开发顺利!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



