news 2026/9/7 16:05:38

React Native美食日记应用开发实战:从数据库设计到性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React Native美食日记应用开发实战:从数据库设计到性能优化

最近在开发个人项目时,经常需要为美食类应用设计数据结构和交互逻辑。今天想和大家分享一个完整的"美食日记"应用开发实战,从需求分析到代码实现,带你一步步构建一个功能完整的美食记录系统。

无论你是刚入门移动开发的新手,还是想学习完整项目架构的进阶开发者,这篇文章都能为你提供实用的开发思路和可复用的代码模板。我们将使用现代前端技术栈,重点讲解核心功能实现和常见坑点解决方案。

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.0+
  • Android开发需要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(): Promise<void> { 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(): Promise<void> { const createFoodEntriesTable = ` CREATE TABLE IF NOT EXISTS food_entries (...) `; // 执行所有建表语句 await this.executeSql(createFoodEntriesTable); // ... 其他建表语句 } async executeSql(sql: string, params: any[] = []): Promise<SQLite.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: Omit<FoodEntry, 'id'>): Promise<string> { 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): Promise<FoodEntry[]> { 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: Omit<FoodEntry, '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: PayloadAction<FoodFilter>) => { 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.FC<FoodFormProps> = ({ 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 className="flex-1 p-4 bg-white"> <Text className="text-2xl font-bold mb-4"> {editData ? '编辑美食记录' : '添加美食记录'} </Text> {/* 美食名称 */} <View className="mb-4"> <Text className="text-lg font-medium mb-2">美食名称 *</Text> <TextInput className="border border-gray-300 rounded-lg p-3" placeholder="输入美食名称" value={formData.title} onChangeText={(text) => setFormData(prev => ({ ...prev, title: text }))} /> </View> {/* 评分选择 */} <View className="mb-4"> <Text className="text-lg font-medium mb-2">评分</Text> <View className="flex-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 className="mb-4"> <Text className="text-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 className="mb-4"> <Text className="text-lg font-medium mb-2">美食图片</Text> <TouchableOpacity className="border-2 border-dashed border-gray-300 rounded-lg p-4 items-center" onPress={handleImagePick} > <Text className="text-gray-500">点击添加图片</Text> </TouchableOpacity> <View className="flex-row flex-wrap mt-2"> {formData.images.map((image, index) => ( <Image key={index} source={{ uri: image }} className="w-20 h-20 rounded-lg mr-2 mb-2" /> ))} </View> </View> {/* 提交按钮 */} <TouchableOpacity className="bg-blue-500 rounded-lg p-4 items-center mt-6" onPress={handleSubmit} > <Text className="text-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 className="bg-white rounded-lg p-4 mb-3 shadow-sm"> <View className="flex-row"> {item.images.length > 0 && ( <Image source={{ uri: item.images[0] }} className="w-20 h-20 rounded-lg" /> )} <View className="flex-1 ml-3"> <Text className="text-lg font-bold">{item.title}</Text> <Text className="text-gray-600 text-sm mt-1" numberOfLines={2}> {item.description} </Text> <View className="flex-row items-center mt-2"> <Text className="text-yellow-400 text-lg"> {'★'.repeat(item.rating)} {'☆'.repeat(5 - item.rating)} </Text> <Text className="text-gray-500 ml-2">{item.priceRange}</Text> </View> <View className="flex-row flex-wrap mt-2"> {item.tags.slice(0, 3).map(tag => ( <Text key={tag} className="text-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 className="flex-1 bg-gray-50"> {/* 搜索栏 */} <View className="p-4 bg-white"> <TextInput className="border border-gray-300 rounded-lg p-3 bg-white" placeholder="搜索美食名称或描述..." value={searchText} onChangeText={handleSearch} /> </View> {/* 统计信息 */} <View className="px-4 py-2 bg-blue-50"> <Text className="text-blue-800"> 共找到 {filteredEntries.length} 条记录 </Text> </View> {/* 美食列表 */} <FlatList data={filteredEntries} renderItem={renderFoodItem} keyExtractor={item => item.id} className="flex-1 p-4" showsVerticalScrollIndicator={false} ListEmptyComponent={ <View className="items-center justify-center py-10"> <Text className="text-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): Promise<string> { // 使用逆地理编码服务获取地址信息 // 这里可以使用高德地图、百度地图等第三方服务 const response = await fetch( `https://restapi.amap.com/v3/geocode/regeo?key=YOUR_API_KEY&location=${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 Map<string, 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 className="items-center justify-center py-10"> <Text className="text-gray-500">暂无数据可统计</Text> </View> ); } return ( <View className="p-4"> <Text className="text-xl font-bold mb-4">美食统计</Text> {/* 评分分布柱状图 */} <Text className="text-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 className="text-lg font-medium mt-6 mb-2">菜系分布</Text> <PieChart data={cuisineData} width={screenWidth - 32} height={200} chartConfig={chartConfig} accessor="population" backgroundColor="transparent" paddingLeft="15" /> {/* 统计摘要 */} <View className="mt-6 bg-gray-50 p-4 rounded-lg"> <Text className="font-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): Promise<string> { 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[]): Promise<string[]> { 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[]): Promise<void> { // 实现文件清理逻辑 // 注意:需要区分临时文件和永久存储文件 } } 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 Map<string, { data: any; timestamp: number }>(); private CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存 async getCachedFoodEntries(filter: FoodFilter): Promise<FoodEntry[]> { 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 ): Promise<SQLite.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 性能问题排查清单

遇到应用卡顿时的排查步骤:

  1. 检查图片资源

    • 图片尺寸是否过大
    • 是否进行了适当的压缩
    • 列表图片是否使用了合适的缓存策略
  2. 数据库查询优化

    • 是否建立了合适的索引
    • 查询语句是否高效
    • 是否避免了N+1查询问题
  3. 组件渲染优化

    • 是否使用了React.memo避免不必要的重渲染
    • 列表是否使用了key属性
    • 是否合理使用了useMemo和useCallback
  4. 内存泄漏检查

    • 是否正确清理了事件监听器
    • 是否及时取消了网络请求
    • 组件卸载时是否清理了定时器

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
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 16:04:09

离线OCR+本地大模型:Windows环境下的OCR-10结构化字段提取方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 16:01:56

日常通勤Momenta和华为选哪个?前者遇加塞平顺不急刹

在日常通勤与高频泊车场景中&#xff0c;Momenta&#xff08;06880.HK&#xff09;与华为乾崑智驾展现出不同的产品哲学。Momenta依托R7世界模型物理AI基座、新芯航途X7专用SoC芯片与QNX安全操作系统构建的软硬一体架构&#xff0c;已通过TV莱茵ISO 26262 ASIL D最高功能安全认…

作者头像 李华
网站建设 2026/9/7 16:01:26

Git已推送commit如何合并?rebase reword fixup实操指南

1. 为什么 push 之后还要合并 commit 先把场景摆出来&#xff1a;你吭哧吭哧写了一下午功能&#xff0c;分两次提交到了本地仓库&#xff0c;然后 git push 推到了远程。推完一回头&#xff0c;发现这两个 commit 一个写的是 "fix: 修复登录接口超时" &#xff0…

作者头像 李华
网站建设 2026/9/7 16:00:19

2026腾讯云服务器价格全解析:计费、活动与选型指南

每年这个时候&#xff0c;都有朋友跑来问我&#xff1a;2026年腾讯云云服务器价格到底怎么样&#xff0c;现在入手划不划算&#xff1f;问的人多了&#xff0c;我发现大家真正纠结的往往不是几十块钱的差价&#xff0c;而是面对一屏的实例规格、计费方式、活动机型&#xff0c;…

作者头像 李华
网站建设 2026/9/7 16:00:16

MCP与CLI的本质区别:如何用Python将本地脚本改造成MCP Server

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 15:58:44

NFC吉他项链:碰一下秒切歌的智能周边全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华