跨平台开发实战: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 核心类型定义

typescript 复制代码
// 定义符合 HTTP 标准的请求方法
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "HEAD" 

// 请求配置对象(注意 UTS 的类型系统要求)
export type RequestOptions = {
  url : string
  method : HttpMethod
  data : any | null
  headers : Map<string, string>
}

// 标准化响应结构
export type ResponseData<T> = {
  code : number
  message : string
  data : T
}

技术要点

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

1.2 Request 类实现

typescript 复制代码
export class Request {
  private baseUrl : string
  private headers : Map<string, string>

  constructor(baseUrl : string) {
    this.baseUrl = baseUrl
    this.headers = new Map<string, string>()
    this.headers.set('Content-Type', 'application/json')
    this.headers.set('accept', 'application/json')
  }

  // 请求头合并方法(转换 Map 为 any)
  private buildHeader(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)
    return new Promise<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(new Error(`HTTP ${res.statusCode}`))
          }
        },
        fail: (err) => {
          reject(new Error(err.errMsg))
        }
      })
    })
  }

  // 快捷方法示例
  get<T>(url: string, params?: any, headers?: Map<string, string>) {
    return this.request<T>({
      url: url + (params ? `?${new URLSearchParams(params)}` : ''),
      method: 'GET',
      data: null,
      headers: headers ?? new Map<string, string>()
    })
  }
}

关键实现细节

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

1.3 完整封装代码

request.uts

typescript 复制代码
/**
 * file:request.uts
 * bref:uniapp-x 网络请求模块封装
 * author:yangyongzhen
 * date:2026-0224 23:11
 * qq:534117529
 */
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "HEAD" 

export type RequestOptions = {
  url : string
  method : HttpMethod
  data : any | null
  headers : Map<string, string>
}

export type ResponseData<T> = {
  code : number
  message : string
  data : T
}

export class Request {
  private baseUrl : string
  private headers : Map<string, string>

  constructor(baseUrl : string) {
    this.baseUrl = baseUrl
    this.headers = new Map<string, string>()
    this.headers.set('Content-Type', 'application/json')
    this.headers.set('accept', 'application/json')
  }

  // 合并默认头与自定义头,转为 any 对象传给 uni.request
  private buildHeader(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)
    return new Promise<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(new Error(`Request failed: ${res.statusCode}`))
          }
        },
        fail: (err) => {
          reject(new Error(err.errMsg))
        }
      })
    })
  }

  get<T>(url : string, params : any | null, headers : Map<string, string> | null) : Promise<ResponseData<T>> {
    return this.request<T>({
      url: url,
      method: 'GET',
      data: params,
      headers: headers ?? new Map<string, string>()
    })
  }

  post<T>(url : string, data : any | null, headers : Map<string, string> | null) : Promise<ResponseData<T>> {
    return this.request<T>({
      url: url,
      method: 'POST',
      data: data,
      headers: headers ?? new Map<string, string>()
    })
  }

  put<T>(url : string, data : any | null, headers : Map<string, string> | null) : Promise<ResponseData<T>> {
    return this.request<T>({
      url: url,
      method: 'PUT',
      data: data,
      headers: headers ?? new Map<string, string>()
    })
  }

  delete<T>(url : string, data : any | null, headers : Map<string, string> | null) : Promise<ResponseData<T>> {
    return this.request<T>({
      url: url,
      method: 'DELETE',
      data: data,
      headers: headers ?? new Map<string, string>()
    })
  }
}

export const request = new Request('http://49.235.52.102:8000/api/v1')

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

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

typescript 复制代码
/**
 * file:request.uts
 * bref:uniapp-x 网络请求模块封装
 * author:yangyongzhen
 * date:2026-0224 23:11
 * qq:534117529
 */
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "HEAD"

export type RequestOptions = {
  url : string
  method : HttpMethod
  data : any | null
  headers : Map<string, string>
}

export type ResponseData<T> = {
  code : number
  message : string
  data : T
}

// 请求拦截器:接收 RequestOptions,返回修改后的 RequestOptions
export type RequestInterceptorFn = (options : RequestOptions) => RequestOptions

// 响应拦截器:接收原始响应 any,返回处理后的 any
export type ResponseInterceptorFn = (response : any) => any

export class Request {
  private baseUrl : string
  private headers : Map<string, string>
  // 拦截器存储:Map<自增ID, 拦截器函数>,保证按注册顺序执行且移除不影响其他
  private requestInterceptors : Map<number, RequestInterceptorFn>
  private responseInterceptors : Map<number, ResponseInterceptorFn>
  private requestInterceptorId : number
  private responseInterceptorId : number

  constructor(baseUrl : string) {
    this.baseUrl = baseUrl
    this.headers = new Map<string, string>()
    this.headers.set('Content-Type', 'application/json')
    this.headers.set('accept', 'application/json')
    this.requestInterceptors = new Map<number, RequestInterceptorFn>()
    this.responseInterceptors = new Map<number, ResponseInterceptorFn>()
    this.requestInterceptorId = 0
    this.responseInterceptorId = 0
  }

  // ---- 拦截器注册 / 移除 ----

