uniapp开发鸿蒙:网络请求与数据交互实战
请求封装:基础请求方法的封装和常用方法扩展拦截器系统:请求和响应拦截器的统一处理API管理:模块化的API组织方式数据缓存:本地存储和请求缓存策略文件操作:文件上传下载的完整实现错误处理:全局错误捕获和重试机制实战案例:商品列表页的完整实现鸿蒙适配:鸿蒙平台特有的网络配置关键要点使用拦截器统一处理token、loading、错误提示模块化组织API接口,提高可维护性合理使用缓存策略,提升用户体验完
uniapp开发鸿蒙:网络请求与数据交互实战
引入:构建健壮的网络层
在前几篇文章中,我们学习了uniapp鸿蒙开发的环境配置、页面布局、状态管理等核心知识。今天,我们将深入探讨网络请求与数据交互的完整方案,这是应用与后端服务通信的桥梁,也是保证应用稳定性和用户体验的关键环节。
uniapp提供了uni.request作为网络请求的基础API,但直接使用会遇到代码冗余、缺乏统一管理、错误处理复杂等问题。通过合理的封装和架构设计,我们可以构建出高效、可维护的网络请求体系。
一、基础请求封装
1.1 创建请求配置文件
首先创建配置文件,统一管理请求参数:
utils/config.js
// 环境配置
export const ENV = process.env.NODE_ENV || 'development'
// 基础URL配置
export const BASE_URL = {
development: 'https://dev-api.example.com',
production: 'https://api.example.com'
}[ENV]
// 请求超时时间
export const TIMEOUT = 15000
// 公共请求头
export const COMMON_HEADERS = {
'Content-Type': 'application/json',
'X-App-Version': '1.0.0'
}
// 业务状态码映射
export const ERROR_CODE_MAP = {
401: '登录状态已过期',
403: '无权限访问',
500: '服务器异常,请稍后重试'
}
1.2 核心请求方法封装
utils/request.js
import { BASE_URL, TIMEOUT, COMMON_HEADERS } from './config'
// 防重复请求队列
const pendingRequests = new Map()
// 生成请求唯一标识
const generateReqKey = (config) => {
return `${config.url}&${config.method}&${JSON.stringify(config.data)}`
}
// 基础请求方法
const request = (options = {}) => {
// 合并配置
const mergedConfig = {
url: options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
...COMMON_HEADERS,
...(options.header || {}),
'Authorization': uni.getStorageSync('token') || ''
},
timeout: options.timeout || TIMEOUT,
loading: options.loading !== false, // 默认显示loading
...options
}
// 处理完整URL
if (!mergedConfig.url.startsWith('http')) {
mergedConfig.url = mergedConfig.url.startsWith('/')
? `${BASE_URL}${mergedConfig.url}`
: `${BASE_URL}/${mergedConfig.url}`
}
// 防重复请求检查
const requestKey = generateReqKey(mergedConfig)
if (pendingRequests.has(requestKey)) {
return Promise.reject(new Error('重复请求'))
}
pendingRequests.set(requestKey, true)
return new Promise((resolve, reject) => {
uni.request({
...mergedConfig,
success: (res) => {
pendingRequests.delete(requestKey)
resolve(res)
},
fail: (err) => {
pendingRequests.delete(requestKey)
reject(err)
}
})
})
}
// 常用请求方法封装
export const get = (url, data, options = {}) => {
return request({
url,
data,
method: 'GET',
...options
})
}
export const post = (url, data, options = {}) => {
return request({
url,
data,
method: 'POST',
...options
})
}
export const put = (url, data, options = {}) => {
return request({
url,
data,
method: 'PUT',
...options
})
}
export const del = (url, data, options = {}) => {
return request({
url,
data,
method: 'DELETE',
...options
})
}
export default request
二、拦截器系统实现
2.1 请求拦截器
utils/interceptors.js
import { ERROR_CODE_MAP } from './config'
// 请求拦截器
uni.addInterceptor('request', {
invoke: (config) => {
console.log('请求开始:', config)
// 自动添加token
const token = uni.getStorageSync('token')
if (token) {
config.header = config.header || {}
config.header.Authorization = token
}
// 显示loading
if (config.loading) {
uni.showLoading({
title: '加载中...',
mask: true
})
}
return config
},
success: (res) => {
console.log('请求成功:', res)
// 隐藏loading
uni.hideLoading()
const { statusCode, data } = res
// HTTP状态码错误处理
if (statusCode >= 400) {
const errorMsg = ERROR_CODE_MAP[statusCode] || `网络错误: ${statusCode}`
uni.showToast({
title: errorMsg,
icon: 'none'
})
return Promise.reject(res)
}
// 业务状态码处理
if (data && data.code !== 200) {
uni.showToast({
title: data.message || '请求失败',
icon: 'none'
})
// token过期处理
if (data.code === 401) {
uni.removeStorageSync('token')
uni.reLaunch({
url: '/pages/login/login'
})
return Promise.reject(res)
}
return Promise.reject(res)
}
return res
},
fail: (err) => {
console.error('请求失败:', err)
uni.hideLoading()
// 网络错误处理
uni.showToast({
title: '网络异常,请检查网络连接',
icon: 'none'
})
return Promise.reject(err)
}
})
2.2 响应拦截器优化
utils/response.js
// 响应统一处理
export const handleResponse = (response) => {
const { statusCode, data } = response
if (statusCode >= 200 && statusCode < 300) {
// 业务成功
if (data && data.code === 200) {
return data.data
}
// 业务失败
throw new Error(data.message || '请求失败')
}
// HTTP错误
throw new Error(`HTTP错误: ${statusCode}`)
}
// 错误统一处理
export const handleError = (error) => {
console.error('请求错误:', error)
if (error.errMsg) {
// 网络错误
uni.showToast({
title: '网络异常,请检查网络连接',
icon: 'none'
})
} else if (error.message) {
// 业务错误
uni.showToast({
title: error.message,
icon: 'none'
})
}
throw error
}
三、API模块化管理
3.1 用户模块API
api/user.js
import { get, post } from '@/utils/request'
// 用户登录
export const login = (data) => {
return post('/user/login', data)
}
// 获取用户信息
export const getUserInfo = () => {
return get('/user/info')
}
// 更新用户信息
export const updateUserInfo = (data) => {
return post('/user/update', data)
}
// 退出登录
export const logout = () => {
return post('/user/logout')
}
3.2 商品模块API
api/product.js
import { get, post } from '@/utils/request'
// 获取商品列表
export const getProductList = (params) => {
return get('/product/list', params)
}
// 获取商品详情
export const getProductDetail = (id) => {
return get(`/product/detail/${id}`)
}
// 添加商品到购物车
export const addToCart = (data) => {
return post('/cart/add', data)
}
// 获取购物车列表
export const getCartList = () => {
return get('/cart/list')
}
3.3 API统一出口
api/index.js
export * from './user'
export * from './product'
四、数据缓存策略
4.1 本地存储封装
utils/storage.js
// 带过期时间的缓存
export const setWithExpire = (key, data, expire = 60 * 60 * 1000) => {
const cacheObj = {
data,
timestamp: Date.now() + expire
}
try {
uni.setStorageSync(key, JSON.stringify(cacheObj))
} catch (e) {
console.error('设置缓存失败:', e)
}
}
// 获取带过期时间的缓存
export const getWithExpire = (key) => {
try {
const str = uni.getStorageSync(key)
if (!str) return null
const cacheObj = JSON.parse(str)
if (Date.now() > cacheObj.timestamp) {
uni.removeStorageSync(key)
return null
}
return cacheObj.data
} catch (e) {
console.error('获取缓存失败:', e)
return null
}
}
// 清除所有缓存
export const clearAllCache = () => {
try {
const storageInfo = uni.getStorageInfoSync()
storageInfo.keys.forEach(key => {
if (key.startsWith('cache_')) {
uni.removeStorageSync(key)
}
})
} catch (e) {
console.error('清除缓存失败:', e)
}
}
4.2 请求缓存策略
utils/cache.js
import { getWithExpire, setWithExpire } from './storage'
// 请求缓存
export const cachedRequest = async (key, requestFn, expire = 5 * 60 * 1000) => {
// 从缓存获取
const cachedData = getWithExpire(key)
if (cachedData) {
return cachedData
}
// 发起请求
try {
const data = await requestFn()
setWithExpire(key, data, expire)
return data
} catch (error) {
console.error('请求失败:', error)
throw error
}
}
// 清除指定缓存
export const clearCache = (key) => {
uni.removeStorageSync(key)
}
五、文件上传下载
5.1 文件上传
utils/upload.js
import { BASE_URL } from './config'
// 上传文件
export const uploadFile = (filePath, name = 'file') => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${BASE_URL}/upload`,
filePath,
name,
header: {
'Authorization': uni.getStorageSync('token') || ''
},
success: (res) => {
const data = JSON.parse(res.data)
if (data.code === 200) {
resolve(data.data)
} else {
reject(new Error(data.message || '上传失败'))
}
},
fail: (err) => {
reject(err)
}
})
})
}
// 选择并上传图片
export const chooseAndUploadImage = () => {
return new Promise((resolve, reject) => {
uni.chooseImage({
count: 1,
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
uploadFile(tempFilePath)
.then(resolve)
.catch(reject)
},
fail: reject
})
})
}
5.2 文件下载
utils/download.js
// 下载文件
export const downloadFile = (url, fileName) => {
return new Promise((resolve, reject) => {
const downloadTask = uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
// 保存到本地
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
resolve(saveRes.savedFilePath)
},
fail: reject
})
} else {
reject(new Error(`下载失败: ${res.statusCode}`))
}
},
fail: reject
})
// 监听下载进度
downloadTask.onProgressUpdate((res) => {
console.log('下载进度:', res.progress)
})
})
}
六、错误处理与重试机制
6.1 统一错误处理
utils/errorHandler.js
// 全局错误处理器
export const errorHandler = {
// 网络错误
networkError: (error) => {
console.error('网络错误:', error)
uni.showToast({
title: '网络异常,请检查网络连接',
icon: 'none'
})
},
// 业务错误
businessError: (error) => {
console.error('业务错误:', error)
uni.showToast({
title: error.message || '操作失败',
icon: 'none'
})
},
// 登录过期
authError: () => {
uni.removeStorageSync('token')
uni.showModal({
title: '提示',
content: '登录已过期,请重新登录',
showCancel: false,
success: () => {
uni.reLaunch({
url: '/pages/login/login'
})
}
})
},
// 未知错误
unknownError: (error) => {
console.error('未知错误:', error)
uni.showToast({
title: '系统异常,请稍后重试',
icon: 'none'
})
}
}
// 全局错误捕获
export const setupGlobalErrorHandler = () => {
// Vue错误捕获
if (typeof Vue !== 'undefined') {
Vue.config.errorHandler = (err, vm, info) => {
console.error('Vue错误:', err, info)
errorHandler.unknownError(err)
}
}
// Promise错误捕获
window.addEventListener('unhandledrejection', (event) => {
console.error('Promise错误:', event.reason)
errorHandler.unknownError(event.reason)
})
}
6.2 请求重试机制
utils/retry.js
// 请求重试
export const retryRequest = async (requestFn, maxRetries = 3, delay = 1000) => {
let retries = 0
while (retries < maxRetries) {
try {
return await requestFn()
} catch (error) {
retries++
// 如果是网络错误,等待后重试
if (error.errMsg && error.errMsg.includes('网络错误')) {
if (retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, delay * retries))
continue
}
}
throw error
}
}
}
七、实战案例:商品列表页
7.1 页面实现
pages/product/list.vue
<template>
<view class="container">
<!-- 搜索框 -->
<view class="search-box">
<uni-search-bar
placeholder="搜索商品"
v-model="searchKeyword"
@confirm="handleSearch"
@clear="handleClearSearch"
/>
</view>
<!-- 商品列表 -->
<view class="product-list">
<view
v-for="item in productList"
:key="item.id"
class="product-item"
@click="goToDetail(item.id)"
>
<image :src="item.image" class="product-image" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<text class="product-price">¥{{ item.price }}</text>
<text class="product-sales">已售{{ item.sales }}件</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMore" class="load-more">
<uni-load-more :status="loadingMore ? 'loading' : 'more'" />
</view>
<!-- 空状态 -->
<view v-if="!loading && productList.length === 0" class="empty">
<image src="/static/empty.png" class="empty-image" />
<text class="empty-text">暂无商品</text>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getProductList } from '@/api/product'
const searchKeyword = ref('')
const productList = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
const pageSize = ref(10)
// 获取商品列表
const fetchProductList = async (page = 1, isLoadMore = false) => {
if (loading.value || loadingMore.value) return
if (isLoadMore) {
loadingMore.value = true
} else {
loading.value = true
}
try {
const params = {
page,
pageSize: pageSize.value,
keyword: searchKeyword.value
}
const res = await getProductList(params)
const list = res.list || []
if (page === 1) {
productList.value = list
} else {
productList.value = [...productList.value, ...list]
}
// 判断是否还有更多
hasMore.value = list.length >= pageSize.value
currentPage.value = page
} catch (error) {
console.error('获取商品列表失败:', error)
} finally {
loading.value = false
loadingMore.value = false
}
}
// 搜索
const handleSearch = () => {
currentPage.value = 1
fetchProductList(1)
}
// 清除搜索
const handleClearSearch = () => {
searchKeyword.value = ''
currentPage.value = 1
fetchProductList(1)
}
// 加载更多
const loadMore = () => {
if (!hasMore.value || loadingMore.value) return
fetchProductList(currentPage.value + 1, true)
}
// 跳转到详情页
const goToDetail = (id) => {
uni.navigateTo({
url: `/pages/product/detail?id=${id}`
})
}
// 初始化
onMounted(() => {
fetchProductList()
})
</script>
<style scoped>
.container {
padding: 20rpx;
}
.search-box {
margin-bottom: 20rpx;
}
.product-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.product-item {
display: flex;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.product-image {
width: 200rpx;
height: 200rpx;
}
.product-info {
flex: 1;
padding: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.product-price {
font-size: 32rpx;
color: #ff6b35;
font-weight: bold;
}
.product-sales {
font-size: 24rpx;
color: #999;
}
.load-more {
padding: 30rpx 0;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
</style>
7.2 页面配置
pages.json
{
"pages": [
{
"path": "pages/product/list",
"style": {
"navigationBarTitleText": "商品列表",
"enablePullDownRefresh": true
}
}
]
}
八、鸿蒙平台适配
8.1 鸿蒙特有配置
manifest.json
{
"app-plus": {
"harmony": {
"network": {
"cleartextTraffic": true // 允许HTTP请求
}
}
}
}
8.2 鸿蒙网络权限
manifest.json
{
"app-plus": {
"harmony": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
}
九、性能优化建议
9.1 请求优化
- 防抖节流:对频繁触发的请求进行防抖处理
- 请求合并:合并多个小请求为一个大请求
- 数据压缩:开启Gzip压缩减少传输数据量
- CDN加速:静态资源使用CDN加速
9.2 缓存优化
- 合理设置缓存时间:根据数据更新频率设置缓存过期时间
- 缓存版本控制:数据更新时清除旧缓存
- 内存缓存:频繁访问的数据使用内存缓存
9.3 错误降级
- 网络降级:网络异常时使用本地缓存数据
- 接口降级:接口失败时展示降级页面
- 重试机制:网络波动时自动重试
总结
通过本篇文章的学习,我们掌握了uniapp在鸿蒙平台下的网络请求与数据交互的完整方案:
- 请求封装:基础请求方法的封装和常用方法扩展
- 拦截器系统:请求和响应拦截器的统一处理
- API管理:模块化的API组织方式
- 数据缓存:本地存储和请求缓存策略
- 文件操作:文件上传下载的完整实现
- 错误处理:全局错误捕获和重试机制
- 实战案例:商品列表页的完整实现
- 鸿蒙适配:鸿蒙平台特有的网络配置
关键要点:
- 使用拦截器统一处理token、loading、错误提示
- 模块化组织API接口,提高可维护性
- 合理使用缓存策略,提升用户体验
- 完善的错误处理机制,保证应用稳定性
下一篇文章,我们将深入讲解uniapp在鸿蒙平台下的性能优化与调试技巧,包括内存优化、渲染优化、打包优化等核心内容,帮助大家构建更高效的应用。
更多推荐




所有评论(0)