
最近在开发个人项目时经常需要为美食类应用设计数据结构和交互逻辑。今天想和大家分享一个完整的美食日记应用开发实战从需求分析到代码实现带你一步步构建一个功能完整的美食记录系统。无论你是刚入门移动开发的新手还是想学习完整项目架构的进阶开发者这篇文章都能为你提供实用的开发思路和可复用的代码模板。我们将使用现代前端技术栈重点讲解核心功能实现和常见坑点解决方案。1. 项目需求分析与技术选型1.1 核心功能规划美食日记应用需要满足用户记录、查看、管理美食体验的基本需求。经过分析我们确定以下核心功能模块美食记录支持文字描述、评分、照片上传、地理位置标记分类管理按菜系、口味、场合等维度分类美食记录搜索筛选基于关键词、评分、时间范围等多条件搜索数据统计生成美食偏好分析、消费趋势等可视化报表社交分享选择性分享美食记录到社交平台1.2 技术架构设计考虑到跨平台需求和开发效率我们选择React Native Node.js的技术组合前端框架React Native 0.72支持iOS和Android双平台状态管理Redux Toolkit简化状态管理复杂度本地存储AsyncStorage SQLite兼顾简单数据和复杂查询图片处理react-native-image-picker 图片压缩优化地图集成react-native-maps实现地理位置标记功能后端服务Node.js Express MongoDB提供数据同步能力1.3 开发环境准备确保你的开发环境满足以下要求Node.js 16.0 版本React Native CLI 环境配置完成iOS开发需要Xcode 14.0Android开发需要Android Studio 2022推荐使用VS Code作为代码编辑器2. 项目结构与基础配置2.1 初始化项目结构使用React Native CLI创建新项目npx react-native init MollyFoodDiary --template react-native-template-typescript cd MollyFoodDiary项目目录结构规划如下MollyFoodDiary/ ├── src/ │ ├── components/ # 可复用组件 │ ├── screens/ # 页面组件 │ ├── store/ # 状态管理 │ ├── services/ # API服务层 │ ├── utils/ # 工具函数 │ ├── types/ # TypeScript类型定义 │ └── assets/ # 静态资源 ├── android/ # Android原生代码 ├── ios/ # iOS原生代码 └── __tests__/ # 测试文件2.2 核心依赖配置安装项目所需的关键依赖包# 状态管理 npm install reduxjs/toolkit react-redux # 导航路由 npm install react-navigation/native react-navigation/stack npm install react-native-screens react-native-safe-area-context # 本地存储 npm install react-native-async-storage/async-storage npm install react-native-sqlite-storage # 图片选择与处理 npm install react-native-image-picker npm install react-native-compressor # 地图功能 npm install react-native-maps # 开发工具 npm install --save-dev types/react-native types/react更新package.json中的依赖配置确保版本兼容性{ dependencies: { react: 18.2.0, react-native: 0.72.6, reduxjs/toolkit: ^1.9.7, react-redux: ^8.1.3, react-navigation/native: ^6.1.9, react-native-sqlite-storage: ^6.0.1 } }2.3 TypeScript类型定义创建核心数据类型的TypeScript定义文件// src/types/food.ts export interface FoodEntry { id: string; title: string; description: string; rating: number; priceRange: | | ; cuisineType: string; location?: { latitude: number; longitude: number; address: string; }; images: string[]; tags: string[]; createdAt: Date; updatedAt: Date; } export interface FoodFilter { searchText?: string; minRating?: number; cuisineType?: string; priceRange?: string[]; dateRange?: { start: Date; end: Date; }; } // src/types/navigation.ts export type RootStackParamList { Home: undefined; FoodDetail: { foodId: string }; AddFood: { editData?: FoodEntry }; Search: undefined; Statistics: undefined; };3. 数据库设计与本地存储3.1 SQLite数据库架构设计合理的数据表结构是应用稳定性的基础-- 创建美食记录表 CREATE TABLE IF NOT EXISTS food_entries ( id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT, rating INTEGER DEFAULT 0, price_range TEXT CHECK(price_range IN (, , )), cuisine_type TEXT, latitude REAL, longitude REAL, address TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); -- 创建图片表 CREATE TABLE IF NOT EXISTS food_images ( id TEXT PRIMARY KEY, food_entry_id TEXT NOT NULL, image_path TEXT NOT NULL, display_order INTEGER DEFAULT 0, FOREIGN KEY (food_entry_id) REFERENCES food_entries (id) ON DELETE CASCADE ); -- 创建标签表 CREATE TABLE IF NOT EXISTS food_tags ( id TEXT PRIMARY KEY, food_entry_id TEXT NOT NULL, tag_name TEXT NOT NULL, FOREIGN KEY (food_entry_id) REFERENCES food_entries (id) ON DELETE CASCADE ); -- 创建索引优化查询性能 CREATE INDEX IF NOT EXISTS idx_food_rating ON food_entries(rating); CREATE INDEX IF NOT EXISTS idx_food_cuisine ON food_entries(cuisine_type); CREATE INDEX IF NOT EXISTS idx_food_created ON food_entries(created_at);3.2 数据库服务层实现封装数据库操作的核心服务类// src/services/database.service.ts import SQLite from react-native-sqlite-storage; class DatabaseService { private db: SQLite.SQLiteDatabase | null null; async initializeDatabase(): Promisevoid { try { this.db await SQLite.openDatabase({ name: FoodDiary.db, location: default, }); await this.createTables(); console.log(数据库初始化成功); } catch (error) { console.error(数据库初始化失败:, error); throw error; } } private async createTables(): Promisevoid { const createFoodEntriesTable CREATE TABLE IF NOT EXISTS food_entries (...) ; // 执行所有建表语句 await this.executeSql(createFoodEntriesTable); // ... 其他建表语句 } async executeSql(sql: string, params: any[] []): PromiseSQLite.ResultSet { return new Promise((resolve, reject) { if (!this.db) { reject(new Error(数据库未初始化)); return; } this.db.executeSql( sql, params, (result) resolve(result), (error) reject(error) ); }); } // 美食记录CRUD操作 async addFoodEntry(entry: OmitFoodEntry, id): Promisestring { const id this.generateId(); const sql INSERT INTO food_entries (id, title, description, rating, price_range, cuisine_type, latitude, longitude, address, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ; const now Date.now(); await this.executeSql(sql, [ id, entry.title, entry.description, entry.rating, entry.priceRange, entry.cuisineType, entry.location?.latitude, entry.location?.longitude, entry.location?.address, now, now ]); return id; } async getFoodEntries(filter?: FoodFilter): PromiseFoodEntry[] { let sql SELECT fe.*, GROUP_CONCAT(DISTINCT ft.tag_name) as tags, GROUP_CONCAT(DISTINCT fi.image_path) as images FROM food_entries fe LEFT JOIN food_tags ft ON fe.id ft.food_entry_id LEFT JOIN food_images fi ON fe.id fi.food_entry_id ; const conditions: string[] []; const params: any[] []; if (filter?.searchText) { conditions.push((fe.title LIKE ? OR fe.description LIKE ?)); params.push(%${filter.searchText}%, %${filter.searchText}%); } if (filter?.minRating ! undefined) { conditions.push(fe.rating ?); params.push(filter.minRating); } if (conditions.length 0) { sql WHERE conditions.join( AND ); } sql GROUP BY fe.id ORDER BY fe.created_at DESC; const result await this.executeSql(sql, params); return this.mapFoodEntries(result); } private generateId(): string { return Date.now().toString(36) Math.random().toString(36).substr(2); } private mapFoodEntries(result: SQLite.ResultSet): FoodEntry[] { const entries: FoodEntry[] []; for (let i 0; i result.rows.length; i) { const row result.rows.item(i); entries.push({ id: row.id, title: row.title, description: row.description, rating: row.rating, priceRange: row.price_range, cuisineType: row.cuisine_type, location: row.latitude ? { latitude: row.latitude, longitude: row.longitude, address: row.address } : undefined, images: row.images ? row.images.split(,) : [], tags: row.tags ? row.tags.split(,) : [], createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at) }); } return entries; } } export const databaseService new DatabaseService();4. 核心功能模块实现4.1 状态管理设计使用Redux Toolkit管理应用状态// src/store/foodSlice.ts import { createSlice, createAsyncThunk, PayloadAction } from reduxjs/toolkit; import { FoodEntry, FoodFilter } from ../types/food; import { databaseService } from ../services/database.service; interface FoodState { entries: FoodEntry[]; loading: boolean; error: string | null; filters: FoodFilter; } const initialState: FoodState { entries: [], loading: false, error: null, filters: {}, }; // 异步Thunk处理数据操作 export const fetchFoodEntries createAsyncThunk( food/fetchEntries, async (filter?: FoodFilter) { return await databaseService.getFoodEntries(filter); } ); export const addFoodEntry createAsyncThunk( food/addEntry, async (entry: OmitFoodEntry, id | createdAt | updatedAt) { const id await databaseService.addFoodEntry(entry); return { ...entry, id, createdAt: new Date(), updatedAt: new Date() }; } ); const foodSlice createSlice({ name: food, initialState, reducers: { setFilters: (state, action: PayloadActionFoodFilter) { state.filters { ...state.filters, ...action.payload }; }, clearFilters: (state) { state.filters {}; }, }, extraReducers: (builder) { builder .addCase(fetchFoodEntries.pending, (state) { state.loading true; state.error null; }) .addCase(fetchFoodEntries.fulfilled, (state, action) { state.loading false; state.entries action.payload; }) .addCase(fetchFoodEntries.rejected, (state, action) { state.loading false; state.error action.error.message || 获取数据失败; }) .addCase(addFoodEntry.fulfilled, (state, action) { state.entries.unshift(action.payload); }); }, }); export const { setFilters, clearFilters } foodSlice.actions; export default foodSlice.reducer;4.2 美食记录表单组件实现完整的美食记录添加/编辑界面// src/components/FoodForm.tsx import React, { useState, useEffect } from react; import { View, TextInput, Text, TouchableOpacity, ScrollView, Alert, Image, } from react-native; import { useDispatch } from react-redux; import { addFoodEntry } from ../store/foodSlice; import ImagePicker from react-native-image-picker; import { Picker } from react-native-picker/picker; interface FoodFormProps { onSubmit?: () void; editData?: FoodEntry; } const FoodForm: React.FCFoodFormProps ({ onSubmit, editData }) { const dispatch useDispatch(); const [formData, setFormData] useState({ title: , description: , rating: 0, priceRange: as const, cuisineType: , tags: [] as string[], images: [] as string[], }); const [currentTag, setCurrentTag] useState(); useEffect(() { if (editData) { setFormData({ title: editData.title, description: editData.description, rating: editData.rating, priceRange: editData.priceRange, cuisineType: editData.cuisineType, tags: [...editData.tags], images: [...editData.images], }); } }, [editData]); const handleImagePick () { const options { title: 选择美食图片, storageOptions: { skipBackup: true, path: images, }, quality: 0.8, maxWidth: 800, maxHeight: 800, }; ImagePicker.showImagePicker(options, (response) { if (response.didCancel) { console.log(用户取消选择); } else if (response.error) { Alert.alert(图片选择错误, response.error); } else if (response.uri) { setFormData(prev ({ ...prev, images: [...prev.images, response.uri] })); } }); }; const handleAddTag () { if (currentTag.trim() !formData.tags.includes(currentTag.trim())) { setFormData(prev ({ ...prev, tags: [...prev.tags, currentTag.trim()] })); setCurrentTag(); } }; const handleSubmit async () { if (!formData.title.trim()) { Alert.alert(提示, 请输入美食名称); return; } try { await dispatch(addFoodEntry(formData) as any); Alert.alert(成功, 美食记录添加成功); onSubmit?.(); } catch (error) { Alert.alert(错误, 添加失败请重试); } }; return ( ScrollView classNameflex-1 p-4 bg-white Text classNametext-2xl font-bold mb-4 {editData ? 编辑美食记录 : 添加美食记录} /Text {/* 美食名称 */} View classNamemb-4 Text classNametext-lg font-medium mb-2美食名称 */Text TextInput classNameborder border-gray-300 rounded-lg p-3 placeholder输入美食名称 value{formData.title} onChangeText{(text) setFormData(prev ({ ...prev, title: text }))} / /View {/* 评分选择 */} View classNamemb-4 Text classNametext-lg font-medium mb-2评分/Text View classNameflex-row justify-between {[1, 2, 3, 4, 5].map(star ( TouchableOpacity key{star} onPress{() setFormData(prev ({ ...prev, rating: star }))} Text className{text-3xl ${star formData.rating ? text-yellow-400 : text-gray-300}} ★ /Text /TouchableOpacity ))} /View /View {/* 价格范围 */} View classNamemb-4 Text classNametext-lg font-medium mb-2价格范围/Text Picker selectedValue{formData.priceRange} onValueChange{(value) setFormData(prev ({ ...prev, priceRange: value }))} Picker.Item label经济实惠 () value / Picker.Item label中等价位 () value / Picker.Item label高端消费 () value / /Picker /View {/* 图片上传 */} View classNamemb-4 Text classNametext-lg font-medium mb-2美食图片/Text TouchableOpacity classNameborder-2 border-dashed border-gray-300 rounded-lg p-4 items-center onPress{handleImagePick} Text classNametext-gray-500点击添加图片/Text /TouchableOpacity View classNameflex-row flex-wrap mt-2 {formData.images.map((image, index) ( Image key{index} source{{ uri: image }} classNamew-20 h-20 rounded-lg mr-2 mb-2 / ))} /View /View {/* 提交按钮 */} TouchableOpacity classNamebg-blue-500 rounded-lg p-4 items-center mt-6 onPress{handleSubmit} Text classNametext-white text-lg font-bold {editData ? 更新记录 : 添加记录} /Text /TouchableOpacity /ScrollView ); }; export default FoodForm;4.3 美食列表与搜索功能实现带搜索筛选的美食列表页面// src/components/FoodList.tsx import React, { useState, useMemo } from react; import { View, Text, FlatList, TouchableOpacity, Image, TextInput, } from react-native; import { useSelector, useDispatch } from react-redux; import { RootState } from ../store; import { setFilters } from ../store/foodSlice; const FoodList: React.FC () { const dispatch useDispatch(); const { entries, loading, filters } useSelector((state: RootState) state.food); const [searchText, setSearchText] useState(filters.searchText || ); // 筛选后的数据 const filteredEntries useMemo(() { return entries.filter(entry { const matchesSearch !searchText || entry.title.toLowerCase().includes(searchText.toLowerCase()) || entry.description.toLowerCase().includes(searchText.toLowerCase()); const matchesRating !filters.minRating || entry.rating filters.minRating; return matchesSearch matchesRating; }); }, [entries, searchText, filters]); const handleSearch (text: string) { setSearchText(text); dispatch(setFilters({ searchText: text })); }; const renderFoodItem ({ item }: { item: FoodEntry }) ( TouchableOpacity classNamebg-white rounded-lg p-4 mb-3 shadow-sm View classNameflex-row {item.images.length 0 ( Image source{{ uri: item.images[0] }} classNamew-20 h-20 rounded-lg / )} View classNameflex-1 ml-3 Text classNametext-lg font-bold{item.title}/Text Text classNametext-gray-600 text-sm mt-1 numberOfLines{2} {item.description} /Text View classNameflex-row items-center mt-2 Text classNametext-yellow-400 text-lg {★.repeat(item.rating)} {☆.repeat(5 - item.rating)} /Text Text classNametext-gray-500 ml-2{item.priceRange}/Text /View View classNameflex-row flex-wrap mt-2 {item.tags.slice(0, 3).map(tag ( Text key{tag} classNametext-blue-500 text-xs bg-blue-50 px-2 py-1 rounded-full mr-1 mb-1 {tag} /Text ))} /View /View /View /TouchableOpacity ); return ( View classNameflex-1 bg-gray-50 {/* 搜索栏 */} View classNamep-4 bg-white TextInput classNameborder border-gray-300 rounded-lg p-3 bg-white placeholder搜索美食名称或描述... value{searchText} onChangeText{handleSearch} / /View {/* 统计信息 */} View classNamepx-4 py-2 bg-blue-50 Text classNametext-blue-800 共找到 {filteredEntries.length} 条记录 /Text /View {/* 美食列表 */} FlatList data{filteredEntries} renderItem{renderFoodItem} keyExtractor{item item.id} classNameflex-1 p-4 showsVerticalScrollIndicator{false} ListEmptyComponent{ View classNameitems-center justify-center py-10 Text classNametext-gray-500 text-lg {loading ? 加载中... : 暂无美食记录} /Text /View } / /View ); }; export default FoodList;5. 高级功能实现5.1 地理位置集成实现自动获取位置和地图显示功能// src/services/location.service.ts import Geolocation from react-native-community/geolocation; import { PermissionsAndroid, Platform } from react-native; class LocationService { async getCurrentLocation(): Promise{ latitude: number; longitude: number; address: string } { // 检查位置权限 if (Platform.OS android) { const granted await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION ); if (granted ! PermissionsAndroid.RESULTS.GRANTED) { throw new Error(位置权限被拒绝); } } return new Promise((resolve, reject) { Geolocation.getCurrentPosition( async (position) { try { const { latitude, longitude } position.coords; const address await this.reverseGeocode(latitude, longitude); resolve({ latitude, longitude, address }); } catch (error) { reject(error); } }, (error) reject(error), { enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 } ); }); } private async reverseGeocode(latitude: number, longitude: number): Promisestring { // 使用逆地理编码服务获取地址信息 // 这里可以使用高德地图、百度地图等第三方服务 const response await fetch( https://restapi.amap.com/v3/geocode/regeo?keyYOUR_API_KEYlocation${longitude},${latitude} ); const data await response.json(); return data.regeocode?.formatted_address || 未知地址; } } export const locationService new LocationService();5.2 数据统计与分析实现美食数据可视化统计// src/components/StatisticsChart.tsx import React, { useMemo } from react; import { View, Text, Dimensions } from react-native; import { PieChart, BarChart } from react-native-chart-kit; import { useSelector } from react-redux; import { RootState } from ../store; const StatisticsChart: React.FC () { const { entries } useSelector((state: RootState) state.food); const screenWidth Dimensions.get(window).width; // 计算评分分布 const ratingData useMemo(() { const distribution [0, 0, 0, 0, 0]; entries.forEach(entry { if (entry.rating 1 entry.rating 5) { distribution[entry.rating - 1]; } }); return distribution.map((count, index) ({ rating: index 1, count, percentage: entries.length 0 ? (count / entries.length * 100).toFixed(1) : 0 })); }, [entries]); // 计算菜系分布 const cuisineData useMemo(() { const cuisineMap new Mapstring, number(); entries.forEach(entry { const count cuisineMap.get(entry.cuisineType) || 0; cuisineMap.set(entry.cuisineType, count 1); }); return Array.from(cuisineMap.entries()) .sort((a, b) b[1] - a[1]) .slice(0, 5) .map(([cuisine, count]) ({ name: cuisine || 未分类, population: count, color: #${Math.floor(Math.random()*16777215).toString(16)}, legendFontColor: #7F7F7F, legendFontSize: 12, })); }, [entries]); const chartConfig { backgroundGradientFrom: #ffffff, backgroundGradientTo: #ffffff, color: (opacity 1) rgba(0, 0, 0, ${opacity}), strokeWidth: 2, barPercentage: 0.5, }; if (entries.length 0) { return ( View classNameitems-center justify-center py-10 Text classNametext-gray-500暂无数据可统计/Text /View ); } return ( View classNamep-4 Text classNametext-xl font-bold mb-4美食统计/Text {/* 评分分布柱状图 */} Text classNametext-lg font-medium mb-2评分分布/Text BarChart data{{ labels: [1星, 2星, 3星, 4星, 5星], datasets: [{ data: ratingData.map(d d.count) }], }} width{screenWidth - 32} height{220} chartConfig{chartConfig} verticalLabelRotation{0} fromZero / {/* 菜系分布饼图 */} Text classNametext-lg font-medium mt-6 mb-2菜系分布/Text PieChart data{cuisineData} width{screenWidth - 32} height{200} chartConfig{chartConfig} accessorpopulation backgroundColortransparent paddingLeft15 / {/* 统计摘要 */} View classNamemt-6 bg-gray-50 p-4 rounded-lg Text classNamefont-medium mb-2统计摘要/Text Text总记录数: {entries.length}/Text Text平均评分: {(entries.reduce((sum, e) sum e.rating, 0) / entries.length).toFixed(1)}/Text Text最常消费: {cuisineData[0]?.name || 无}/Text /View /View ); }; export default StatisticsChart;6. 性能优化与最佳实践6.1 图片优化策略大规模图片处理时的性能考虑// src/utils/imageOptimizer.ts import ImageResizer from react-native-image-resizer; class ImageOptimizer { async compressImage(uri: string, maxWidth: number 800, maxHeight: number 800): Promisestring { try { const result await ImageResizer.createResizedImage( uri, maxWidth, maxHeight, JPEG, 80, // 质量压缩到80% 0, // 旋转角度 null, // 输出路径 false // 是否保持元数据 ); return result.uri; } catch (error) { console.warn(图片压缩失败使用原图:, error); return uri; } } async optimizeImages(uris: string[]): Promisestring[] { const optimizedUris: string[] []; for (const uri of uris) { try { const optimizedUri await this.compressImage(uri); optimizedUris.push(optimizedUri); } catch (error) { // 单张图片失败不影响其他图片 optimizedUris.push(uri); } } return optimizedUris; } // 清理临时图片文件 async cleanupTempFiles(uris: string[]): Promisevoid { // 实现文件清理逻辑 // 注意需要区分临时文件和永久存储文件 } } export const imageOptimizer new ImageOptimizer();6.2 数据库查询优化提升大数据量下的查询性能// 添加分页查询支持 async getFoodEntriesPaginated( filter?: FoodFilter, page: number 1, pageSize: number 20 ): Promise{ entries: FoodEntry[]; total: number } { const offset (page - 1) * pageSize; // 先获取总数 const countResult await this.executeSql( SELECT COUNT(*) as total FROM food_entries, [] ); const total countResult.rows.item(0).total; // 再获取分页数据 let sql ...; // 基础查询SQL sql LIMIT ? OFFSET ?; const entries await this.getFoodEntriesWithSql(sql, [...params, pageSize, offset]); return { entries, total }; } // 添加查询缓存 private queryCache new Mapstring, { data: any; timestamp: number }(); private CACHE_DURATION 5 * 60 * 1000; // 5分钟缓存 async getCachedFoodEntries(filter: FoodFilter): PromiseFoodEntry[] { const cacheKey JSON.stringify(filter); const cached this.queryCache.get(cacheKey); if (cached Date.now() - cached.timestamp this.CACHE_DURATION) { return cached.data; } const data await this.getFoodEntries(filter); this.queryCache.set(cacheKey, { data, timestamp: Date.now() }); return data; }7. 常见问题与解决方案7.1 数据库操作异常处理// 增强的数据库错误处理 async executeSqlWithRetry( sql: string, params: any[] [], maxRetries: number 3 ): PromiseSQLite.ResultSet { let lastError: Error; for (let attempt 1; attempt maxRetries; attempt) { try { return await this.executeSql(sql, params); } catch (error) { lastError error as Error; console.warn(数据库操作失败第${attempt}次重试:, error); if (attempt maxRetries) { // 等待指数退避时间 await new Promise(resolve setTimeout(resolve, Math.pow(2, attempt) * 1000) ); } } } throw new Error(数据库操作失败已重试${maxRetries}次: ${lastError?.message}); }7.2 图片上传失败处理// 图片上传的容错处理 const handleImageUpload async (images: string[]) { const successfulUploads: string[] []; const failedUploads: string[] []; for (const imageUri of images) { try { // 尝试压缩图片 const optimizedUri await imageOptimizer.compressImage(imageUri); // 上传到服务器或保存到本地 const savedPath await this.saveImage(optimizedUri); successfulUploads.push(savedPath); } catch (error) { console.error(图片处理失败: ${imageUri}, error); failedUploads.push(imageUri); } } if (failedUploads.length 0) { Alert.alert( 部分图片处理失败, 成功处理 ${successfulUploads.length} 张失败 ${failedUploads.length} 张 ); } return successfulUploads; };7.3 性能问题排查清单遇到应用卡顿时的排查步骤检查图片资源图片尺寸是否过大是否进行了适当的压缩列表图片是否使用了合适的缓存策略数据库查询优化是否建立了合适的索引查询语句是否高效是否避免了N1查询问题组件渲染优化是否使用了React.memo避免不必要的重渲染列表是否使用了key属性是否合理使用了useMemo和useCallback内存泄漏检查是否正确清理了事件监听器是否及时取消了网络请求组件卸载时是否清理了定时器8. 项目部署与测试8.1 测试用例编写为关键功能编写单元测试// __tests__/foodSlice.test.ts import { configureStore } from reduxjs/toolkit; import foodReducer, { addFoodEntry, fetchFoodEntries } from ../src/store/foodSlice; describe(food slice, () { let store: any; beforeEach(() { store configureStore({ reducer: { food: foodReducer, }, }); }); it(should handle initial state, () { expect(store.getState().food.entries).toEqual([]); expect(store.getState().food.loading).toBe(false); }); it(should handle adding food entry, async () { const newEntry { title: 测试美食, description: 测试描述, rating: 4, priceRange: as const, cuisineType: 中餐, tags: [测试], images