uni-app x跨平台开发实战:鸿蒙HarmonyOS网络模块封装与轮播图实现

uni-app x跨平台开发实战:鸿蒙HarmonyOS网络模块封装与轮播图实现
在玩中学,直接上手实战是猫哥一贯的自学方法心得。假期期间实在无聊!我不睡懒觉、不看电影、也不刷手机、不玩游戏、也无处可去。那么我干嘛嘞?闲的都想看蚂蚁上树,无聊透顶,百无聊赖,感觉假期好没意思啊。做什么呢? 于是翻出来之前做过的“爱影家”影视app项目,找个跨多端的技术栈再玩一把。

我先后尝试了kuikly、flutter 、arkui-x等框架,结果…,额,这几个没少踩坑做不动了。真想向天问一下,跨平台框架开发哪家强?最后尝试了下uni-app x,这个还真不错,就选它了,用它来实现个跨多端的免费观影APP分享给大家。

本文内容介绍uni-app x框架的网络请求和组件复用,这是每个开发者必须掌握的技能。本文将通过 uni-app x 框架,结合uni-app x独有的 UTS 语言规范,实践如何构建规范的网络请求模块,并实现动态轮播图组件。我们选用的案例是影视类应用的首页轮播图实现,接口来源于真实的开放 API。

关于uniapp-x的介绍:
可以体验打包后的hello uni-app x这个demo项目,地址:https://hellouniappx.dcloud.net.cn/

可以看到组件很全面啊,我先后体验了android端,鸿蒙端和小程序端,界面UI效果一致,且鸿蒙端运行相当流畅。可以看到组件还是很丰富的。浏览器端的体检们可以直接访问:https://hellouniappx.dcloud.net.cn/web#/pages/component/view/view

UTS语法介绍:https://doc.dcloud.net.cn/uni-app-x/uts/

鸿蒙next手机端的体验 uni-app x:

使用鸿蒙next手机的应用商店,搜索“DCloud开发者中心系统”可以下载安装体验。据说渲染速度超过了原生写法,你说牛不牛吧?

一、网络请求模块的 UTS 封装

1.1 核心类型定义

// 定义符合 HTTP 标准的请求方法typeHttpMethod="GET"|"POST"|"PUT"|"DELETE"|"OPTIONS"|"HEAD"// 请求配置对象(注意 UTS 的类型系统要求)exporttypeRequestOptions={ url :string method : HttpMethod data :any|null headers : Map<string,string>}// 标准化响应结构exporttypeResponseData<T>={ code :number message :string data :T}

技术要点

  • UTS 要求显式类型声明,不支持 TS 的类型推断
  • 使用 Map 类型存储 headers 保证类型安全
  • 泛型结构保持接口响应的灵活性

1.2 Request 类实现