  /**
   * 注册请求拦截器,返回 ID(可用于移除)
   */
  addRequestInterceptor(fn : RequestInterceptorFn) : number {
    this.requestInterceptorId += 1
    const 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 += 1
    const id = this.responseInterceptorId
    this.responseInterceptors.set(id, fn)
    return id
  }

  removeResponseInterceptor(id : number) : void {
    this.responseInterceptors.delete(id)
  }

  // ---- 拦截器链执行 ----

  // 收集 Map 中所有 ID 并升序排序,保证按注册先后顺序执行
  private runRequestInterceptors(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
  }

  private runResponseInterceptors(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
  }

  // ---- 合并请求头 ----

  private buildHeader(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 已带协议头时直接使用,否则拼接实例默认 baseUrl
    const url = interceptedOptions.url.startsWith('http://') || interceptedOptions.url.startsWith('https://')
      ? interceptedOptions.url
      : this.baseUrl + interceptedOptions.url
    const header = this.buildHeader(interceptedOptions.headers)

    return new Promise<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(new Error(`Request failed: ${res.statusCode}`))
          }
        },
        fail: (err) => {
          reject(new Error(err.errMsg))
        }
      })
    })
  }

  get<T>(url : string, params : any | null, headers : Map<string, string> | null) : Promise<ResponseData<T>> {
    return this.request<T>({
      url: url,
      method: 'GET',
      data: params,
      headers: headers ?? new Map<string, string>()
    })
  }

  post<T>(url : string, data : any | null, headers : Map<string, string> | null) : Promise<ResponseData<T>> {
    return this.request<T>({
      url: url,
      method: 'POST',
      data: data,
      headers: headers ?? new Map<string, string>()
    })
  }

  put<T>(url : string, data : any | null, headers : Map<string, string> | null) : Promise<ResponseData<T>> {
    return this.request<T>({
      url: url,
      method: 'PUT',
      data: data,
      headers: headers ?? new Map<string, string>()
    })
  }

  delete<T>(url : string, data : any | null, headers : Map<string, string> | null) : Promise<ResponseData<T>> {
    return this.request<T>({
      url: url,
      method: 'DELETE',
      data: data,
      headers: headers ?? new Map<string, string>()
    })
  }
}

export const request = new Request('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.csdn.net/qq8864/article/details/154404554

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

bash 复制代码
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 数据模型定义

typescript 复制代码
// api/swiper.uts
export type SwiperItem = {
  id: string
  imageUrl: string
  title: string
  url: string
  description: string
}

export class SwiperApi {
  static async getSwiperData(): Promise<SwiperItem[]> {
    const response = await request.get<SwiperItem[]>('/swiperdata')
    if (response.code === 0) {
      return response.data
    }
    throw new Error(response.message)
  }
}

2.2 界面实现及接口使用

html 复制代码
<template>
  <view class="container">
    <!-- 轮播图组件 -->
    <swiper 
      class="swiper-container"
      autoplay 
      interval="3000"
      circular
      indicator-dots
      indicator-color="rgba(255,255,255,0.5)"
      indicator-active-color="#ffffff"
    >
      <swiper-item 
        v-for="(item, index) in swiperList" 
        :key="item.id"
        @click="handleSwiperClick(item)"
      >
        <image 
          :src="item.imageUrl" 
          mode="aspectFill"
          class="swiper-image"
          :alt="item.title"
        />
        <text class="swiper-title">{{ item.title }}</text>
      </swiper-item>
    </swiper>
  </view>
</template>

<script setup lang="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
    }
  })

  const handleSwiperClick = (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 应用开发提供有价值的参考。在实践中遇到的任何问题,欢迎访问文末的 CSDN 博客链接进行技术交流。

相关推荐
coooliang1 小时前
【鸿蒙 NEXT】自定义dialog
华为·harmonyos
xiaoliuliu123451 小时前
bugfree缺陷管理工具部署步骤详解(附PHP+MySQL环境准备与安装教程)
android
不爱吃糖的程序媛1 小时前
Flutter-OH 三方库 devicelocale 鸿蒙适配
flutter·华为·harmonyos
summerkissyou19871 小时前
Android-Audio-adb分析
android·audio
云飞云共享云桌面1 小时前
10人SolidWorks设计团队如何提升SolidWorks软件利用率
大数据·linux·运维·服务器·网络·人工智能
加农炮手Jinx10 小时前
Flutter for OpenHarmony 实战:JWT — 构建安全的无状态认证中心
网络·flutter·华为·harmonyos·鸿蒙
左手厨刀右手茼蒿10 小时前
Flutter for OpenHarmony: Flutter 三方库 hashlib 为鸿蒙应用提供军用级加密哈希算法支持(安全数据完整性卫士)
安全·flutter·华为·c#·哈希算法·linq·harmonyos
王码码203510 小时前
Flutter for OpenHarmony: Flutter 三方库 cryptography 在鸿蒙上实现金融级现代加解密(高性能安全库)
android·安全·flutter·华为·金融·harmonyos
iambooo11 小时前
Shell在日志分析与故障排查中的实战应用
linux·服务器·网络