React Native开源鸿蒙跨平台工程 DAY3:网络请求集成和美食博客页面开发
一、任务目标
按开源鸿蒙跨平台工程 DAY3 要求,基于React Native 技术栈结合鸿蒙原生适配能力,完成工程网络请求全流程开发与配置,实现美食博客核心页面搭建,包含首页、分类页、食谱详情页,保证网络请求流程规范、页面功能完整,在鸿蒙模拟器正常运行验证。
在开发过程中,我选择了以下技术栈:
- 网络请求库:使用 axios,因为它是通用型 HTTP 请求库,支持拦截器、请求取消、响应转换等功能,且在 OpenHarmony 已兼容三方库清单中。
- UI 组件:使用 React Native 内置组件(FlatList、ScrollView、Image、Text 等),确保跨平台兼容性,避免使用平台特定的依赖。
- 数据管理:使用 React 的 useState 和 useEffect 钩子进行状态管理和副作用处理,实现数据的加载、刷新与页面渲染。
- 路由管理:使用 React Navigation 实现页面之间的跳转(首页→分类页→详情页)。
- 鸿蒙适配:基于 React Native for OpenHarmony 0.72.5 版本,完成网络权限配置与跨平台运行验证。
二、核心任务拆解
1. 网络能力与权限配置
- 基于 axios 封装网络请求工具,实现请求 / 响应拦截、超时处理、异常捕获;
- 在鸿蒙工程中声明 ohos.permission.INTERNET 网络权限,完成跨平台网络访问授权;
- 模拟接口返回数据(美食分类、食谱列表、详情信息),遵循真实接口请求流程(请求→解析→渲染)。
2. 数据清单列表构建
- 数据解析:解析 axios 返回的 JSON 数据,提取轮播图、分类、食谱、用料、步骤等核心字段;
- 列表渲染:使用 RN FlatList 实现食谱列表、分类列表,使用 ScrollView 实现详情页内容滚动;
- 异常兜底:处理网络错误、空数据、解析异常场景,展示 “加载失败”“暂无数据” 等友好提示。
3. 核心页面开发(美食博客)
- 首页:标题 + 搜索框 + 美食轮播(自动播放)+ 分类入口(横向滑动)+ 推荐食谱列表(点击跳转详情);
- 分类页:分类标签切换 + 食谱列表 / 网格视图切换 + 搜索功能,按分类筛选食谱;
- 详情页:食谱封面 + 名称 + 难度 / 耗时 + 用料清单 + 烹饪步骤 + 评论区(模拟数据)。
4. 运行验证与代码规范
- 工程在 鸿蒙模拟器(Mate 70 Pro) 正常编译、安装、运行,无崩溃、无布局错乱;
- 代码结构清晰,按 RN + 鸿蒙开发规范划分目录(pages、components、network、model 等);
- 网络请求流程完整,数据加载、页面跳转、交互操作均验证通过。
三、关键实现步骤
1. 网络权限配置(鸿蒙端)
在 RN 工程的鸿蒙适配配置中,添加网络权限声明,确保 axios 能正常发起请求:
// module.json5(鸿蒙权限配置)
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"usedScene": {
"ability": ["EntryAbility"],
"when": "always"
}
}
]
2. axios 网络请求封装(RN 端)
// network/axios.js
import axios from 'axios';
// 创建axios实例
const instance = axios.create({
baseURL: 'https://mock-api.food-blog.com', // 模拟接口地址
timeout: 10000, // 超时时间10s
});
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 可添加请求头、token等
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
instance.interceptors.response.use(
(response) => {
// 解析数据,过滤异常状态码
if (response.data.code !== 200) {
throw new Error('接口请求失败');
}
return response.data.data;
},
(error) => {
// 处理网络错误、超时等
if (error.message.includes('timeout')) {
console.error('请求超时,请检查网络');
} else if (error.message.includes('Network Error')) {
console.error('网络连接失败,请检查网络设置');
}
return Promise.reject(error);
}
);
export default instance;
3. 模拟数据与页面渲染(首页示例)
// pages/Index.js
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, Image, StyleSheet, TouchableOpacity } from 'react-native';
import axios from '../network/axios';
import Carousel from 'react-native-snap-carousel'; // 轮播组件
import { useNavigation } from '@react-navigation/native';
const Index = () => {
const [carouselData, setCarouselData] = useState([]); // 轮播数据
const [categoryData, setCategoryData] = useState([]); // 分类数据
const [recipeData, setRecipeData] = useState([]); // 食谱数据
const [loading, setLoading] = useState(true); // 加载状态
const navigation = useNavigation();
// 获取首页数据
useEffect(() => {
const fetchIndexData = async () => {
try {
setLoading(true);
// 并行请求轮播、分类、食谱数据
const [carouselRes, categoryRes, recipeRes] = await Promise.all([
axios.get('/carousel'),
axios.get('/categories'),
axios.get('/recipes/recommend'),
]);
setCarouselData(carouselRes);
setCategoryData(categoryRes);
setRecipeData(recipeRes);
} catch (error) {
console.error('首页数据请求失败:', error);
} finally {
setLoading(false);
}
};
fetchIndexData();
}, []);
// 渲染轮播项
const renderCarouselItem = ({ item }) => (
<TouchableOpacity
onPress={() => navigation.navigate('RecipeDetail', { id: item.id })}
>
<Image source={{ uri: item.cover }} style={styles.carouselImage} />
<Text style={styles.carouselTitle}>{item.title}</Text>
</TouchableOpacity>
);
// 渲染食谱项
const renderRecipeItem = ({ item }) => (
<TouchableOpacity
style={styles.recipeCard}
onPress={() => navigation.navigate('RecipeDetail', { id: item.id })}
>
<Image source={{ uri: item.cover }} style={styles.recipeImage} />
<View style={styles.recipeInfo}>
<Text style={styles.recipeTitle}>{item.title}</Text>
<Text style={styles.recipeDesc}>{item.desc}</Text>
<View style={styles.recipeMeta}>
<Text style={styles.recipeDifficulty}>难度:{item.difficulty}</Text>
<Text style={styles.recipeTime}>耗时:{item.time}</Text>
</View>
</View>
</TouchableOpacity>
);
if (loading) {
return <Text style={styles.loadingText}>加载中...</Text>;
}
return (
<View style={styles.container}>
{/* 标题+搜索框 */}
<View style={styles.header}>
<Text style={styles.headerTitle}>美食博客</Text>
<View style={styles.searchBox}>
<Text style={styles.searchPlaceholder}>搜索美食...</Text>
</View>
</View>
{/* 轮播图 */}
<Carousel
data={carouselData}
renderItem={renderCarouselItem}
sliderWidth={350}
itemWidth={330}
autoplay
autoplayInterval={3000}
loop
/>
{/* 美食分类 */}
<Text style={styles.sectionTitle}>美食分类</Text>
<FlatList
data={categoryData}
horizontal
renderItem={({ item }) => (
<TouchableOpacity
style={styles.categoryItem}
onPress={() => navigation.navigate('Category', { id: item.id })}
>
<Text style={styles.categoryName}>{item.name}</Text>
</TouchableOpacity>
)}
keyExtractor={(item) => item.id.toString()}
showsHorizontalScrollIndicator={false}
/>
{/* 推荐食谱 */}
<Text style={styles.sectionTitle}>推荐食谱</Text>
<FlatList
data={recipeData}
renderItem={renderRecipeItem}
keyExtractor={(item) => item.id.toString()}
showsVerticalScrollIndicator={false}
/>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: '#f9f4f8' },
header: { marginBottom: 16 },
headerTitle: { fontSize: 24, fontWeight: 'bold', color: '#7b2cbf', marginBottom: 8 },
searchBox: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 8, backgroundColor: '#fff' },
searchPlaceholder: { color: '#999' },
carouselImage: { width: '100%', height: 180, borderRadius: 12 },
carouselTitle: { position: 'absolute', bottom: 16, left: 16, color: '#fff', fontSize: 18, fontWeight: 'bold' },
sectionTitle: { fontSize: 18, fontWeight: 'bold', marginVertical: 12, color: '#333' },
categoryItem: { padding: 8, marginRight: 12, backgroundColor: '#fff', borderRadius: 8, borderWidth: 1, borderColor: '#7b2cbf' },
categoryName: { color: '#7b2cbf', fontWeight: '500' },
recipeCard: { flexDirection: 'row', marginBottom: 12, backgroundColor: '#fff', borderRadius: 12, padding: 8, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4 },
recipeImage: { width: 100, height: 100, borderRadius: 8 },
recipeInfo: { flex: 1, marginLeft: 12, justifyContent: 'center' },
recipeTitle: { fontSize: 16, fontWeight: 'bold', color: '#333' },
recipeDesc: { fontSize: 12, color: '#666', marginVertical: 4 },
recipeMeta: { flexDirection: 'row', justifyContent: 'space-between' },
recipeDifficulty: { fontSize: 12, color: '#7b2cbf' },
recipeTime: { fontSize: 12, color: '#666' },
loadingText: { flex: 1, textAlign: 'center', fontSize: 16, color: '#666' },
});
export default Index;
四、运行效果展示
1. 美食博客首页