exportclassRequest{private baseUrl :stringprivate headers : Map<string,string>constructor(baseUrl :string){this.baseUrl = baseUrl this.headers =newMap<string,string>()this.headers.set('Content-Type','application/json')this.headers.set('accept','application/json')}// 请求头合并方法(转换 Map 为 any)privatebuildHeader(customHeaders : Map<string,string>):any{const merged :any={}this.headers.forEach((value :string, key :string)=>{ merged[key]= value }) customHeaders.forEach((value :string, key :string)=>{ merged[key]= value })return merged }// 核心请求方法request<T>(options : RequestOptions):Promise<ResponseData<T>>{const header =this.buildHeader(options.headers)returnnewPromise<ResponseData<T>>((resolve, reject)=>{ uni.request({ url:this.baseUrl + options.url, method: options.method, data: options.data, header: header,success:(res)=>{if(res.statusCode ===200){resolve(res.data as ResponseData<T>)}else{reject(newError(`HTTP ${res.statusCode}`))}},fail:(err)=>{reject(newError(err.errMsg))}})})}// 快捷方法示例get<T>(url:string, params?:any, headers?: Map<string,string>){returnthis.request<T>({ url: url +(params ?`?${newURLSearchParams(params)}`:''), method:'GET', data:null, headers: headers ??newMap<string,string>()})}}

关键实现细节

  • 内置默认请求头设置
  • 使用 Promise 封装异步请求
  • 状态码标准化处理
  • URL 参数自动拼接(GET 请求)
  • 类型断言保证数据安全

1.3 完整封装代码

request.uts

/** * file:request.uts * bref:uniapp-x 网络请求模块封装 * author:yangyongzhen * date:2026-0224 23:11 * qq:534117529 */typeHttpMethod="GET"|"POST"|"PUT"|"DELETE"|"OPTIONS"|"HEAD"exporttypeRequestOptions={ url :string method : HttpMethod data :any|null headers : Map<string,string>}exporttypeResponseData<T>={ code :number message :string data :T}exportclassRequest{private baseUrl :stringprivate headers : Map<string,string>constructor(baseUrl :string){this.baseUrl = baseUrl this.headers =newMap<string,string>()this.headers.set('Content-Type','application/json')this.headers.set('accept','application/json')}// 合并默认头与自定义头,转为 any 对象传给 uni.requestprivatebuildHeader(customHeaders : Map<string,string>):any{const merged :any={}this.headers.forEach((value :string, key :string)=>{ merged[key]= value }) customHeaders.forEach((value :string, key :string)=>{ merged[key]= value })return merged }request<T>(options : RequestOptions):Promise<ResponseData<T>>{const url =this.baseUrl + options.url const header =this.buildHeader(options.headers)returnnewPromise<ResponseData<T>>((resolve, reject)=>{ uni.request({ url: url, method: options.method, data: options.data, header: header,success:(res)=>{if(res.statusCode ===200){resolve(res.data as ResponseData<T>)}else{reject(newError(`Request failed: ${res.statusCode}`))}},fail:(err)=>{reject(newError(err.errMsg))}})})}get<T>(url :string, params :any|null, headers : Map<string,string>|null):Promise<ResponseData<T>>{returnthis.request<T>({ url: url, method:'GET', data: params, headers: headers ??newMap<string,string>()})}post<T>(url :string, data :any|null, headers : Map<string,string>|null):Promise<ResponseData<T>>{returnthis.request<T>({ url: url, method:'POST', data: data, headers: headers ??newMap<string,string>()})}put<T>(url :string, data :any|null, headers : Map<string,string>|null):Promise<ResponseData<T>>{returnthis.request<T>({ url: url, method:'PUT', data: data, headers: headers ??newMap<string,string>()})}delete<T>(url :string, data :any|null, headers : Map<string,string>|null):Promise<ResponseData<T>>{returnthis.request<T>({ url: url, method:'DELETE', data: data, headers: headers ??newMap<string,string>()})}}exportconst request =newRequest('http://49.235.52.102:8000/api/v1')

1.4 进一步封装,增加支持拦截器

上述封装完成后,可以满足基础的使用。但是在实际项目中,我们常需要有个拦截器的实现,这样有个好处是可以对请求和响应进行拦截或日志打印等,这样可以支持token 注入、日志、统一错误处理等扩展。

/** * file:request.uts * bref:uniapp-x 网络请求模块封装 * author:yangyongzhen * date:2026-0224 23:11 * qq:534117529 */typeHttpMethod="GET"|"POST"|"PUT"|"DELETE"|"OPTIONS"|"HEAD"exporttypeRequestOptions={ url :string method : HttpMethod data :any|null headers : Map<string,string>}exporttypeResponseData<T>={ code :number message :string data :T}// 请求拦截器:接收 RequestOptions,返回修改后的 RequestOptionsexporttypeRequestInterceptorFn=(options : RequestOptions)=> RequestOptions // 响应拦截器:接收原始响应 any,返回处理后的 anyexporttypeResponseInterceptorFn=(response :any)=>anyexportclassRequest{private baseUrl :stringprivate headers : Map<string,string>// 拦截器存储:Map<自增ID, 拦截器函数>,保证按注册顺序执行且移除不影响其他private requestInterceptors : Map<number, RequestInterceptorFn>private responseInterceptors : Map<number, ResponseInterceptorFn>private requestInterceptorId :numberprivate responseInterceptorId :numberconstructor(baseUrl :string){this.baseUrl = baseUrl this.headers =newMap<string,string>()this.headers.set('Content-Type','application/json')this.headers.set('accept','application/json')this.requestInterceptors =newMap<number, RequestInterceptorFn>()this.responseInterceptors =newMap<number, ResponseInterceptorFn>()this.requestInterceptorId =0this.responseInterceptorId =0}// ---- 拦截器注册 / 移除 ----/** * 注册请求拦截器,返回 ID(可用于移除) */addRequestInterceptor(fn : RequestInterceptorFn):number{this.requestInterceptorId +=1const id =this.requestInterceptorId this.requestInterceptors.set(id, fn)return id }removeRequestInterceptor(id :number):void{this.requestInterceptors.delete(id)}/** * 注册响应拦截器,返回 ID(可用于移除) */addResponseInterceptor(fn : ResponseInterceptorFn):number{this.responseInterceptorId +=1const id =this.responseInterceptorId this.responseInterceptors.set(id, fn)return id }removeResponseInterceptor(id :number):void{this.responseInterceptors.delete(id)}// ---- 拦截器链执行 ----// 收集 Map 中所有 ID 并升序排序,保证按注册先后顺序执行privaterunRequestInterceptors(options : RequestOptions): RequestOptions {const ids :Array<number>=[]this.requestInterceptors.forEach((_ : RequestInterceptorFn, id :number)=>{ ids.push(id)}) ids.sort((a :number, b :number):number=> a - b)let current = options for(let i =0; i < ids.length; i++){const fn =this.requestInterceptors.get(ids[i])if(fn !=null){ current =fn(current)}}return current }privaterunResponseInterceptors(response :any):any{const ids :Array<number>=[]this.responseInterceptors.forEach((_ : ResponseInterceptorFn, id :number)=>{ ids.push(id)}) ids.sort((a :number, b :number):number=> a - b)let current :any= response for(let i =0; i < ids.length; i++){const fn =this.responseInterceptors.get(ids[i])if(fn !=null){ current =fn(current)}}return current }// ---- 合并请求头 ----privatebuildHeader(customHeaders : Map<string,string>):any{const merged :any={}this.headers.forEach((value :string, key :string)=>{ merged[key]= value }) customHeaders.forEach((value :string, key :string)=>{ merged[key]= value })return merged }// ---- 核心请求方法 ----request<T>(options : RequestOptions):Promise<ResponseData<T>>{// 1. 执行请求拦截器链const interceptedOptions =this.runRequestInterceptors(options)// url 已带协议头时直接使用,否则拼接实例默认 baseUrlconst url = interceptedOptions.url.startsWith('http://')|| interceptedOptions.url.startsWith('https://')? interceptedOptions.url :this.baseUrl + interceptedOptions.url const header =this.buildHeader(interceptedOptions.headers)returnnewPromise<ResponseData<T>>((resolve, reject)=>{ uni.request({ url: url, method: interceptedOptions.method, data: interceptedOptions.data, header: header,success:(res)=>{if(res.statusCode ===200){// 2. 执行响应拦截器链const processed =this.runResponseInterceptors(res.data)resolve(processed as ResponseData<T>)}else{reject(newError(`Request failed: ${res.statusCode}`))}},fail:(err)=>{reject(newError(err.errMsg))}})})}get<T>(url :string, params :any|null, headers : Map<string,string>|null):Promise<ResponseData<T>>{returnthis.request<T>({ url: url, method:'GET', data: params, headers: headers ??newMap<string,string>()})}post<T>(url :string, data :any|null, headers : Map<string,string>|null):Promise<ResponseData<T>>{returnthis.request<T>({ url: url, method:'POST', data: data, headers: headers ??newMap<string,string>()})}put<T>(url :string, data :any|null, headers : Map<string,string>|null):Promise<ResponseData<T>>{returnthis.request<T>({ url: url, method:'PUT', data: data, headers: headers ??newMap<string,string>()})}delete<T>(url :string, data :any|null, headers : Map<string,string>|null):Promise<ResponseData<T>>{returnthis.request<T>({ url: url, method:'DELETE', data: data, headers: headers ??newMap<string,string>()})}}exportconst request =newRequest('http://49.235.52.102:8000/api/v1')// 全局请求拦截器:举例打印请求 URL request.addRequestInterceptor((options : RequestOptions): RequestOptions =>{console.log(`[Request] ${options.method}${options.url}`)return options })// 全局响应拦截器:举例打印响应码和 message request.addResponseInterceptor((response :any):any=>{const res = response as ResponseData<any>console.log(`[Response] code=${res.code} message=${res.message}`)if(res.code !==0){console.error('[API Error]', res.code, res.message)}return response })/** * 拦截器使用示例 * import { request } from '@/api/request' // 注册 token 注入拦截器 const tokenId = request.addRequestInterceptor((options) => { const token = uni.getStorageSync('token') as string if (token != null && token.length > 0) { options.headers.set('Authorization', `Bearer ${token}`) } return options }) // 注册响应统一错误处理拦截器 const errId = request.addResponseInterceptor((response) => { const res = response as ResponseData<any> if (res.code !== 0) { console.error('[API Error]', res.code, res.message) } return response }) // 不需要时移除 // request.removeRequestInterceptor(tokenId) // request.removeResponseInterceptor(errId) */

二、轮播图接口对接实战

2.0 轮播图后台接口介绍

使用的后台轮播图接口,参加博文链接:https://blog.ZEEKLOG.net/qq8864/article/details/154404554

后台接口的Swagger文档地址:
http://49.235.52.102:8000/static/docs/swagger/swagger-ui.html

curl -X 'GET'\'http://49.235.52.102:8000/api/v1/swiperdata'\ -H 'accept: application/json' 返回数据: {"code":0, "message":"success", "data":[{"id":"1306951", "imageUrl":"https://cdn.bibij.icu/bbjposter/2026/02/13/mzc00200f85ound.jpg", "title":"鹿鼎记", "url":"", "description":"暂无公告发布"}, {"id":"36146692", "imageUrl":"https://cdn.bibij.icu/bbjposter/2026/02/04/mzc00200m2r3dan.jpg", "title":"奇迹少女第五季", "url":"", "description":"暂无公告发布"}, {"id":"36660838", "imageUrl":"https://cdn.bibij.icu/bbjposter/2026/02/13/mzc00200ufp1cvx.jpg", "title":"我的妈妈是校花", "url":"", "description":"暂无公告发布"}, {"id":"36686460", "imageUrl":"https://cdn.bibij.icu/bbjposter/2026/02/10/mzc002003yv87zv.jpg", "title":"突然的喜欢", "url":"", "description":"暂无公告发布"}, {"id":"36707378", "imageUrl":"https://cdn.bibij.icu/bbjposter/2026/02/10/mzc00200y4y2br6.jpg", "title":"铁血战士:杀戮之地", "url":"", "description":"暂无公告发布"}, {"id":"37279767", "imageUrl":"https://cdn.bibij.icu/bbjposter/2026/02/04/mzc00200mervl94.jpg", "title":"喜羊羊与灰太狼之古古怪界有古怪", "url":"", "description":"暂无公告发布"}, {"id":"37462812", "imageUrl":"https://cdn.bibij.icu/bbjposter/2026/02/04/mzc002000vi0s1j.jpg", "title":"变形联盟6星启之战", "url":"", "description":"暂无公告发布"}, {"id":"37938300", "imageUrl":"https://cdn.bibij.icu/bbjposter/2026/02/04/mzc002002ktdbpa.jpg", "title":"猪猪侠之功夫小英雄1", "url":"", "description":"暂无公告发布"}, {"id":"38230948", "imageUrl":"https://cdn.bibij.icu/bbjposter/2026/02/13/mzc0020071gx4kh.jpg", "title":"唐诡奇案", "url":"", "description":"暂无公告发布"}, {"id":"无豆瓣id1770200778227", "imageUrl":"https://cdn.bibij.icu/bbjposter/2026/02/04/mzc00200pdl8ocp.jpg", "title":"愿望喵喵", "url":"", "description":"暂无公告发布"}]}

2.1 数据模型定义

// api/swiper.utsexporttypeSwiperItem={ id:string imageUrl:string title:string url:string description:string}exportclassSwiperApi{staticasyncgetSwiperData():Promise<SwiperItem[]>{const response =await request.get<SwiperItem[]>('/swiperdata')if(response.code ===0){return response.data }thrownewError(response.message)}}

2.2 界面实现及接口使用

<template><viewclass="container"><!-- 轮播图组件 --><swiperclass="swiper-container"autoplayinterval="3000"circularindicator-dotsindicator-color="rgba(255,255,255,0.5)"indicator-active-color="#ffffff"><swiper-itemv-for="(item, index) in swiperList":key="item.id"@click="handleSwiperClick(item)"><image:src="item.imageUrl"mode="aspectFill"class="swiper-image":alt="item.title"/><textclass="swiper-title">{{ item.title }}</text></swiper-item></swiper></view></template><scriptsetuplang="uts">import{ ref, onMounted }from'vue'import{ SwiperApi, SwiperItem }from'@/api/swiper'const swiperList = ref<SwiperItem[]>([])const isLoading =ref(true)onMounted(async()=>{try{ swiperList.value =await SwiperApi.getSwiperData()}catch(error){ uni.showToast({title:'数据加载失败',icon:'error'})}finally{ isLoading.value =false}})consthandleSwiperClick=(item: SwiperItem)=>{if(item.url){ uni.navigateTo({url: item.url })}}</script><style>.swiper-container{height: 400rpx;border-radius: 16rpx;overflow: hidden;}.swiper-image{width: 100%;height: 100%;}.swiper-title{position: absolute;bottom: 40rpx;left: 30rpx;color: white;font-size: 36rpx;text-shadow: 2rpx 2rpx 4rpx rgba(0,0,0,0.5);}</style>

界面优化要点

  • 使用 aspectFill 图片模式保持比例
  • 添加点击事件处理
  • 加入加载状态管理
  • 样式美化(圆角、投影、定位文字)
  • 兼容不同屏幕尺寸(rpx 单位)

在这里插入图片描述

总结

本文实现了从底层网络模块封装到上层业务组件开发的完整流程。通过 UTS 类型系统构建的 Request 类,既保证了代码规范性,又兼容了 uniapp-x 多端特性。轮播图组件的实现展示了以下优势:

  1. 模块解耦:网络层与 UI 层完全分离
  2. 类型安全:完善的类型定义避免运行时错误
  3. 多端兼容:一套代码适配 iOS/Android/HarmonyOS

实际开发中还需考虑以下扩展方向:

  • 接口请求拦截器
  • 自动化测试套件
  • 性能监控体系

项目完整代码已托管至:GitCode 仓库链接

希望本文能为您的 HarmonyOS 应用开发提供有价值的参考。在实践中遇到的任何问题,欢迎访问文末的 ZEEKLOG 博客链接进行技术交流。

Read more

【C++仿Muduo库#3】Server 服务器模块实现上

【C++仿Muduo库#3】Server 服务器模块实现上

📃个人主页:island1314 ⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞 * 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》 🔥 目录 * 一、Buffer 模块 * 二、日志模块 * 三、套接字 Socket 设计 * 1. 代码实现 * 2. 代码检测 * 3. 细节处理 * 细节1:处理 Recv 函数时, errno 的来源以及 为啥不用 `EWOULDBLOCK` * 细节2:MSG_DONWAIT 的概述 * 细节3:关于 ReuseAddr() * 📌 为什么默认不允许端口复用? * 🧠 举个例子:服务重启时的 `TIME_WAIT` 问题 * 🧾小结 * 细节4:宏污染

By Ne0inhk
C++ 继承入门(上):从基础概念定义到默认成员函数,吃透类复用的核心逻辑

C++ 继承入门(上):从基础概念定义到默认成员函数,吃透类复用的核心逻辑

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》 《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心 ✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游 目录 前言 一. 继承的概念与定义   1、继承的核心概念   2、继承的定义格式   3、继承方式与成员访问权限 二. 基类与派生类的转换:子类对象能当父类用吗? 三. 继承中的作用域:同名成员会冲突吗?   1、变量隐藏   2、函数隐藏 四、派生类的默认成员函数:构造、拷贝、析构怎么写?   1、构造函数:先调用父类构造,再初始化子类成员   2、拷贝构造:先拷贝父类,再拷贝子类   3、 赋值重载:

By Ne0inhk
《C++ 多态》三大面向对象编程——多态:虚函数机制、重写规范与现代C++多态控制全概要

《C++ 多态》三大面向对象编程——多态:虚函数机制、重写规范与现代C++多态控制全概要

🔥个人主页:Cx330🌸 ❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》 《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔 🌟心向往之行必能至 🎥Cx330🌸的简介: 目录 前言: 一、认识多态:面向对象编程的灵魂 1.1  多态的核心概念解析 1.2  联系实际:现实世界中的多态类比 二、多态的实现机制深度探索 2.1  多态的本质与构成必要条件 2.1.1  多态的科学定义 2.1.2  实现多态的两个关键条件 2.2  虚函数:多态的基石 2.3  虚函数重写(覆盖)详解 2.4

By Ne0inhk
C++ 继承入门(下):友元、静态成员与菱形继承的底层逻辑

C++ 继承入门(下):友元、静态成员与菱形继承的底层逻辑

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》 《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心 ✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游 目录 前言 一. 友元 —— 友元关系不可继承   1、错误版本   2、正确版本 二. 静态成员 —— 继承体系中静态成员的共享性 三. 多继承及菱形继承问题:本质特点与解决方案   1、单继承与多继承模型   2、菱形继承:虚继承解决“数据冗余”与“二义性”     2.1 菱形继承出现的坑(解决二义性问题)     2.2 虚继承:彻底解决菱形继承问题     3、多继承中指针偏移问题 友元,静态成员,

By Ne0inhk