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在鴻蒙平台下的性能優化與調試技巧,包括內存優化、渲染優化、打包優化等核心內容,幫助大家構建更高效的應用。