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 多端特性。轮播图组件的实现展示了以下优势:
- 模块解耦:网络层与 UI 层完全分离
- 类型安全:完善的类型定义避免运行时错误
- 多端兼容:一套代码适配 iOS/Android/HarmonyOS
实际开发中还需考虑以下扩展方向:
- 接口请求拦截器
- 自动化测试套件
- 性能监控体系
项目完整代码已托管至:GitCode 仓库链接
希望本文能为您的 HarmonyOS 应用开发提供有价值的参考。在实践中遇到的任何问题,欢迎访问文末的 ZEEKLOG 博客链接进行技术交流。