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 请求优化

  1. 防抖节流:对频繁触发的请求进行防抖处理
  2. 请求合并:合并多个小请求为一个大请求
  3. 数据压缩:开启Gzip压缩减少传输数据量
  4. CDN加速:静态资源使用CDN加速

9.2 缓存优化

  1. 合理设置缓存时间:根据数据更新频率设置缓存过期时间
  2. 缓存版本控制:数据更新时清除旧缓存
  3. 内存缓存:频繁访问的数据使用内存缓存

9.3 错误降级

  1. 网络降级:网络异常时使用本地缓存数据
  2. 接口降级:接口失败时展示降级页面
  3. 重试机制:网络波动时自动重试

总结

通过本篇文章的学习,我们掌握了uniapp在鸿蒙平台下的网络请求与数据交互的完整方案:

  1. 请求封装:基础请求方法的封装和常用方法扩展
  2. 拦截器系统:请求和响应拦截器的统一处理
  3. API管理:模块化的API组织方式
  4. 数据缓存:本地存储和请求缓存策略
  5. 文件操作:文件上传下载的完整实现
  6. 错误处理:全局错误捕获和重试机制
  7. 实战案例:商品列表页的完整实现
  8. 鸿蒙适配:鸿蒙平台特有的网络配置

关键要点

  • 使用拦截器统一处理token、loading、错误提示
  • 模块化组织API接口,提高可维护性
  • 合理使用缓存策略,提升用户体验
  • 完善的错误处理机制,保证应用稳定性

下一篇文章,我们将深入讲解uniapp在鸿蒙平台下的性能优化与调试技巧,包括内存优化、渲染优化、打包优化等核心内容,帮助大家构建更高效的应用。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