在玩中学,直接上手实战是猫哥一贯的自学方法心得。假期期间实在无聊!我不睡懒觉、不看电影、也不刷手机、不玩游戏、也无处可去。那么我干嘛嘞?闲的都想看蚂蚁上树,无聊透顶,百无聊赖,感觉假期好没意思啊。做什么呢? 于是翻出来之前做过的"爱影家"影视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 多端特性。轮播图组件的实现展示了以下优势:
- 模块解耦:网络层与 UI 层完全分离
- 类型安全:完善的类型定义避免运行时错误
- 多端兼容:一套代码适配 iOS/Android/HarmonyOS
实际开发中还需考虑以下扩展方向:
- 接口请求拦截器
- 自动化测试套件
- 性能监控体系
项目完整代码已托管至:GitCode 仓库链接
希望本文能为您的 HarmonyOS 应用开发提供有价值的参考。在实践中遇到的任何问题,欢迎访问文末的 CSDN 博客链接进行技术交流。