- 页面由 RN 内置组件搭建,轮播图自动播放,分类横向滑动,食谱列表可点击跳转详情,数据由 axios 请求解析后渲染。
2. 美食分类页

- 支持分类标签切换、列表 / 网格视图切换,搜索框可输入筛选,布局适配鸿蒙模拟器屏幕,无跨平台偏移。
3. 食谱详情页

- 展示食谱完整信息(封面、名称、难度、耗时、用料、步骤、评论),所有模块由 RN 组件实现,交互流畅。
4. 工程编译运行日志

- DevEco Studio 编译无报错,RN 工程成功安装到鸿蒙模拟器,axios 网络请求、数据解析日志正常打印,验证工程运行稳定。
五、遇到的问题与解决方法
1. 问题:RN + 鸿蒙环境下,axios 请求提示 “网络权限未授权”
解决:在鸿蒙module.json5中补充ohos.permission.INTERNET权限的usedScene配置,指定ability与when字段,重新编译后权限生效。
2. 问题:axios 请求超时,数据加载失败
解决:调整 axios 超时时间为 10s,添加弱网重试逻辑(请求失败后自动重试 2 次),同时优化模拟接口响应速度,确保数据正常加载。
3. 问题:RN 轮播组件在鸿蒙模拟器中滑动卡顿
解决:使用react-native-snap-carousel的enableSnap属性优化滑动体验,减少轮播图预加载数量,降低鸿蒙模拟器资源占用。
4. 问题:RN 列表在鸿蒙端渲染缓慢,数据展示延迟
解决:使用FlatList的initialNumToRender和maxToRenderPerBatch属性优化列表渲染,只加载可视区域内的列表项,提升渲染速度。
六、任务总结
本次 DAY3 任务基于React Native + 开源鸿蒙技术栈,完成了网络请求集成与美食博客核心页面开发:
技术上:通过 axios 实现了规范的网络请求流程,完成鸿蒙网络权限配置,解决了跨平台网络访问、组件适配等问题;
功能上:实现了首页轮播、分类筛选、食谱详情、异常兜底等核心功能,页面交互流畅、布局适配鸿蒙设备;
验证上:工程在鸿蒙模拟器中正常运行,网络请求、数据渲染、页面跳转均验证通过,满足 DAY3 任务的所有要求。
后续可在此基础上对接真实美食接口,替换模拟数据实现动态加载;同时完善评论发布、点赞、收藏等功能,进一步丰富美食博客的使用体验。
欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)