鸿蒙开发实战:网络请求与新闻阅读应用开发完全指南
本文是《鸿蒙HarmonyOS新手入门系列》第四篇,通过开发一个完整的新闻阅读应用,帮助读者掌握鸿蒙OS中网络请求、数据解析和页面交互的核心技能。文章从项目概述开始,介绍了新闻应用的开发价值和技术亮点,并展示了最终效果。详细讲解了数据模型设计、API响应结构和加载状态管理。重点部分提供了网络请求服务的完整封装代码,包括HTTP请求基础、新闻服务类实现和Mock数据测试方案。所有代码严格遵循华为官方
鸿蒙开发实战:网络请求与新闻阅读应用开发完全指南
专栏说明:本文是《鸿蒙HarmonyOS新手入门系列》第四篇,建议先学习前三篇基础教程。本文将带你开发一个完整的新闻阅读应用,掌握HTTP网络请求、JSON数据解析、图片加载、下拉刷新、页面路由等实用技能。所有代码严格遵循华为官方开发文档规范。
文章目录
一、项目概述
1.1 为什么选择新闻应用?
在完成前三篇的学习后,你已经掌握了:
- ✅ 第一篇:ArkTS基础语法、组件使用
- ✅ 第二篇:列表渲染、数据持久化
- ✅ 第三篇:状态管理、组件通信
但这些都是本地应用,真实的移动应用往往需要从服务器获取数据。新闻阅读应用是学习网络请求的最佳项目:
为什么适合新手?
- 涉及网络请求:学习HTTP协议、异步编程
- 数据展示丰富:文字、图片、列表、详情页
- 交互完整:下拉刷新、加载状态、错误处理
- 实用性强:类似今日头条、网易新闻等主流应用
1.2 应用功能预览
核心功能:
- 新闻列表展示(标题、封面图、来源、时间)
- 下拉刷新获取最新数据
- 加载状态提示(加载中、失败、空数据)
- 点击查看新闻详情
- 网络图片懒加载
- 相对时间显示(10分钟前、2小时前)
技术亮点:
- 使用官方
@ohos.net.http模块发起网络请求 - JSON数据解析与TypeScript类型安全
- Refresh组件实现下拉刷新
- Router路由实现页面跳转传参
- 枚举类型管理加载状态
- 优雅的错误处理机制
1.3 最终效果展示
首页效果:
详情页效果:
二、数据模型设计
2.1 新闻数据接口定义
根据TypeScript最佳实践,我们首先定义数据模型:
// 新闻文章数据模型
export interface NewsArticle {
id: number // 文章唯一标识
title: string // 文章标题
content: string // 文章正文内容
author: string // 作者名称
source: string // 新闻来源(如:新华社、人民网)
imageUrl: string // 封面图片URL
publishTime: string // 发布时间(格式:2025-01-15 10:30:00)
category: string // 文章分类(科技、财经、娱乐等)
viewCount: number // 浏览次数
}
设计说明:
- 使用
interface定义数据结构,提供类型安全 - 所有字段都有明确的类型约束
export关键字表示可以在其他文件中导入使用
2.2 API响应数据结构
真实的API接口通常会返回统一的响应格式:
// 通用API响应格式
interface ApiResponse<T> {
code: number // 状态码(0表示成功)
message: string // 提示信息
data: T // 实际数据(泛型)
}
// 新闻列表接口返回的data结构
interface NewsListData {
articles: NewsArticle[] // 文章数组
}
知识点:
- 泛型
<T>:让ApiResponse可以适配不同的数据类型 - 嵌套结构:真实API通常有多层数据结构
2.3 加载状态枚举
使用枚举类型管理页面的不同状态:
// 加载状态枚举
enum LoadingState {
IDLE, // 初始状态(闲置)
LOADING, // 加载中
SUCCESS, // 加载成功
ERROR // 加载失败
}
为什么用枚举?
- 代码更易读:
LoadingState.LOADING比'loading'更清晰 - 类型安全:编译时可以检查拼写错误
- 便于维护:修改状态值时IDE会自动提示所有使用位置
三、网络请求服务封装
3.1 HTTP请求基础知识
在HarmonyOS中,使用官方提供的@ohos.net.http模块发起网络请求。
核心API:
import http from '@ohos.net.http'
// 1. 创建HTTP请求对象
let httpRequest = http.createHttp()
// 2. 发起请求
httpRequest.request(url, options)
// 3. 销毁请求对象(防止内存泄漏)
httpRequest.destroy()
3.2 创建新闻服务类
根据面向对象原则,我们创建一个专门的服务类来管理网络请求:
import http from '@ohos.net.http'
class NewsServiceClass {
// 基础URL(实际开发中应配置为真实API地址)
private BASE_URL: string = 'https://api.example.com'
// 请求超时时间(毫秒)
private TIMEOUT: number = 10000
/**
* 获取新闻列表
* @param category 新闻分类
* @param page 页码
* @param pageSize 每页数量
* @returns 新闻文章数组
*/
async getNewsList(
category: string = '全部',
page: number = 1,
pageSize: number = 10
): Promise<NewsArticle[]> {
// 1. 创建HTTP请求对象
let httpRequest = http.createHttp()
// 2. 构建请求URL(带查询参数)
const url = `${this.BASE_URL}/news?category=${category}&page=${page}&pageSize=${pageSize}`
try {
// 3. 发起GET请求
const response = await httpRequest.request(url, {
method: http.RequestMethod.GET,
header: {
'Content-Type': 'application/json'
},
expectDataType: http.HttpDataType.STRING,
connectTimeout: this.TIMEOUT,
readTimeout: this.TIMEOUT
})
// 4. 检查响应状态码
if (response.responseCode === 200) {
// 5. 解析JSON数据
const apiResponse: ApiResponse<NewsListData> =
JSON.parse(response.result as string)
// 6. 检查业务状态码
if (apiResponse.code === 0) {
return apiResponse.data.articles
}
}
// 失败时返回空数组
return []
} catch (error) {
console.error('网络请求失败:', JSON.stringify(error))
return []
} finally {
// 7. 销毁请求对象(重要!)
httpRequest.destroy()
}
}
}
// 导出单例对象
export const NewsService = new NewsServiceClass()
代码详解:
| 步骤 | 说明 | 注意事项 |
|---|---|---|
| 1. 创建HTTP对象 | createHttp()创建请求实例 |
每次请求都要创建新对象 |
| 2. 构建URL | 拼接基础URL和查询参数 | 参数需要URL编码 |
| 3. 发起请求 | request()方法支持GET/POST等 |
使用async/await处理异步 |
| 4. 检查状态码 | 200表示HTTP请求成功 | 不等于200要做错误处理 |
| 5. 解析JSON | JSON.parse()解析响应体 |
要使用类型断言 |
| 6. 检查业务码 | 服务端自定义的状态码 | 约定0表示成功 |
| 7. 销毁对象 | 防止内存泄漏 | 一定要在finally中执行 |
3.3 Mock数据实现(用于测试)
在没有真实后端API时,我们可以先用Mock数据测试:
class NewsServiceClass {
// ... 其他代码
/**
* 获取模拟数据
* 用于开发测试,模拟网络延迟
*/
async getMockData(): Promise<NewsArticle[]> {
return new Promise<NewsArticle[]>((resolve) => {
// 模拟网络延迟1秒
setTimeout(() => {
const articles: NewsArticle[] = [
{
id: 1,
title: 'HarmonyOS NEXT正式发布',
content: 'HarmonyOS NEXT正式版发布,开启纯血鸿蒙时代。这是一个完全独立的操作系统,不再兼容Android应用,标志着鸿蒙生态的全面成熟。系统采用全新的微内核架构,在性能、安全性和流畅度方面都有显著提升。',
author: '张记者',
source: '科技日报',
imageUrl: 'https://picsum.photos/400/250',
publishTime: '2025-01-15 10:30:00',
category: '科技',
viewCount: 15200
},
{
id: 2,
title: '鸿蒙开发者突破100万',
content: '华为宣布鸿蒙应用开发者数量突破100万大关,生态建设进入快车道。越来越多的企业和个人开发者加入鸿蒙生态,为用户带来丰富多样的应用体验。华为承诺将继续加大对开发者的支持力度。',
author: '李编辑',
source: '人民网',
imageUrl: 'https://picsum.photos/400/250',
publishTime: '2025-01-15 09:15:00',
category: '科技',
viewCount: 8900
},
{
id: 3,
title: 'ArkTS成为最受欢迎语言',
content: 'Stack Overflow最新调查显示,ArkTS语言受欢迎程度大幅提升,成为最受开发者喜爱的移动开发语言之一。ArkTS基于TypeScript扩展,提供了更强大的类型系统和更好的开发工具支持。',
author: '王主编',
source: '开发者头条',
imageUrl: 'https://picsum.photos/400/250',
publishTime: '2025-01-15 08:00:00',
category: '编程',
viewCount: 12500
}
// ... 更多数据
]
resolve(articles)
}, 1000)
})
}
}
Mock数据的作用:
- 前端独立开发:不依赖后端接口
- 测试UI逻辑:验证加载状态、错误处理
- 学习最佳实践:先用Mock,后期替换为真实API
四、新闻列表页面开发
4.1 页面状态管理
首先定义页面需要的状态变量:
@Entry
@Component
struct Index {
// 新闻列表数据
@State newsList: NewsArticle[] = []
// 加载状态(使用枚举)
@State loadingState: LoadingState = LoadingState.IDLE
// 错误信息
@State errorMessage: string = ''
// 是否正在刷新
@State isRefreshing: boolean = false
// 组件即将出现时调用
aboutToAppear(): void {
this.loadNews()
}
build() {
// UI代码(后续实现)
}
}
状态说明:
| 状态变量 | 类型 | 作用 |
|---|---|---|
newsList |
数组 | 存储新闻数据 |
loadingState |
枚举 | 管理页面状态(加载中/成功/失败) |
errorMessage |
字符串 | 存储错误提示信息 |
isRefreshing |
布尔值 | 控制下拉刷新动画 |
4.2 加载新闻数据
实现数据加载方法:
/**
* 加载新闻数据
*/
async loadNews(): Promise<void> {
// 1. 设置加载状态
this.loadingState = LoadingState.LOADING
this.errorMessage = ''
try {
// 2. 调用服务类获取数据
const articles = await NewsService.getMockData()
// 3. 更新数据和状态
this.newsList = articles
this.loadingState = LoadingState.SUCCESS
} catch (err) {
// 4. 错误处理
console.error('加载新闻失败:', JSON.stringify(err))
this.loadingState = LoadingState.ERROR
this.errorMessage = '网络请求失败,请稍后重试'
}
}
流程说明:
- 设置加载中状态:显示loading动画
- 发起异步请求:使用await等待结果
- 成功处理:更新数据,设置成功状态
- 失败处理:捕获异常,设置错误状态
4.3 下拉刷新实现
实现下拉刷新功能:
/**
* 下拉刷新
*/
async onRefresh(): Promise<void> {
// 1. 开启刷新动画
this.isRefreshing = true
try {
// 2. 重新获取数据
const articles = await NewsService.getMockData()
// 3. 更新列表
this.newsList = articles
this.loadingState = LoadingState.SUCCESS
} catch (err) {
console.error('刷新失败:', JSON.stringify(err))
} finally {
// 4. 关闭刷新动画(无论成功失败都要关闭)
this.isRefreshing = false
}
}
关键点:
- 使用
finally确保刷新动画一定会关闭 - 刷新失败时不改变已有数据,避免列表清空
4.4 时间格式化
实现相对时间显示(“10分钟前”、“2小时前”):
/**
* 格式化时间为相对时间
* @param timeStr 时间字符串(2025-01-15 10:30:00)
* @returns 相对时间(10分钟前)
*/
formatTime(timeStr: string): string {
const now = new Date()
const time = new Date(timeStr)
const diff = now.getTime() - time.getTime() // 时间差(毫秒)
const minutes = Math.floor(diff / 60000) // 分钟数
const hours = Math.floor(diff / 3600000) // 小时数
const days = Math.floor(diff / 86400000) // 天数
if (minutes < 60) {
return `${minutes}分钟前`
} else if (hours < 24) {
return `${hours}小时前`
} else if (days < 7) {
return `${days}天前`
} else {
// 超过7天显示具体日期
return timeStr.substring(0, 10)
}
}
时间计算:
- 1分钟 = 60,000毫秒
- 1小时 = 3,600,000毫秒
- 1天 = 86,400,000毫秒
4.5 页面跳转
实现点击新闻跳转到详情页:
import router from '@ohos.router'
/**
* 跳转到新闻详情页
* @param article 新闻文章数据
*/
navigateToDetail(article: NewsArticle): void {
router.pushUrl({
url: 'pages/NewsDetailPage', // 目标页面路径
params: article // 传递参数
})
}
路由说明:
pushUrl:跳转到新页面,可以返回params:传递任意对象给目标页面url:不需要加.ets后缀
五、UI界面构建
5.1 主页面结构
build() {
Column() {
// 1. 顶部导航栏
Row() {
Text('📰 新闻头条')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.width('100%')
.height(56)
.padding({ left: 20, right: 20 })
.backgroundColor('#1890FF')
// 2. 根据加载状态显示不同内容
if (this.loadingState === LoadingState.LOADING && this.newsList.length === 0) {
this.LoadingView() // 加载中视图
} else if (this.loadingState === LoadingState.ERROR) {
this.ErrorView() // 错误视图
} else if (this.newsList.length === 0) {
this.EmptyView() // 空数据视图
} else {
this.NewsListView() // 新闻列表视图
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
条件渲染逻辑:
- 首次加载且列表为空 → 显示loading
- 加载失败 → 显示错误页
- 加载成功但无数据 → 显示空状态
- 有数据 → 显示列表
5.2 加载中视图
@Builder
LoadingView() {
Column() {
// 系统提供的加载动画组件
LoadingProgress()
.width(60)
.height(60)
.color('#1890FF')
Text('加载中...')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 15 })
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
5.3 错误视图
@Builder
ErrorView() {
Column({ space: 15 }) {
Text('😢')
.fontSize(60)
Text(this.errorMessage)
.fontSize(16)
.fontColor('#999999')
// 重新加载按钮
Button('重新加载')
.fontSize(16)
.backgroundColor('#1890FF')
.onClick(() => {
this.loadNews()
})
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
5.4 空数据视图
@Builder
EmptyView() {
Column({ space: 15 }) {
Text('📭')
.fontSize(60)
Text('暂无新闻')
.fontSize(16)
.fontColor('#999999')
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
5.5 新闻列表视图
@Builder
NewsListView() {
// Refresh组件实现下拉刷新
Refresh({ refreshing: $$this.isRefreshing }) {
List({ space: 0 }) {
ForEach(this.newsList, (article: NewsArticle) => {
ListItem() {
this.NewsItemView(article)
}
.onClick(() => {
this.navigateToDetail(article)
})
}, (article: NewsArticle) => article.id.toString())
}
.width('100%')
.height('100%')
}
.onRefreshing(() => {
this.onRefresh()
})
.width('100%')
.layoutWeight(1)
}
Refresh组件说明:
refreshing:控制刷新动画的显示$$:双向绑定语法,Refresh组件会自动修改isRefreshing的值onRefreshing:下拉刷新触发的回调
5.6 新闻项视图
@Builder
NewsItemView(article: NewsArticle) {
Column() {
Row({ space: 12 }) {
// 封面图片
Image(article.imageUrl)
.width(120)
.height(90)
.borderRadius(8)
.objectFit(ImageFit.Cover)
.syncLoad(false) // 异步加载图片
// 右侧文字信息
Column({ space: 8 }) {
// 标题
Text(article.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.maxLines(2) // 最多显示2行
.textOverflow({ overflow: TextOverflow.Ellipsis }) // 超出显示省略号
// 来源和时间
Row({ space: 15 }) {
Text(article.source)
.fontSize(12)
.fontColor('#999999')
Text(this.formatTime(article.publishTime))
.fontSize(12)
.fontColor('#999999')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.height(90)
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
.padding(15)
// 分隔线
Divider()
.strokeWidth(1)
.color('#F0F0F0')
}
.width('100%')
.backgroundColor(Color.White)
}
布局说明:
- 左侧:120x90的封面图
- 右侧:垂直布局,标题在上、信息在下
- 分隔线:用Divider组件实现
六、新闻详情页开发
6.1 创建详情页文件
在pages目录下创建NewsDetailPage.ets:
import router from '@ohos.router'
import { NewsArticle } from './Index'
@Entry
@Component
struct NewsDetailPage {
// 文章数据
@State article: NewsArticle = {
id: 0,
title: '',
content: '',
author: '',
source: '',
imageUrl: '',
publishTime: '',
category: '',
viewCount: 0
}
// 组件即将出现时获取传递的参数
aboutToAppear(): void {
const params = router.getParams() as NewsArticle
if (params && params.id) {
this.article = params
}
}
build() {
// UI代码(后续实现)
}
}
接收路由参数:
router.getParams():获取跳转时传递的参数- 使用类型断言
as NewsArticle确保类型安全 - 检查
params.id确认数据有效
6.2 详情页UI实现
build() {
Column() {
// 1. 顶部导航栏
Row() {
// 返回按钮
Button({ type: ButtonType.Normal }) {
Text('←')
.fontSize(24)
.fontColor(Color.White)
}
.width(40)
.height(40)
.backgroundColor('transparent')
.onClick(() => {
router.back() // 返回上一页
})
Text('新闻详情')
.fontSize(18)
.fontColor(Color.White)
.layoutWeight(1)
.textAlign(TextAlign.Center)
// 占位元素(保持标题居中)
Row().width(40).height(40)
}
.width('100%')
.height(56)
.padding({ left: 10, right: 10 })
.backgroundColor('#1890FF')
// 2. 文章内容区
if (this.article.id > 0) {
Scroll() {
Column({ space: 0 }) {
// 封面大图
Image(this.article.imageUrl)
.width('100%')
.height(250)
.objectFit(ImageFit.Cover)
// 文章信息
Column({ space: 15 }) {
// 标题
Text(this.article.title)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.lineHeight(32)
// 元信息(来源、作者、阅读量)
Row({ space: 20 }) {
Row({ space: 5 }) {
Text('📰').fontSize(14)
Text(this.article.source)
.fontSize(14)
.fontColor('#666666')
}
Row({ space: 5 }) {
Text('✍️').fontSize(14)
Text(this.article.author)
.fontSize(14)
.fontColor('#666666')
}
Row({ space: 5 }) {
Text('👁').fontSize(14)
Text(this.article.viewCount.toString())
.fontSize(14)
.fontColor('#666666')
}
}
.width('100%')
// 发布时间
Text(this.article.publishTime)
.fontSize(12)
.fontColor('#999999')
// 分隔线
Divider()
.strokeWidth(1)
.color('#E0E0E0')
.margin({ top: 15, bottom: 15 })
// 正文内容
Text(this.article.content)
.fontSize(16)
.fontColor('#333333')
.lineHeight(28)
// 底部留白
Row().height(30)
}
.width('100%')
.padding(20)
.backgroundColor(Color.White)
}
}
.width('100%')
.layoutWeight(1)
.backgroundColor('#F5F5F5')
} else {
// 无数据提示
Column() {
Text('未找到文章')
.fontSize(16)
.fontColor('#999999')
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.height('100%')
}
UI结构:
- 顶部导航栏:返回按钮 + 标题
- 封面图片:全宽展示
- 文章信息:标题、来源、作者、阅读量、时间
- 正文内容:可滚动的长文本
七、网络请求进阶
7.1 请求方法对比
HarmonyOS HTTP模块支持多种请求方法:
// GET请求(获取数据)
httpRequest.request(url, {
method: http.RequestMethod.GET,
header: {
'Content-Type': 'application/json'
}
})
// POST请求(提交数据)
httpRequest.request(url, {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json'
},
extraData: JSON.stringify({
username: 'zhangsan',
password: '123456'
})
})
// PUT请求(更新数据)
httpRequest.request(url, {
method: http.RequestMethod.PUT,
extraData: JSON.stringify({ title: '新标题' })
})
// DELETE请求(删除数据)
httpRequest.request(url, {
method: http.RequestMethod.DELETE
})
7.2 请求头配置
常用的HTTP请求头:
header: {
'Content-Type': 'application/json', // JSON格式
'Authorization': 'Bearer token123', // 身份认证
'User-Agent': 'HarmonyOS App/1.0', // 客户端标识
'Accept-Language': 'zh-CN', // 语言偏好
}
7.3 错误处理最佳实践
完善的错误处理:
async getNewsList(): Promise<NewsArticle[]> {
let httpRequest = http.createHttp()
try {
const response = await httpRequest.request(url, options)
// 1. 检查HTTP状态码
if (response.responseCode !== 200) {
console.error(`HTTP错误: ${response.responseCode}`)
return []
}
// 2. 解析JSON
let apiResponse: ApiResponse<NewsListData>
try {
apiResponse = JSON.parse(response.result as string)
} catch (parseError) {
console.error('JSON解析失败:', parseError)
return []
}
// 3. 检查业务状态码
if (apiResponse.code !== 0) {
console.error(`业务错误: ${apiResponse.message}`)
return []
}
// 4. 返回数据
return apiResponse.data.articles
} catch (error) {
// 网络异常、超时等
console.error('请求异常:', JSON.stringify(error))
return []
} finally {
// 5. 确保销毁请求对象
httpRequest.destroy()
}
}
错误处理层级:
- 网络层:连接失败、超时(try-catch捕获)
- HTTP层:状态码404、500等(检查responseCode)
- 业务层:服务端返回的错误码(检查apiResponse.code)
- 数据层:JSON解析失败(try-catch捕获)
7.4 请求超时配置
const response = await httpRequest.request(url, {
method: http.RequestMethod.GET,
connectTimeout: 10000, // 连接超时:10秒
readTimeout: 10000 // 读取超时:10秒
})
超时时间建议:
- 🏠 WiFi环境:5-10秒
- 📱 4G/5G网络:10-15秒
- 🐌 弱网环境:15-30秒
八、图片加载优化
8.1 Image组件核心属性
Image(url)
.width(120)
.height(90)
.objectFit(ImageFit.Cover) // 图片填充模式
.syncLoad(false) // 异步加载(默认)
.interpolation(ImageInterpolation.High) // 高质量插值
.alt($r('app.media.placeholder')) // 加载失败时的占位图
objectFit参数说明:
| 参数 | 说明 | 使用场景 |
|---|---|---|
Cover |
裁剪填充,保持比例 | 封面图、头像 |
Contain |
完整显示,留白 | Logo、图标 |
Fill |
拉伸填充 | 背景图 |
None |
原始尺寸 | 表情、小图标 |
ScaleDown |
缩小但不放大 | 详情图 |
8.2 懒加载实现
Image组件默认支持懒加载,只需设置syncLoad(false):
List() {
ForEach(this.newsList, (article: NewsArticle) => {
ListItem() {
Image(article.imageUrl)
.syncLoad(false) // 滚动到可见区域才加载
}
})
}
8.3 加载失败处理
设置占位图和加载失败回调:
Image(article.imageUrl)
.alt($r('app.media.image_placeholder')) // 占位图
.onError(() => {
console.error('图片加载失败:', article.imageUrl)
})
.onComplete(() => {
console.info('图片加载成功')
})
8.4 缓存策略
HarmonyOS会自动缓存网络图片,无需额外配置。缓存位置:
/data/storage/el2/base/haps/<bundleName>/cache/web/Cache/
清理缓存:
- 应用卸载时自动清理
- 可以手动调用清理API(需要权限)
九、下拉刷新进阶
9.1 Refresh组件详解
Refresh({ refreshing: $$this.isRefreshing }) {
// 需要刷新的内容
List() {
// ...
}
}
.onRefreshing(() => {
// 刷新触发时的回调
this.loadData()
})
.refreshOffset(64) // 触发刷新的偏移量
.pullToRefresh(true) // 是否支持下拉刷新
9.2 自定义刷新动画
Refresh({
refreshing: $$this.isRefreshing,
builder: this.customRefreshBuilder // 自定义刷新视图
}) {
List() { /* ... */ }
}
@Builder
customRefreshBuilder() {
Row() {
LoadingProgress()
.width(30)
.height(30)
.color('#1890FF')
Text('正在刷新...')
.fontSize(14)
.fontColor('#999999')
.margin({ left: 10 })
}
}
9.3 上拉加载更多
实现分页加载:
@State page: number = 1
@State hasMore: boolean = true
// 列表滚动到底部触发
onReachEnd() {
if (this.hasMore && !this.isLoading) {
this.loadMore()
}
}
async loadMore(): Promise<void> {
this.page++
const newArticles = await NewsService.getNewsList('全部', this.page)
if (newArticles.length > 0) {
this.newsList = [...this.newsList, ...newArticles]
} else {
this.hasMore = false // 没有更多数据了
}
}
十、性能优化技巧
10.1 列表性能优化
ForEach键生成函数优化:
// ❌ 不推荐:使用索引作为key
ForEach(this.newsList, (article: NewsArticle, index: number) => {
ListItem() { /* ... */ }
}, (article: NewsArticle, index: number) => index.toString())
// ✅ 推荐:使用唯一标识作为key
ForEach(this.newsList, (article: NewsArticle) => {
ListItem() { /* ... */ }
}, (article: NewsArticle) => article.id.toString())
为什么key很重要?
- 帮助框架识别哪些元素发生了变化
- 避免不必要的DOM重建
- 提升列表滚动流畅度
10.2 图片加载优化
Image(article.imageUrl)
.syncLoad(false) // 异步加载
.renderMode(ImageRenderMode.Original) // 渲染模式
.sourceSize({ width: 400, height: 250 }) // 指定解码尺寸
优化建议:
- 使用CDN加速图片加载
- 服务端提供多种尺寸的图片
- 列表缩略图使用小尺寸,详情页使用大尺寸
10.3 网络请求优化
请求合并:
// 批量获取多个分类的新闻
async getMultiCategoryNews(categories: string[]): Promise<Map<string, NewsArticle[]>> {
const promises = categories.map(cat => this.getNewsList(cat))
const results = await Promise.all(promises)
const map = new Map<string, NewsArticle[]>()
categories.forEach((cat, index) => {
map.set(cat, results[index])
})
return map
}
请求去重:
private requestCache = new Map<string, Promise<NewsArticle[]>>()
async getNewsList(category: string): Promise<NewsArticle[]> {
const key = `news_${category}`
// 检查是否有正在进行的相同请求
if (this.requestCache.has(key)) {
return this.requestCache.get(key)!
}
// 发起新请求
const promise = this.fetchNews(category)
this.requestCache.set(key, promise)
try {
const result = await promise
return result
} finally {
// 请求完成后清除缓存
this.requestCache.delete(key)
}
}
10.4 数据缓存策略
简单的内存缓存实现:
class CacheManager {
private cache = new Map<string, { data: any, expireTime: number }>()
// 设置缓存(5分钟过期)
set(key: string, data: any, ttl: number = 5 * 60 * 1000): void {
const expireTime = Date.now() + ttl
this.cache.set(key, { data, expireTime })
}
// 获取缓存
get(key: string): any | null {
const item = this.cache.get(key)
if (!item) return null
// 检查是否过期
if (Date.now() > item.expireTime) {
this.cache.delete(key)
return null
}
return item.data
}
}
十一、常见问题与解决方案
11.1 网络权限配置
问题:网络请求报错"Permission denied"
解决方案:在module.json5中添加网络权限:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:internet_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
11.2 图片无法显示
常见原因和解决方法:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 图片不显示 | URL地址错误 | 检查控制台错误日志 |
| 显示占位图 | 网络权限未配置 | 添加INTERNET权限 |
| 加载很慢 | 图片太大 | 使用压缩后的图片 |
| 显示模糊 | 插值算法问题 | 设置.interpolation(ImageInterpolation.High) |
11.3 数据不刷新
问题:修改数组元素后UI不更新
// ❌ 错误写法:直接修改数组元素
this.newsList[0].title = '新标题'
// ✅ 正确写法:创建新数组
this.newsList = this.newsList.map((article, index) => {
if (index === 0) {
return { ...article, title: '新标题' }
}
return article
})
11.4 路由跳转失败
问题:点击新闻无反应
检查清单:
- ✅ 目标页面路径是否正确(
pages/NewsDetailPage) - ✅ 目标页面是否在
main_pages.json中注册 - ✅ 是否使用了
@Entry装饰器 - ✅ 查看控制台是否有错误日志
main_pages.json配置:
{
"src": [
"pages/Index",
"pages/NewsDetailPage" // 添加新页面
]
}
11.5 下拉刷新无效
问题:下拉没有触发刷新
解决方案:
// 确保使用双向绑定 $$
Refresh({ refreshing: $$this.isRefreshing }) {
// 不是 $this.isRefreshing,而是 $$this.isRefreshing
}
11.6 JSON解析报错
问题:JSON.parse()抛出异常
原因:服务端返回的不是标准JSON格式
解决方案:
try {
const data = JSON.parse(response.result as string)
return data
} catch (error) {
console.error('JSON解析失败,原始数据:', response.result)
return []
}
十二、完整代码汇总
12.1 主页面代码(Index.ets)
import http from '@ohos.net.http'
import router from '@ohos.router'
// ========== 数据模型定义 ==========
export interface NewsArticle {
id: number
title: string
content: string
author: string
source: string
imageUrl: string
publishTime: string
category: string
viewCount: number
}
interface ApiResponse<T> {
code: number
message: string
data: T
}
interface NewsListData {
articles: NewsArticle[]
}
// ========== 新闻服务类 ==========
class NewsServiceClass {
private BASE_URL: string = 'https://api.example.com'
private TIMEOUT: number = 10000
async getMockData(): Promise<NewsArticle[]> {
return new Promise<NewsArticle[]>((resolve) => {
setTimeout(() => {
const articles: NewsArticle[] = [
{
id: 1,
title: 'HarmonyOS NEXT正式发布',
content: 'HarmonyOS NEXT正式版发布,开启纯血鸿蒙时代...',
author: '张记者',
source: '科技日报',
imageUrl: 'https://picsum.photos/400/250',
publishTime: '2025-01-15 10:30:00',
category: '科技',
viewCount: 15200
},
// ... 更多数据
]
resolve(articles)
}, 1000)
})
}
}
export const NewsService = new NewsServiceClass()
// ========== 加载状态枚举 ==========
enum LoadingState {
IDLE,
LOADING,
SUCCESS,
ERROR
}
// ========== 主页面组件 ==========
@Entry
@Component
struct Index {
@State newsList: NewsArticle[] = []
@State loadingState: LoadingState = LoadingState.IDLE
@State errorMessage: string = ''
@State isRefreshing: boolean = false
aboutToAppear(): void {
this.loadNews()
}
async loadNews(): Promise<void> {
this.loadingState = LoadingState.LOADING
this.errorMessage = ''
try {
const articles = await NewsService.getMockData()
this.newsList = articles
this.loadingState = LoadingState.SUCCESS
} catch (err) {
console.error('加载新闻失败:', JSON.stringify(err))
this.loadingState = LoadingState.ERROR
this.errorMessage = '网络请求失败,请稍后重试'
}
}
async onRefresh(): Promise<void> {
this.isRefreshing = true
try {
const articles = await NewsService.getMockData()
this.newsList = articles
this.loadingState = LoadingState.SUCCESS
} catch (err) {
console.error('刷新失败:', JSON.stringify(err))
}
this.isRefreshing = false
}
formatTime(timeStr: string): string {
const now = new Date()
const time = new Date(timeStr)
const diff = now.getTime() - time.getTime()
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(diff / 3600000)
const days = Math.floor(diff / 86400000)
if (minutes < 60) {
return `${minutes}分钟前`
} else if (hours < 24) {
return `${hours}小时前`
} else if (days < 7) {
return `${days}天前`
} else {
return timeStr.substring(0, 10)
}
}
navigateToDetail(article: NewsArticle): void {
router.pushUrl({
url: 'pages/NewsDetailPage',
params: article
})
}
build() {
Column() {
// 顶部导航栏
Row() {
Text('📰 新闻头条')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.width('100%')
.height(56)
.padding({ left: 20, right: 20 })
.backgroundColor('#1890FF')
// 根据状态显示不同视图
if (this.loadingState === LoadingState.LOADING && this.newsList.length === 0) {
this.LoadingView()
} else if (this.loadingState === LoadingState.ERROR) {
this.ErrorView()
} else if (this.newsList.length === 0) {
this.EmptyView()
} else {
this.NewsListView()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
LoadingView() {
Column() {
LoadingProgress()
.width(60)
.height(60)
.color('#1890FF')
Text('加载中...')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 15 })
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
@Builder
ErrorView() {
Column({ space: 15 }) {
Text('😢')
.fontSize(60)
Text(this.errorMessage)
.fontSize(16)
.fontColor('#999999')
Button('重新加载')
.fontSize(16)
.backgroundColor('#1890FF')
.onClick(() => {
this.loadNews()
})
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
@Builder
EmptyView() {
Column({ space: 15 }) {
Text('📭')
.fontSize(60)
Text('暂无新闻')
.fontSize(16)
.fontColor('#999999')
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
@Builder
NewsListView() {
Refresh({ refreshing: $$this.isRefreshing }) {
List({ space: 0 }) {
ForEach(this.newsList, (article: NewsArticle) => {
ListItem() {
this.NewsItemView(article)
}
.onClick(() => {
this.navigateToDetail(article)
})
}, (article: NewsArticle) => article.id.toString())
}
.width('100%')
.height('100%')
}
.onRefreshing(() => {
this.onRefresh()
})
.width('100%')
.layoutWeight(1)
}
@Builder
NewsItemView(article: NewsArticle) {
Column() {
Row({ space: 12 }) {
Image(article.imageUrl)
.width(120)
.height(90)
.borderRadius(8)
.objectFit(ImageFit.Cover)
.syncLoad(false)
Column({ space: 8 }) {
Text(article.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 15 }) {
Text(article.source)
.fontSize(12)
.fontColor('#999999')
Text(this.formatTime(article.publishTime))
.fontSize(12)
.fontColor('#999999')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.height(90)
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
.padding(15)
Divider()
.strokeWidth(1)
.color('#F0F0F0')
}
.width('100%')
.backgroundColor(Color.White)
}
}
12.2 详情页代码(NewsDetailPage.ets)
import router from '@ohos.router'
import { NewsArticle } from './Index'
@Entry
@Component
struct NewsDetailPage {
@State article: NewsArticle = {
id: 0,
title: '',
content: '',
author: '',
source: '',
imageUrl: '',
publishTime: '',
category: '',
viewCount: 0
}
aboutToAppear(): void {
const params = router.getParams() as NewsArticle
if (params && params.id) {
this.article = params
}
}
build() {
Column() {
// 顶部导航栏
Row() {
Button({ type: ButtonType.Normal }) {
Text('←')
.fontSize(24)
.fontColor(Color.White)
}
.width(40)
.height(40)
.backgroundColor('transparent')
.onClick(() => {
router.back()
})
Text('新闻详情')
.fontSize(18)
.fontColor(Color.White)
.layoutWeight(1)
.textAlign(TextAlign.Center)
Row().width(40).height(40)
}
.width('100%')
.height(56)
.padding({ left: 10, right: 10 })
.backgroundColor('#1890FF')
if (this.article.id > 0) {
Scroll() {
Column({ space: 0 }) {
Image(this.article.imageUrl)
.width('100%')
.height(250)
.objectFit(ImageFit.Cover)
Column({ space: 15 }) {
Text(this.article.title)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.lineHeight(32)
Row({ space: 20 }) {
Row({ space: 5 }) {
Text('📰').fontSize(14)
Text(this.article.source).fontSize(14).fontColor('#666666')
}
Row({ space: 5 }) {
Text('✍️').fontSize(14)
Text(this.article.author).fontSize(14).fontColor('#666666')
}
Row({ space: 5 }) {
Text('👁').fontSize(14)
Text(this.article.viewCount.toString()).fontSize(14).fontColor('#666666')
}
}
.width('100%')
Text(this.article.publishTime)
.fontSize(12)
.fontColor('#999999')
Divider()
.strokeWidth(1)
.color('#E0E0E0')
.margin({ top: 15, bottom: 15 })
Text(this.article.content)
.fontSize(16)
.fontColor('#333333')
.lineHeight(28)
Row().height(30)
}
.width('100%')
.padding(20)
.backgroundColor(Color.White)
}
}
.width('100%')
.layoutWeight(1)
.backgroundColor('#F5F5F5')
} else {
Column() {
Text('未找到文章')
.fontSize(16)
.fontColor('#999999')
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.height('100%')
}
}
🔗 参考资料
本文所有代码严格遵循华为官方文档规范:
💡 结语
恭喜你!通过本教程,你已经:
✅ 掌握了HTTP网络请求的完整流程
✅ 学会了JSON数据解析与类型安全处理
✅ 实现了一个完整的新闻阅读应用
✅ 理解了异步编程和错误处理机制
✅ 学会了图片懒加载和下拉刷新
✅ 掌握了页面路由和参数传递
作者寄语:网络请求是移动应用开发的必备技能,掌握本文知识后,你已经能开发大部分常见的应用了。不要停下学习的脚步,继续探索鸿蒙生态的更多可能性。记住:实践是最好的老师,多动手才能真正掌握!
如果本文对你有帮助,欢迎点赞👍、收藏⭐、关注➕!有问题欢迎在评论区讨论!
标签:#HarmonyOS #鸿蒙开发 #ArkTS #网络请求 #HTTP #JSON解析 #新闻应用 #实战教程
版权声明:本文为作者原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
更多推荐


所有评论(0)