【云驻共创】把网络请求玩出花来:OpenHarmony应用开发中的数据交互终极实战指南,你确定不点进来看看?

举报
bug菌 发表于 2025/06/16 21:36:12 2025/06/16
【摘要】 💖 前言:嘿,还记得被网络请求支配的恐惧吗?哈喽,各位奋战在一线的代码英雄们!👋 我是你们的老朋友,一个热爱生活、更热爱Coding的普通程序员。今天,咱们不聊风花雪月,就聊点硬核的、能让你在项目里横着走的干货——OpenHarmony应用开发中的网络数据请求与数据解析。想当年,我还是个萌新的时候,面对网络请求,那叫一个头大啊!🤯 啥是GET,啥是POST?跨域是啥玩意儿?为啥我的请...

💖 前言:嘿,还记得被网络请求支配的恐惧吗?

哈喽,各位奋战在一线的代码英雄们!👋 我是你们的老朋友,一个热爱生活、更热爱Coding的普通程序员。今天,咱们不聊风花雪月,就聊点硬核的、能让你在项目里横着走的干货——OpenHarmony应用开发中的网络数据请求与数据解析

想当年,我还是个萌新的时候,面对网络请求,那叫一个头大啊!🤯 啥是GET,啥是POST?跨域是啥玩意儿?为啥我的请求发出去了,服务器那边却说“你谁啊”?为啥拿到的数据是一堆看不懂的“天书”?还有那该死的undefined is not a function… 简直是午夜梦回的“惊魂一刻”!

但是!朋友们,时代变了!随着OpenHarmony的崛起,它给我们开发者提供了一套堪称“保姆级”的网络API。它不仅强大,而且优雅。今天,我就将我这几年踩过的坑、总结的经验、压箱底的宝贝,毫无保留地分享给大家。这篇文章的目标只有一个:让你从网络请求的小白,一跃成为同事眼中的“网络大神”。我们将从最基础的API讲起,一步步深入到封装、缓存、安全、异常处理,最后用一个完整的实战项目,把所有知识点串起来。

这篇文章会很长,但相信我,绝对值得你泡上一杯咖啡☕,慢慢品味。准备好了吗?让我们一起,把OpenHarmony的网络请求,彻底玩明白,玩出花来!🎉

📜 目录

  • 第一章:启程之前 —— 我们的“兵器库”里都有啥?

    • 1.1 官方钦点:@ohos.net.http 模块概览
    • 1.2 选兵点将:http vs fetch,我该用哪个?
    • 1.3 环境配置:权限申请,万里长征第一步
  • 第二章:稳扎稳打 —— HTTP请求基础实战

    • 2.1 GET 请求:从服务器“取”数据的艺术
    • 2.2 POST 请求:把我们的“心意”送达服务器
    • 2.3 PUT & DELETE 等其他请求:做个全能的“指挥官”
    • 2.4 请求头(Headers):与服务器“对暗号”的正确姿势
    • 2.5 解读响应(Response):不止有数据,还有“藏头诗”
  • 第三章:数据“翻译官” —— 五花八门的数据解析实战

    • 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 等其他请求:做个全能的“指挥官”

除了GETPOSTPUT(更新资源)和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();
  }
}

看吧,是不是很简单?掌握了GETPOST,其他的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-TypeContent-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为文件操作提供了专门的uploaddownload任务,非常强大。

场景:上传用户头像

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应用开发之网络数据请求与数据解析

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。