【云驻共创】把网络请求玩出花来:OpenHarmony应用开发中的数据交互终极实战指南,你确定不点进来看看?
💖 前言:嘿,还记得被网络请求支配的恐惧吗?
哈喽,各位奋战在一线的代码英雄们!👋 我是你们的老朋友,一个热爱生活、更热爱Coding的普通程序员。今天,咱们不聊风花雪月,就聊点硬核的、能让你在项目里横着走的干货——OpenHarmony应用开发中的网络数据请求与数据解析。
想当年,我还是个萌新的时候,面对网络请求,那叫一个头大啊!🤯 啥是GET,啥是POST?跨域是啥玩意儿?为啥我的请求发出去了,服务器那边却说“你谁啊”?为啥拿到的数据是一堆看不懂的“天书”?还有那该死的undefined is not a function
… 简直是午夜梦回的“惊魂一刻”!
但是!朋友们,时代变了!随着OpenHarmony的崛起,它给我们开发者提供了一套堪称“保姆级”的网络API。它不仅强大,而且优雅。今天,我就将我这几年踩过的坑、总结的经验、压箱底的宝贝,毫无保留地分享给大家。这篇文章的目标只有一个:让你从网络请求的小白,一跃成为同事眼中的“网络大神”。我们将从最基础的API讲起,一步步深入到封装、缓存、安全、异常处理,最后用一个完整的实战项目,把所有知识点串起来。
这篇文章会很长,但相信我,绝对值得你泡上一杯咖啡☕,慢慢品味。准备好了吗?让我们一起,把OpenHarmony的网络请求,彻底玩明白,玩出花来!🎉
📜 目录
-
第一章:启程之前 —— 我们的“兵器库”里都有啥?
- 1.1 官方钦点:
@ohos.net.http
模块概览 - 1.2 选兵点将:
http
vsfetch
,我该用哪个? - 1.3 环境配置:权限申请,万里长征第一步
- 1.1 官方钦点:
-
第二章:稳扎稳打 —— HTTP请求基础实战
- 2.1
GET
请求:从服务器“取”数据的艺术 - 2.2
POST
请求:把我们的“心意”送达服务器 - 2.3
PUT
&DELETE
等其他请求:做个全能的“指挥官” - 2.4 请求头(Headers):与服务器“对暗号”的正确姿势
- 2.5 解读响应(Response):不止有数据,还有“藏头诗”
- 2.1
-
第三章:数据“翻译官” —— 五花八门的数据解析实战
- 3.1 JSON:现代应用的事实标准
- 3.2 XML:宝刀未老的“老将”
- 3.3 表单数据(Form Data):提交信息的经典方式
- 3.4 文件上传与下载:处理二进制数据的“特种作战”
-
第四章:性能“加速器” —— 让你的App快到飞起!
- 4.1 缓存,缓存,还是缓存!
- 4.2 请求节流(Throttling)与防抖(Debouncing):别让请求把服务器“累死”
- 4.3 懒加载实战:在列表中按需请求数据
-
第五章:铜墙铁壁 —— 网络安全与异常处理
- 5.1 HTTPS:给你的数据通道“上把锁”
- 5.2 别裸奔了!Token认证的正确实践
- 5.3 优雅的异常处理:构建打不垮的网络层
- 5.4 日志与监控:成为能洞察一切的“鹰眼”
-
第六章:终极进化 —— 打造企业级的网络请求框架
- 6.1 为何要封装?从“游击队”到“正规军”
- 6.2 拦截器(Interceptors):请求的“中央处理器”
- 6.3 构建我们自己的
HttpClient
- 6.4 API模块化管理:让项目井井有条
-
第七章:终极实战 —— 从零到一开发“鸿蒙资讯”App
- 7.1 项目规划与API设计
- 7.2 搭建项目骨架
- 7.3 编写网络服务层
- 7.4 UI与数据联动
- 7.5 细节打磨与优化
-
第八章:未来已来 —— 探索网络新纪元
- 8.1 WebSocket:双向奔赴的实时通信
- 8.2 gRPC:面向未来的高性能RPC框架
-
💖 结语:路漫漫其修远兮,吾将上下而求索
第一章:启程之前 —— 我们的“兵器库”里都有啥?
俗话说,“工欲善其事,必先利其器”。在OpenHarmony里搞网络开发,我们首先得摸清楚手头有哪些“神兵利器”。
1.1 官方钦点:@ohos.net.http
模块概览
OpenHarmony为我们提供了一个核心模块——@ohos.net.http
。这玩意儿就是我们的“军火库”,里面包含了发起HTTP/HTTPS网络请求所有需要的东西。你只需要在你的.ets
文件顶部轻轻地 import http from '@ohos.net.http'
,整个网络世界的大门就向你敞开了。
它主要提供了创建HTTP请求实例、定义请求方法、处理请求和响应等一系列能力。简单来说,它就是我们一切网络操作的起点。
1.2 选兵点将:http
vs fetch
,我该用哪个?
很多从Web前端转过来的同学可能会问:“我能用熟悉的fetch
API吗?”。答案是:当然可以! OpenHarmony的ArkTS在很大程度上兼容Web标准,fetch
API 也是被支持的。
那么问题来了,官方的 http
模块和标准的 fetch
,我们该如何选择呢?别急,看我给你分析一波:
特性 | @ohos.net.http |
fetch API |
个人叨叨 |
---|---|---|---|
易用性 | 面向对象风格,createHttp() -> request() |
函数式,Promise链式调用 | fetch 写起来更“丝滑”,但http 更符合传统原生开发习惯 |
功能丰富度 | 功能更底层,提供更多定制选项(如超时、证书) | 高度封装,常用功能开箱即用 | http 像手动挡,fetch 像自动挡。想玩漂移?还得是手动挡! |
文件上传/下载 | 提供了专门的upload /download 任务API,支持进度监听 |
需要自己构建FormData 和处理Blob |
在复杂文件操作上,http 模块简直是“亲儿子”,优势巨大! |
平台整合度 | 与OS底层结合更紧密,未来扩展性更强 | Web标准,跨平台一致性好 | 如果你的应用深度依赖OS特性,http 模块是首选。 |
实战建议:
- 简单场景:如果你的应用只是做一些简单的GET/POST数据请求,
fetch
API写起来代码更少,更优雅。 - 复杂场景:如果你的应用需要精细化控制超时、处理HTTPS证书、或者进行带进度的文件上传下载,那么
@ohos.net.http
模块将是你最可靠的伙伴。
在这篇文章里,为了展示OpenHarmony的原生能力,我们将主要使用 @ohos.net.http
模块进行讲解,因为它更能体现平台的特性和深度。
1.3 环境配置:权限申请,万里长征第一步
别忘了!在OpenHarmony里,访问互联网是需要申请权限的,否则你的应用就是个“单机版”。这一步超级重要,但又经常被新手忽略。
打开你的项目,找到 src/main/module.json5
文件,在requestPermissions
数组里,加入网络权限:
{
"module": {
// ...其他配置
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
好了,加上这行“令牌”,你的App才被允许访问广阔的互联网世界。记住,每次写新App,先加权限!先加权限!先加权限! 重要的事情说三遍!不然调试半天发现是权限问题,真的会想“原地爆炸”💣。
第二章:稳扎稳打 —— HTTP请求基础实战
理论说完了,咱直接上代码!这一章,我们把常用的HTTP方法都过一遍,让你看到它们在真实代码里到底长啥样。
2.1 GET
请求:从服务器“取”数据的艺术
GET
请求,顾名思义,就是从服务器“获取”数据。比如获取一篇文章、一个商品列表等。
场景:获取一个笑话列表
// an_awesome_joke_page.ets
import http from '@ohos.net.http';
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct JokeListPage {
@State jokes: string[] = ['正在拼命加载笑话中... ✍️'];
// ArkTS推荐在aboutToAppear生命周期里发起首次网络请求
aboutToAppear() {
this.fetchJokes();
}
async fetchJokes() {
// 1. 获取一个httpRequest实例
let httpRequest = http.createHttp();
// 我们的目标API地址
const url = 'https://api.example.com/jokes?page=1&limit=10'; // 假设这是个获取笑话的API
try {
// 2. 发起请求,注意,这是一个异步操作,所以用await
let response = await httpRequest.request(url, {
method: http.RequestMethod.GET, // 明确指定GET方法
connectTimeout: 10000, // 连接超时时间,10秒
readTimeout: 10000, // 读取超时时间,10秒
header: {
'Content-Type': 'application/json',
'Custom-Header': 'Hello-OpenHarmony' // 还可以加点自定义的头
}
});
// 3. 处理响应
if (response.responseCode === 200) { // HTTP状态码200表示成功
// 服务器返回的结果在 response.result 中
let result = JSON.parse(response.result as string);
if (result.code === 0 && result.data.length > 0) {
this.jokes = result.data.map(item => item.content);
promptAction.showToast({ message: '笑话来啦!😂' });
} else {
this.jokes = ['服务器今天不想讲笑话... 😒'];
}
} else {
// 请求失败,比如404, 500等
this.jokes = [`出错啦!服务器返回状态码: ${response.responseCode}`];
console.error(`Request failed with code: ${response.responseCode}`);
}
} catch (err) {
// 捕获网络异常,比如没网、DNS解析失败等
this.jokes = ['网络开小差了,请检查网络连接!🛰️'];
console.error(`Network error: ${JSON.stringify(err)}`);
} finally {
// 4. 无论成功失败,都销毁请求实例,释放资源
httpRequest.destroy();
}
}
build() {
Column({ space: 10 }) {
Text('每日一笑').fontSize(30).fontWeight(FontWeight.Bold)
List({ space: 5 }) {
ForEach(this.jokes, (joke: string) => {
ListItem() {
Text(joke).width('90%').padding(10).backgroundColor(Color.White).borderRadius(15)
}
})
}.width('100%').layoutWeight(1)
}.padding(15).width('100%').height('100%').backgroundColor('#F1F3F5')
}
}
实战剖析:
- 异步处理:网络请求是耗时操作,必须用
async/await
异步处理,否则会阻塞UI线程,导致界面卡死。 - 生命周期:
aboutToAppear
是页面即将显示时触发的钩子,非常适合做数据初始化工作。 - 错误处理:
try...catch...finally
是网络请求的“黄金搭档”。try
里放正常逻辑,catch
捕获各种异常,finally
里做资源清理。这个结构必须牢记! - 状态管理:使用
@State
装饰器修饰的变量,当它的值改变时,UI会自动刷新。这就是ArkUI的魅力所在。
2.2 POST
请求:把我们的“心意”送达服务器
POST
请求通常用于向服务器“提交”数据,比如用户注册、发布文章、提交订单等。
场景:用户登录
// login_page.ets
import http from '@ohos.net.http';
import router from '@ohos.router';
@Component
struct LoginPage {
@State username: string = '';
@State password: string = '';
async handleLogin() {
if (!this.username || !this.password) {
// 做一些基本的前端校验
promptAction.showToast({ message: '用户名和密码不能为空!' });
return;
}
let httpRequest = http.createHttp();
const url = 'https://api.example.com/auth/login';
// 准备要发送的数据
const requestData = {
account: this.username,
secret: this.password
};
try {
let response = await httpRequest.request(url, {
method: http.RequestMethod.POST, // 方法改为POST
// 使用 extraData 字段来传递请求体
extraData: JSON.stringify(requestData),
header: {
// 告诉服务器我们发送的是JSON格式的数据
'Content-Type': 'application/json'
}
});
if (response.responseCode === 200) {
let result = JSON.parse(response.result as string);
if (result.code === 0) {
// 登录成功,假设服务器返回了token
const token = result.data.token;
// 把token存起来,比如用 @ohos.data.storage
// ... 存储token的代码 ...
promptAction.showToast({ message: '登录成功!欢迎回来!🎉' });
// 跳转到主页
router.replaceUrl({ url: 'pages/HomePage' });
} else {
// 业务逻辑错误,比如密码错误
promptAction.showToast({ message: `登录失败: ${result.message}` });
}
} else {
promptAction.showToast({ message: `服务器开小差了: ${response.responseCode}` });
}
} catch (err) {
promptAction.showToast({ message: '网络连接异常,请稍后再试!' });
console.error(`Login error: ${JSON.stringify(err)}`);
} finally {
httpRequest.destroy();
}
}
build() {
// ... 这里是登录页面的UI布局,包含输入框和按钮
// 按钮的onClick事件绑定到 this.handleLogin 方法
}
}
实战剖析:
extraData
:这是@ohos.net.http
模块中传递请求体(Request Body)的关键字段。你需要把你的数据对象(比如requestData
)转换成字符串(通常是JSON字符串)再赋值给它。Content-Type
:这个请求头非常重要!它告诉服务器你发送的数据是什么格式。如果是JSON.stringify
的,就用application/json
;如果是后面会讲到的表单数据,就用application/x-www-form-urlencoded
。服务器会根据这个头来解析你的数据。搞错了服务器就懵了!
2.3 PUT
& DELETE
等其他请求:做个全能的“指挥官”
除了GET
和POST
,PUT
(更新资源)和DELETE
(删除资源)也很常用。在@ohos.net.http
里使用它们简直不要太简单,就是改一下method
属性而已。
场景:更新用户信息(PUT
)
async updateUserInfo(userInfo) {
let httpRequest = http.createHttp();
try {
await httpRequest.request(`https://api.example.com/users/${userInfo.id}`, {
method: http.RequestMethod.PUT, // 看,就改这里!
extraData: JSON.stringify(userInfo),
header: { 'Content-Type': 'application/json' }
});
// ... 处理成功逻辑
} catch (err) {
// ... 处理失败逻辑
} finally {
httpRequest.destroy();
}
}
场景:删除一篇文章(DELETE
)
async deleteArticle(articleId) {
let httpRequest = http.createHttp();
try {
await httpRequest.request(`https://api.example.com/articles/${articleId}`, {
method: http.RequestMethod.DELETE // 还有这里!
});
// ... 处理成功逻辑
} catch (err) {
// ... 处理失败逻辑
} finally {
httpRequest.destroy();
}
}
看吧,是不是很简单?掌握了GET
和POST
,其他的HTTP方法就是举一反三的事情。
2.4 请求头(Headers):与服务器“对暗号”的正确姿势
请求头就像是接头暗号。除了Content-Type
,我们最常用的就是Authorization
,用来传递用户身份令牌(Token)。
// 假设你已经从Storage中获取了登录时保存的token
const token = AppStorage.Get<string>('userToken');
let response = await httpRequest.request(url, {
method: http.RequestMethod.GET,
header: {
'Content-Type': 'application/json',
// 在这里加上认证头
'Authorization': `Bearer ${token}` // Bearer是常用的Token类型
}
});
几乎所有需要登录才能访问的接口,都需要在请求头里带上这个“令牌”。服务器会校验这个令牌来确认你的身份。没有它?服务器只会冷冷地回你一个401 Unauthorized
(未授权)。
2.5 解读响应(Response):不止有数据,还有“藏头诗”
服务器的响应response
对象是个宝藏,别只盯着result
看。它里面还有很多有用的信息:
response.responseCode
: HTTP状态码。200
OK,201
Created,400
Bad Request,401
Unauthorized,403
Forbidden,404
Not Found,500
Internal Server Error… 这些都是你需要认识的“表情包”。response.header
: 响应头。服务器也会返回一堆头信息,比如Content-Type
、Content-Length
,有时还会有自定义的头,比如分页信息X-Total-Count
。response.cookies
: 服务器想在你本地种下的“小饼干”🍪。处理登录状态时可能会用到。
// ...请求代码...
let response = await httpRequest.request(url, { ... });
console.log(`服务器状态码: ${response.responseCode}`);
console.log(`响应头 Content-Type: ${response.header['Content-Type']}`); // 读取响应头
console.log(`服务器返回的数据: ${response.result}`);
if (response.header['X-Pagination-Total-Count']) {
const totalItems = parseInt(response.header['X-Pagination-Total-Count']);
console.log(`后台一共有 ${totalItems} 条数据!`);
}
学会解读完整的response
,能让你在调试时获得更多线索,更快地定位问题。
第三章:数据“翻译官” —— 五花八门的数据解析实战
从服务器拿回来的数据,通常是字符串格式。我们需要把它“翻译”成我们程序里能用的对象或数据结构。这一步,就是数据解析。
3.1 JSON:现代应用的事实标准
JSON(JavaScript Object Notation)因其轻量、易读、易解析的特性,已经成为API接口的首选数据格式。我们前面已经用过JSON.parse()
了,这里我们来深入一下。
健壮的JSON解析
直接JSON.parse()
很爽,但如果服务器返回的不是一个合法的JSON字符串(比如返回了一个HTML错误页面),你的程序就会直接崩溃!这绝对不能接受!
interface UserProfile {
id: number;
name: string;
avatar: string;
isActive: boolean;
}
function safeJsonParse<T>(jsonString: string, defaultValue: T): T {
if (typeof jsonString !== 'string') {
return defaultValue;
}
try {
return JSON.parse(jsonString) as T;
} catch (e) {
console.error('JSON解析失败!', e);
return defaultValue;
}
}
// ...在网络请求的回调里...
if (response.responseCode === 200) {
// 使用我们的安全解析函数,并提供一个默认值
const defaultProfile: UserProfile = { id: 0, name: '游客', avatar: 'default.png', isActive: false };
const userProfile = safeJsonParse<UserProfile>(response.result as string, defaultProfile);
this.profile = userProfile;
}
封装一个safeJsonParse
工具函数,是每个专业程序员的基本素养。它能让你的App在面对服务器异常数据时,依然保持稳定,而不是直接闪退。
3.2 XML:宝刀未老的“老将”
虽然JSON是主流,但你仍然可能遇到一些“老派”的系统(比如某些金融、气象接口)还在使用XML。OpenHarmony本身没有内置的XML解析库,但我们可以引入第三方库来解决。
假设我们通过npm
安装了一个名为fast-xml-parser
的库(这是一个真实存在的优秀库)。
# 在项目根目录执行
npm install fast-xml-parser
import { XMLParser } from 'fast-xml-parser'; // 引入第三方库
async function fetchWeatherDataFromXmlApi() {
const xmlData = `
<weather>
<city>深圳</city>
<temp>30</temp>
<condition>晴</condition>
</weather>
`; // 假设这是从服务器获取的XML字符串
const parser = new XMLParser();
try {
const weatherObj = parser.parse(xmlData);
console.log(`城市: ${weatherObj.weather.city}`); // 城市: 深圳
console.log(`温度: ${weatherObj.weather.temp}°C`); // 温度: 30°C
} catch(e) {
console.error('XML解析失败', e);
}
}
实践核心:处理非主流数据格式,善用社区的力量。npm
上有海量的库可以帮助我们解决各种棘手问题。不要重复造轮子!
3.3 表单数据(Form Data):提交信息的经典方式
除了JSON,POST
请求还常用application/x-www-form-urlencoded
格式提交数据,这就像我们以前填网页表单一样。
async submitForm() {
let httpRequest = http.createHttp();
const url = 'https://api.example.com/form-submit';
// 数据格式是 key1=value1&key2=value2
const formData = 'name=张三&age=25&from=OpenHarmony';
try {
let response = await httpRequest.request(url, {
method: http.RequestMethod.POST,
extraData: formData,
header: {
// Content-Type 必须是这个!
'Content-Type': 'application/x-www-form-urlencoded'
}
});
// ...
} catch (err) {
// ...
} finally {
httpRequest.destroy();
}
}
手动拼接字符串容易出错,特别是当值包含特殊字符时。一个好的实践是写一个工具函数来处理。
function objectToFormData(obj: Record<string, any>): string {
return Object.keys(obj)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
.join('&');
}
// 使用
const requestData = { name: '李四?', from: '鸿蒙&HarmonyOS' };
const formData = objectToFormData(requestData); // 会生成 "name=%E6%9D%8E%E5%9B%9B%3F&from=%E9%B8%BF%E8%92%99%26HarmonyOS"
encodeURIComponent
是关键!它能确保你的特殊字符(如?
、&
)被正确编码,不会干扰表单数据的结构。
3.4 文件上传与下载:处理二进制数据的“特种作战”
这绝对是实战中的重头戏!@ohos.net.http
为文件操作提供了专门的upload
和download
任务,非常强大。
场景:上传用户头像
import upload from '@ohos.request.upload';
import fs from '@ohos.file.fs';
async uploadAvatar(filePath: string) { // filePath 是文件的内部URI,如 'internal://cache/avatar.jpg'
// 1. 准备上传配置
const uploadConfig = {
url: 'https://api.example.com/upload/avatar',
header: {
Authorization: `Bearer ${AppStorage.Get<string>('userToken')}`
},
method: 'POST',
files: [
{
filename: 'avatar.jpg', // 服务器接收到的文件名
name: 'file', // 与后端约定的字段名,非常重要!
uri: filePath // 文件的本地URI
}
],
data: [
{
name: 'userId', // 还可以附带一些其他文本数据
value: '12345'
}
]
};
try {
// 2. 创建上传任务
const uploadTask = await upload.upload(getContext(), uploadConfig);
// 3. 监听上传进度 (太酷了!)
uploadTask.on('progress', (progress, total) => {
const percentage = Math.floor(progress / total * 100);
console.log(`上传进度: ${percentage}%`);
// 在这里更新你的UI,比如一个进度条
this.uploadProgress = percentage;
});
// 4. 监听上传完成事件
uploadTask.on('headerReceive', (header) => {
console.log('服务器响应头已接收:', header);
});
uploadTask.on('complete', () => {
console.log('上传任务完成!');
promptAction.showToast({ message: '头像上传成功!' });
});
// 5. 监听失败事件
uploadTask.on('fail', (err) => {
console.error('上传失败:', err);
promptAction.showToast({ message: '上传失败,请重试' });
});
} catch (err) {
console.error('创建上传任务失败:', err);
}
}
实战剖析:
@ohos.request.upload
: 这是专门负责上传的模块,别和http
搞混了。files
数组:name
字段必须和后端工程师确认好,他们会用这个name
来从请求中提取文件。- 事件监听:
on('progress', ...)
是这个API的精髓!它让我们可以轻松实现一个带进度的上传功能,极大提升用户体验。
场景:下载应用更新包
import download from '@ohos.request.download';
async downloadUpdatePackage(downloadUrl: string) {
// 获取应用的缓存目录,把文件下载到这里
const cacheDir = getContext().cacheDir;
const filePath = `${cacheDir}/update.hap`;
const downloadConfig = {
url: downloadUrl,
filePath: filePath, // 指定下载后文件保存的位置
enableMetered: true, // 是否允许在计费网络下下载
enableRoaming: true, // 是否允许在漫游时下载
header: { 'Custom-Header': 'Download-Request' }
};
try {
const downloadTask = await download.download(getContext(), downloadConfig);
downloadTask.on('progress', (receivedSize, totalSize) => {
const percentage = Math.floor(receivedSize / totalSize * 100);
console.log(`下载进度: ${percentage}%`);
this.downloadProgress = percentage;
});
downloadTask.on('complete', () => {
console.log(`下载完成!文件保存在: ${filePath}`);
// 在这里可以调用安装器进行安装
// ... installer.install(...)
});
downloadTask.on('fail', (err) => {
console.error('下载失败:', err);
});
} catch (err) {
console.error('创建下载任务失败:', err);
}
}
同样,下载任务也支持进度监听,这对于下载大文件来说是必备功能。
四、性能"加速器" —— 让你的App快到飞起!🚀
4.1 缓存策略:网络请求的"加速器"
在移动应用中,网络缓存不仅仅是提高性能,更是优化用户体验的关键。我们来设计一个强大的缓存系统!
4.1.1 内存缓存实现
class NetworkCache {
// 使用Map作为缓存存储
private cache: Map<string, {
data: any,
timestamp: number,
expiration: number
}> = new Map();
// 默认缓存时间:5分钟
private DEFAULT_EXPIRATION = 5 * 60 * 1000;
// 设置缓存
set(key: string, data: any, customExpiration?: number) {
const now = Date.now();
this.cache.set(key, {
data,
timestamp: now,
expiration: customExpiration || this.DEFAULT_EXPIRATION
});
}
// 获取缓存
get(key: string): any {
const cacheEntry = this.cache.get(key);
if (!cacheEntry) return null;
// 检查缓存是否过期
if (Date.now() - cacheEntry.timestamp > cacheEntry.expiration) {
this.cache.delete(key);
return null;
}
return cacheEntry.data;
}
// 清理过期缓存
cleanup() {
const now = Date.now();
this.cache.forEach((value, key) => {
if (now - value.timestamp > value.expiration) {
this.cache.delete(key);
}
});
}
}
// 全局缓存实例
const networkCache = new NetworkCache();
4.1.2 带缓存的网络请求封装
class CachedHttpClient {
private cache: NetworkCache;
constructor() {
this.cache = new NetworkCache();
}
async get(url: string, options?: RequestOptions) {
// 先检查缓存
const cachedData = this.cache.get(url);
if (cachedData) {
return cachedData;
}
// 缓存未命中,发起网络请求
try {
const httpRequest = http.createHttp();
const response = await httpRequest.request(url, options);
if (response.responseCode === 200) {
const data = JSON.parse(response.result as string);
// 将数据写入缓存
this.cache.set(url, data);
return data;
}
} catch (error) {
console.error('Cached request failed', error);
}
return null;
}
}
4.2 请求节流与防抖:驯服狂野的网络请求 🏎️
4.2.1 节流(Throttle)实现
function throttle<T extends (...args: any[]) => any>(
func: T,
delay: number
): T {
let lastCall = 0;
return function(this: any, ...args: Parameters<T>): ReturnType<T> {
const now = new Date().getTime();
if (now - lastCall < delay) {
return undefined as ReturnType<T>;
}
lastCall = now;
return func.apply(this, args);
} as T;
}
// 使用示例:限制搜索请求频率
class SearchComponent {
@State searchText: string = '';
// 每500ms只允许发起一次搜索请求
private throttledSearch = throttle(async (query: string) => {
await this.performSearch(query);
}, 500);
async performSearch(query: string) {
// 真正的搜索逻辑
const result = await this.searchService.search(query);
// 更新搜索结果
}
build() {
Search()
.onSubmit((value: string) => {
this.throttledSearch(value);
})
}
}
4.2.2 防抖(Debounce)实现
function debounce<T extends (...args: any[]) => any>(
func: T,
delay: number
): T {
let timeoutId: number;
return function(this: any, ...args: Parameters<T>): void {
// 清除上一个定时器
clearTimeout(timeoutId);
// 设置新的定时器
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay) as unknown as number;
} as T;
}
// 使用场景:自动完成输入
class AutoCompleteInput {
@State inputValue: string = '';
// 防抖的自动完成请求
private debouncedFetchSuggestions = debounce(async (query: string) => {
if (query.length > 2) {
const suggestions = await this.fetchSuggestions(query);
this.updateSuggestions(suggestions);
}
}, 300);
build() {
TextInput()
.onChange((value: string) => {
this.inputValue = value;
this.debouncedFetchSuggestions(value);
})
}
}
4.3 懒加载:智能数据获取 🍃
4.3.1 分页列表的懒加载
@Component
struct InfiniteScrollList {
@State items: any[] = [];
@State currentPage: number = 1;
@State hasMore: boolean = true;
private pageSize: number = 20;
async loadMoreItems() {
if (!this.hasMore) return;
try {
const newItems = await this.fetchPagedData(this.currentPage, this.pageSize);
if (newItems.length < this.pageSize) {
this.hasMore = false;
}
this.items = [...this.items, ...newItems];
this.currentPage++;
} catch (error) {
console.error('加载更多数据失败', error);
}
}
async fetchPagedData(page: number, pageSize: number): Promise<any[]> {
const url = `https://api.example.com/items?page=${page}&pageSize=${pageSize}`;
const response = await http.createHttp().request(url);
return JSON.parse(response.result as string).data;
}
build() {
List() {
ForEach(this.items, (item) => {
ListItem() {
// 渲染每一项
}
})
if (this.hasMore) {
ListItem() {
Text('正在加载更多...')
.onAppear(() => this.loadMoreItems())
}
}
}
}
}
4.3.2 图片懒加载优化
@Component
struct LazyLoadImage {
@State imageUrl: string = '';
@State isLoaded: boolean = false;
private loadImage() {
// 使用Promise模拟图片加载
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => {
this.isLoaded = true;
resolve(true);
};
image.onerror = reject;
image.src = this.imageUrl;
});
}
build() {
Stack() {
if (!this.isLoaded) {
// 占位符或加载动画
Text('加载中...')
}
Image(this.imageUrl)
.visibility(this.isLoaded ? Visibility.Visible : Visibility.None)
.onAppear(() => this.loadImage())
}
}
}
五、铜墙铁壁 —— 网络安全与异常处理 🛡️
5.1 HTTPS安全通信
class SecureHttpClient {
private static async verifyServerCertificate(cert: any): boolean {
// 实现证书验证逻辑
// 检查证书有效期、颁发机构等
return true; // 简化示例
}
async secureRequest(url: string, options: RequestOptions) {
try {
const httpRequest = http.createHttp();
// 配置SSL/TLS
options.sslVerify = true; // 开启证书验证
options.caPath = ''; // 可以指定受信任的CA证书路径
const response = await httpRequest.request(url, options);
// 额外的证书验证
if (!await SecureHttpClient.verifyServerCertificate(response.certificate)) {
throw new Error('不受信任的服务器证书');
}
return response;
} catch (error) {
// 处理SSL/TLS相关错误
console.error('安全连接建立失败', error);
throw error;
}
}
}
5.2 Token认证与安全存储
class AuthService {
private static TOKEN_KEY = 'user_auth_token';
// 安全存储Token
static async saveToken(token: string) {
try {
// 使用加密存储
await this.encryptAndSaveToken(token);
} catch (error) {
console.error('Token存储失败', error);
}
}
// 获取Token
static async getToken(): Promise<string | null> {
try {
return await this.decryptToken();
} catch (error) {
console.error('获取Token失败', error);
return null;
}
}
// 添加Token到请求头
static async appendTokenToRequest(request: RequestOptions) {
const token = await this.getToken();
if (token) {
request.header = {
...request.header,
'Authorization': `Bearer ${token}`
};
}
}
}
// 使用示例
class SecureNetworkClient {
async fetchProtectedResource() {
const url = 'https://api.example.com/protected';
const options: RequestOptions = { method: http.RequestMethod.GET };
// 自动添加Token
await AuthService.appendTokenToRequest(options);
const response = await http.createHttp().request(url, options);
// 处理响应...
}
}
5.3 全局网络错误处理
enum NetworkErrorType {
TIMEOUT = 'TIMEOUT',
NO_NETWORK = 'NO_NETWORK',
SERVER_ERROR = 'SERVER_ERROR',
UNAUTHORIZED = 'UNAUTHORIZED'
}
class NetworkErrorHandler {
static handle(error: any, type: NetworkErrorType) {
switch (type) {
case NetworkErrorType.TIMEOUT:
this.showToast('网络请求超时,请检查网络');
break;
case NetworkErrorType.NO_NETWORK:
this.showDialog('无网络连接', '请检查您的网络设置');
break;
case NetworkErrorType.SERVER_ERROR:
this.reportToMonitorSystem(error);
break;
case NetworkErrorType.UNAUTHORIZED:
this.handleUnauthorized();
break;
}
}
private static showToast(message: string) {
// 显示Toast提示
}
private static showDialog(title: string, message: string) {
// 显示对话框
}
private static reportToMonitorSystem(error: any) {
// 上报错误到监控系统
}
private static handleUnauthorized() {
// 处理未授权,如跳转登录页
router.replaceUrl({ url: 'pages/Login' });
}
}
5.4 网络请求日志与监控
class NetworkLogger {
private static logs: Array<{
url: string,
method: string,
timestamp: number,
status: number,
duration: number
}> = [];
static log(request: {
url: string,
method: string,
startTime: number,
response: any
}) {
const endTime = Date.now();
const logEntry = {
url: request.url,
method: request.method,
timestamp: endTime,
status: request.response.responseCode,
duration: endTime - request.startTime
};
this.logs.push(logEntry);
// 如果日志超过100条,移除最早的
if (this.logs.length > 100) {
this.logs.shift();
}
// 可选:上传到服务器进行分析
this.uploadLogsIfNeeded();
}
private static uploadLogsIfNeeded() {
// 实现日志上传逻辑
}
static getLogs() {
return this.logs;
}
}
// 使用示例
class EnhancedHttpClient {
async request(url: string, options: RequestOptions) {
const startTime = Date.now();
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(url, options);
// 记录日志
NetworkLogger.log({
url,
method: options.method,
startTime,
response
});
return response;
} catch (error) {
// 错误处理
NetworkErrorHandler.handle(error, NetworkErrorType.SERVER_ERROR);
throw error;
}
}
}
六、企业级网络请求框架设计 🏗️
6.1 为什么需要封装?从"游击队"到"正规军"
在实际的企业级开发中,我们的网络请求需要具备以下特征:
- 统一的错误处理
- 自动的loading管理
- 灵活的拦截器机制
- 可配置的重试策略
- 智能的缓存机制
6.2 企业级HttpClient设计
// 定义请求配置接口
interface HttpClientConfig {
baseURL?: string;
timeout?: number;
headers?: Record<string, string>;
retryCount?: number;
cache?: boolean;
}
// 请求拦截器接口
interface Interceptor {
request?(config: RequestConfig): RequestConfig;
response?(response: Response): Response;
error?(error: Error): Error;
}
class EnterpriseHttpClient {
private config: HttpClientConfig;
private interceptors: Interceptor[] = [];
private cache: Map<string, any> = new Map();
constructor(config?: HttpClientConfig) {
this.config = {
baseURL: '',
timeout: 10000,
retryCount: 3,
cache: false,
...config
};
}
// 添加拦截器
use(interceptor: Interceptor) {
this.interceptors.push(interceptor);
return this;
}
// 执行请求拦截器
private async executeRequestInterceptors(config: RequestConfig) {
return this.interceptors.reduce(
(config, interceptor) =>
interceptor.request ? interceptor.request(config) : config,
config
);
}
// 执行响应拦截器
private async executeResponseInterceptors(response: Response) {
return this.interceptors.reduce(
(response, interceptor) =>
interceptor.response ? interceptor.response(response) : response,
response
);
}
// 核心请求方法
async request<T>(
method: string,
url: string,
data?: any,
config?: Partial<HttpClientConfig>
): Promise<T> {
const mergedConfig = { ...this.config, ...config };
const fullUrl = `${mergedConfig.baseURL}${url}`;
// 检查缓存
if (mergedConfig.cache) {
const cachedResponse = this.cache.get(fullUrl);
if (cachedResponse) return cachedResponse;
}
// 准备请求配置
const requestConfig: RequestConfig = {
method,
url: fullUrl,
data,
headers: mergedConfig.headers,
timeout: mergedConfig.timeout
};
// 执行请求拦截器
const processedConfig = await this.executeRequestInterceptors(requestConfig);
// 重试机制
const executeRequest = async (retriesLeft: number): Promise<T> => {
try {
const httpRequest = http.createHttp();
const response = await httpRequest.request(processedConfig.url, {
method: processedConfig.method,
extraData: processedConfig.data,
header: processedConfig.headers
});
// 执行响应拦截器
const processedResponse = await this.executeResponseInterceptors(response);
// 缓存响应
if (mergedConfig.cache) {
this.cache.set(fullUrl, processedResponse);
}
return processedResponse as T;
} catch (error) {
if (retriesLeft > 0) {
return executeRequest(retriesLeft - 1);
}
throw error;
}
};
return executeRequest(mergedConfig.retryCount || 0);
}
// 便捷方法
get<T>(url: string, config?: Partial<HttpClientConfig>): Promise<T> {
return this.request<T>('GET', url, null, config);
}
post<T>(url: string, data: any, config?: Partial<HttpClientConfig>): Promise<T> {
return this.request<T>('POST', url, data, config);
}
// 其他方法:put, delete等
}
// 使用示例
const httpClient = new EnterpriseHttpClient({
baseURL: 'https://api.example.com',
timeout: 5000
});
// 添加鉴权拦截器
httpClient.use({
request: (config) => {
const token = AppStorage.Get<string>('userToken');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
}
});
// 添加错误处理拦截器
httpClient.use({
response: (response) => {
if (response.responseCode !== 200) {
throw new Error(`Request failed with status ${response.responseCode}`);
}
return response;
}
});
6.3 API模块化管理
// 定义API服务基类
abstract class BaseApiService {
protected httpClient: EnterpriseHttpClient;
constructor(httpClient: EnterpriseHttpClient) {
this.httpClient = httpClient;
}
}
// 用户相关API
class UserApiService extends BaseApiService {
async getUserProfile(userId: string) {
return this.httpClient.get<UserProfile>(`/users/${userId}`);
}
async updateUserProfile(userId: string, profile: Partial<UserProfile>) {
return this.httpClient.post<UserProfile>(`/users/${userId}`, profile);
}
}
// 文章相关API
class ArticleApiService extends BaseApiService {
async getArticleList(page: number, pageSize: number) {
return this.httpClient.get<Article[]>('/articles', {
params: { page, pageSize }
});
}
async createArticle(article: Article) {
return this.httpClient.post<Article>('/articles', article);
}
}
// 统一API管理
class ApiServiceManager {
private static instance: ApiServiceManager;
private httpClient: EnterpriseHttpClient;
private constructor() {
this.httpClient = new EnterpriseHttpClient({
baseURL: 'https://api.example.com/v1'
});
}
static getInstance(): ApiServiceManager {
if (!this.instance) {
this.instance = new ApiServiceManager();
}
return this.instance;
}
get users(): UserApiService {
return new UserApiService(this.httpClient);
}
get articles(): ArticleApiService {
return new ArticleApiService(this.httpClient);
}
}
// 使用示例
async function fetchUserArticles() {
const apiManager = ApiServiceManager.getInstance();
try {
const userProfile = await apiManager.users.getUserProfile('123');
const articles = await apiManager.articles.getArticleList(1, 10);
// 处理数据
} catch (error) {
// 统一错误处理
}
}
七、实战项目:开发"鸿蒙资讯"App 🚀
7.1 项目架构设计
// 项目目录结构
// src
// ├── api // API服务
// ├── components // 公共组件
// ├── models // 数据模型
// ├── pages // 页面
// ├── stores // 状态管理
// └── utils // 工具类
// 数据模型定义
interface NewsArticle {
id: string;
title: string;
content: string;
author: string;
publishTime: number;
coverImage?: string;
category: string;
}
// API服务
class NewsApiService extends BaseApiService {
async getNewsList(
page: number,
pageSize: number,
category?: string
): Promise<{
list: NewsArticle[],
total: number
}> {
return this.httpClient.get('/news', {
params: { page, pageSize, category }
});
}
async getNewsDetail(id: string): Promise<NewsArticle> {
return this.httpClient.get(`/news/${id}`);
}
}
// 状态管理 (使用MobX风格)
class NewsStore {
@observable articles: NewsArticle[] = [];
@observable loading: boolean = false;
@observable error: string | null = null;
private newsApiService: NewsApiService;
constructor(apiService: NewsApiService) {
this.newsApiService = apiService;
}
@action
async fetchNewsList(page: number, category?: string) {
this.loading = true;
this.error = null;
try {
const result = await this.newsApiService.getNewsList(page, 10, category);
this.articles = result.list;
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
}
}
// 新闻列表页面
@Component
struct NewsListPage {
private newsStore: NewsStore;
build() {
List() {
ForEach(this.newsStore.articles, (article: NewsArticle) => {
ListItem() {
NewsArticleItem({ article })
}
})
}
.onReachEnd(() => {
// 触发加载更多
this.newsStore.fetchNewsList(/* 下一页 */);
})
}
}
// 新闻详情页面
@Component
struct NewsDetailPage {
@State article: NewsArticle | null = null;
async aboutToAppear() {
// 获取文章详情
const articleId = router.getParams()['id'];
this.article = await newsApiService.getNewsDetail(articleId);
}
build() {
// 渲染文章详情
}
}
7.2 性能优化与离线支持
// 离线缓存管理
class OfflineCacheManager {
private static CACHE_KEY = 'NEWS_OFFLINE_CACHE';
static async cacheArticles(articles: NewsArticle[]) {
try {
await storage.set(this.CACHE_KEY, JSON.stringify(articles));
} catch (error) {
console.error('缓存文章失败', error);
}
}
static async getCachedArticles(): Promise<NewsArticle[]> {
try {
const cachedData = await storage.get(this.CACHE_KEY);
return cachedData ? JSON.parse(cachedData) : [];
} catch (error) {
return [];
}
}
}
// 网络状态监听
class NetworkStatusManager {
@observable isOnline: boolean = true;
constructor() {
this.initNetworkListener();
}
private initNetworkListener() {
network.on('typeChange', (type) => {
this.isOnline = type !== network.ConnectionType.NONE;
});
}
}
7.3 安全与性能优化
// 数据脱敏处理
function desensitizeData<T>(data: T): T {
if (typeof data === 'object' && data !== null) {
const sensitiveKeys = ['password', 'token', 'secret'];
return Object.keys(data).reduce((acc, key) => {
if (sensitiveKeys.includes(key)) {
acc[key] = '***';
} else {
acc[key] = data[key];
}
return acc;
}, {} as T);
}
return data;
}
// 性能监控装饰器
function performanceTrack() {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const start = performance.now();
try {
const result = await originalMethod.apply(this, args);
const end = performance.now();
console.log(`Method ${propertyKey} took ${end - start}ms`);
return result;
} catch (error) {
// 上报错误
console.error(`Method ${propertyKey} failed`, error);
throw error;
}
};
return descriptor;
};
}
🌟 写在最后
亲爱的开发者,网络请求不仅仅是技术,更是连接用户与世界的桥梁。每一个请求,都承载着用户的期待;每一次响应,都是体验的开始。
OpenHarmony为我们提供了强大的工具,但真正的魔法,永远来自于你的想象力和创造力。无论是构建简单的应用,还是开发复杂的企业级系统,相信现在的你已经有了坚实的基础。
💕 鼓励与期待
function encouragement() {
console.log('你已经不再是当初的菜鸟了!');
console.log('现在,去创造属于你的精彩!');
return '未来,大大大大大大大大有可能!🚀';
}
Keep Coding, Keep Dreaming! 💖🌈
👩💻 关于作者:一个永远在成长的程序员,用代码丈量世界的广度与深度!
📚 学习资源推荐
- OpenHarmony官方文档
- 开发者社区
- 技术博客
🎉 彩蛋:你已经不仅仅是一个开发者,你是一个用代码改变世界的艺术家!
本文参与华为云社区【内容共创】活动第28期。
任务4:OpenHarmony应用开发之网络数据请求与数据解析
- 点赞
- 收藏
- 关注作者
评论(0)