第七篇:网络请求与状态管理:封装 Axios + Pinia 实战

引言:为什么不能直接用 uni.request

在 uni-app 中,发起网络请求最直接的方式是使用 uni.request

uni.request({
  url: 'https://api.example.***/user',
  method: 'GET',
  su***ess: (res) => {
    console.log(res.data)
  },
  fail: (err) => {
    console.error(err)
  }
})

但如果你在项目中到处直接调用 uni.request,很快就会遇到问题:

  1. 重复代码:每个请求都要写 baseURLheader、错误处理。
  2. 缺乏统一拦截:无法统一处理 Token、错误提示、Loading 状态。
  3. 难以维护:API 地址散落在各处,修改 baseUrl 时要改几十个文件。
  4. 类型不安全:TypeScript 项目中无法获得良好的类型提示。

解决方案:我们需要一个企业级的请求库封装

今天,我将带你用 Axios(通过 @uni/axios 兼容层)结合 Pinia,封装一个高内聚、低耦合、类型安全的请求库,并实现:

  • ✅ 统一的基础配置
  • ✅ 请求/响应拦截器
  • ✅ 自动注入 Token
  • ✅ 错误统一处理与提示
  • ✅ Loading 状态管理
  • ✅ Token 过期自动刷新
  • ✅ 与 Pinia 状态管理无缝集成

一、技术选型:为什么用 Axios 而不是 uni.request

虽然 uni.request 是原生 API,但 Axios 拥有更强大的功能和生态:

  • ✅ 更优雅的 Promise API
  • ✅ 强大的拦截器(Interceptors)
  • ✅ 请求取消、超时配置
  • ✅ 自动转换 JSON 数据
  • ✅ 丰富的社区插件

在 uni-app 中,我们可以使用 @uni/axios 这个兼容层,让 Axios 底层调用 uni.request

npm install axios @uni/axios
// utils/request.js
import axios from 'axios'
import adapter from '@uni/axios'

// 配置 adapter
axios.defaults.adapter = adapter

二、封装请求库:从零开始

我们将创建一个模块化的请求库。

2.1 基础配置
// utils/request.js
import axios from 'axios'
import adapter from '@uni/axios'

// 创建 axios 实例
const request = axios.create({
  baseURL: 'https://api.your-app.***', // 后端 API 基地址
  timeout: 10000, // 超时时间
  adapter: adapter, // 使用 uni.request 适配器
  headers: {
    'Content-Type': 'application/json'
  }
})

export default request
2.2 请求拦截器(Request Interceptor)

在请求发出前,可以统一处理:

  • 添加 Token
  • 显示 Loading
  • 添加请求日志
// utils/request.js
import { useUserStore } from '@/stores/user'

// 请求拦截器
request.interceptors.request.use(
  config => {
    const userStore = useUserStore()
    
    // 1. 添加 Token
    if (userStore.token) {
      config.headers['Authorization'] = `Bearer ${userStore.token}`
    }
    
    // 2. 显示 Loading(可配置)
    if (config.showLoading !== false) {
      uni.showLoading({ title: '请求中...' })
    }
    
    // 3. 请求日志(开发环境)
    if (process.env.NODE_ENV === 'development') {
      console.log('[API Request]', config.method?.toUpperCase(), config.url, config.data)
    }
    
    return config
  },
  error => {
    return Promise.reject(error)
  }
)
2.3 响应拦截器(Response Interceptor)

处理服务器响应,统一错误处理。

// utils/request.js
import { useUserStore } from '@/stores/user'

// 响应拦截器
request.interceptors.response.use(
  response => {
    // 隐藏 loading
    uni.hideLoading()
    
    const { data } = response
    
    // 假设后端返回格式:{ code: 0, data: {}, msg: '成功' }
    if (data.code === 0) {
      return data.data // 只返回业务数据
    } else {
      // 业务错误
      uni.showToast({ title: data.msg || '请求失败', icon: 'none' })
      return Promise.reject(new Error(data.msg))
    }
  },
  async error => {
    uni.hideLoading()
    
    const { response } = error
    
    if (!response) {
      uni.showToast({ title: '网络连接失败', icon: 'none' })
      return Promise.reject(error)
    }
    
    switch (response.status) {
      case 401:
        // Token 过期,尝试刷新
        return handleTokenRefresh(error)
      case 403:
        uni.showToast({ title: '无权限访问', icon: 'none' })
        break
      case 404:
        uni.showToast({ title: '请求的资源不存在', icon: 'none' })
        break
      default:
        uni.showToast({ title: `服务器错误: ${response.status}`, icon: 'none' })
    }
    
    return Promise.reject(error)
  }
)
2.4 Token 自动刷新机制

这是高级功能,解决 Token 过期问题。

// utils/request.js
let isRefreshing = false
let refreshSubscribers = []

// 订阅刷新
function subscribeTokenRefresh(cb) {
  refreshSubscribers.push(cb)
}

// 通知所有订阅者
function onRefreshed(token) {
  refreshSubscribers.forEach(cb => cb(token))
  refreshSubscribers = []
}

// 处理 Token 刷新
async function handleTokenRefresh(error) {
  const { config } = error
  const userStore = useUserStore()
  
  if (!isRefreshing) {
    isRefreshing = true
    try {
      // 调用刷新接口
      const res = await request({
        url: '/auth/refresh-token',
        method: 'POST',
        data: { refreshToken: userStore.refreshToken },
        showLoading: false // 刷新时不显示 loading
      })
      
      const { token, refreshToken } = res
      userStore.updateToken(token, refreshToken) // 更新 Pinia 中的 token
      
      // 通知所有等待的请求
      onRefreshed(token)
      // 重试原请求
      config.headers['Authorization'] = `Bearer ${token}`
      return request(config)
      
    } catch (refreshError) {
      // 刷新失败,跳转登录
      userStore.logout()
      uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
      setTimeout(() => {
        uni.reLaunch({ url: '/pages/index/index' })
      }, 1500)
      return Promise.reject(refreshError)
    } finally {
      isRefreshing = false
    }
  } else {
    // 等待刷新完成
    return new Promise((resolve) => {
      subscribeTokenRefresh((token) => {
        config.headers['Authorization'] = `Bearer ${token}`
        resolve(request(config))
      })
    })
  }
}

三、API 接口层:模块化管理

将 API 按业务模块拆分。

3.1 用户模块
// api/user.js
import request from '@/utils/request'

export const userApi = {
  // 登录
  login: (data) => request.post('/auth/login', data),
  
  // 获取用户信息
  getUserInfo: () => request.get('/user/info'),
  
  // 更新用户信息
  updateUserInfo: (data) => request.put('/user/info', data),
  
  // 发送验证码
  sendCode: (phone) => request.post('/auth/send-code', { phone })
}
3.2 商品模块
// api/product.js
import request from '@/utils/request'

export const productApi = {
  // 获取商品列表
  getList: (params) => request.get('/products', { params }),
  
  // 获取商品详情
  getDetail: (id) => request.get(`/products/${id}`),
  
  // 搜索商品
  search: (keyword) => request.get('/products/search', { params: { keyword } })
}
3.3 购物车模块
// api/cart.js
import request from '@/utils/request'

export const cartApi = {
  // 获取购物车
  getCart: () => request.get('/cart'),
  
  // 添加商品到购物车
  addToCart: (data) => request.post('/cart', data),
  
  // 更新购物车商品数量
  updateItem: (id, data) => request.put(`/cart/${id}`, data),
  
  // 删除购物车商品
  removeItem: (id) => request.delete(`/cart/${id}`)
}

四、与 Pinia 状态管理结合

将 API 调用与 Pinia Store 结合,实现数据流的闭环。

4.1 用户 Store
// stores/user.js
import { defineStore } from 'pinia'
import { userApi } from '@/api/user'

export const useUserStore = defineStore('user', () => {
  const userInfo = ref(null)
  const token = ref('')
  const refreshToken = ref('')
  const isLoggedIn = ***puted(() => !!userInfo.value)

  // 登录
  const login = async (credentials) => {
    const data = await userApi.login(credentials)
    token.value = data.token
    refreshToken.value = data.refreshToken
    // 登录成功后获取用户信息
    await fetchUserInfo()
  }

  // 获取用户信息
  const fetchUserInfo = async () => {
    try {
      const data = await userApi.getUserInfo()
      userInfo.value = data
    } catch (error) {
      // 获取失败,可能 Token 无效
      logout()
    }
  }

  // 登出
  const logout = () => {
    userInfo.value = null
    token.value = ''
    refreshToken.value = ''
    uni.removeStorageSync('user')
  }

  // 更新 Token(由请求库调用)
  const updateToken = (newToken, newRefreshToken) => {
    token.value = newToken
    refreshToken.value = newRefreshToken
  }

  return {
    userInfo,
    token,
    refreshToken,
    isLoggedIn,
    login,
    fetchUserInfo,
    logout,
    updateToken
  }
}, {
  persist: true
})
4.2 购物车 Store
// stores/cart.js
import { defineStore } from 'pinia'
import { cartApi } from '@/api/cart'

export const useCartStore = defineStore('cart', () => {
  const items = ref([])
  const loading = ref(false)

  const totalCount = ***puted(() => 
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const totalAmount = ***puted(() => 
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  // 获取购物车
  const fetchCart = async () => {
    loading.value = true
    try {
      const data = await cartApi.getCart()
      items.value = data.items
    } catch (error) {
      uni.showToast({ title: '获取购物车失败', icon: 'none' })
    } finally {
      loading.value = false
    }
  }

  // 添加商品
  const addToCart = async (product) => {
    try {
      await cartApi.addToCart({ productId: product.id, quantity: 1 })
      // 成功后刷新购物车
      await fetchCart()
      uni.showToast({ title: '已加入购物车' })
    } catch (error) {
      // 错误已在拦截器中处理
    }
  }

  return {
    items,
    loading,
    totalCount,
    totalAmount,
    fetchCart,
    addToCart
  }
}, {
  persist: true
})

五、在组件中使用

<!-- pages/user/profile.vue -->
<template>
  <view class="profile">
    <view v-if="loading">加载中...</view>
    <view v-else>
      <text>用户名:{{ userStore.userInfo?.name }}</text>
      <text>邮箱:{{ userStore.userInfo?.email }}</text>
    </view>
    <button @click="handleLogout">退出登录</button>
  </view>
</template>

<script setup>
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const loading = ref(false)

onMounted(async () => {
  if (userStore.isLoggedIn) {
    loading.value = true
    await userStore.fetchUserInfo()
    loading.value = false
  }
})

const handleLogout = () => {
  userStore.logout()
  uni.reLaunch({ url: '/pages/index/index' })
}
</script>
<!-- ***ponents/ProductItem.vue -->
<template>
  <view class="product-item">
    <image :src="product.image" />
    <text>{{ product.name }}</text>
    <text>¥{{ product.price }}</text>
    <button @click="addToCart">加入购物车</button>
  </view>
</template>

<script setup>
import { useCartStore } from '@/stores/cart'

defineProps({
  product: Object
})

const cartStore = useCartStore()

const addToCart = () => {
  cartStore.addToCart(props.product)
}
</script>

六、高级功能与最佳实践

6.1 请求缓存

避免重复请求。

// utils/request.js
const cache = new Map()

// 在请求拦截器中检查缓存
request.interceptors.request.use(config => {
  if (config.cache) {
    const key = `${config.method}-${config.url}-${JSON.stringify(config.params)}`
    const cached = cache.get(key)
    if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) { // 5分钟内有效
      return Promise.resolve(cached.response)
    }
  }
  return config
})

// 在响应拦截器中存入缓存
request.interceptors.response.use(response => {
  const config = response.config
  if (config.cache) {
    const key = `${config.method}-${config.url}-${JSON.stringify(config.params)}`
    cache.set(key, {
      response,
      timestamp: Date.now()
    })
  }
  return response
})

使用:

productApi.getList({ cache: true })
6.2 请求防抖

防止用户快速点击多次提交。

// utils/debounce.js
export function debounceRequest(fn, delay = 300) {
  let timer = null
  return function (...args) {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

// 使用
const debouncedSearch = debounceRequest(() => {
  productApi.search('手机')
}, 500)
6.3 类型安全(TypeScript)
// types/api.d.ts
interface User {
  id: number
  name: string
  email: string
}

interface Product {
  id: number
  name: string
  price: number
  image: string
}

// api/user.ts
export const userApi = {
  login: (data: { username: string; password: string }) => 
    request.post<{ token: string; refreshToken: string }>('/auth/login', data),
  
  getUserInfo: () => request.get<User>('/user/info')
}

七、总结

我们成功封装了一个生产级的请求库,具备:

✅ 统一配置与拦截
✅ 自动 Token 管理
✅ 错误统一处理
✅ 与 Pinia 完美集成
✅ 支持 Token 刷新
✅ 可扩展的缓存与防抖

这套方案已在多个上线项目中稳定运行。

下一篇文章预告:《性能优化:从启动速度到内存管理的 10 个实战技巧》

我们将深入:

  • 启动速度优化(分包、预加载、骨架屏)
  • 图片懒加载与压缩
  • 虚拟列表(长列表性能优化)
  • 内存泄漏检测与避免
  • 编译与构建优化
  • 性能监控与分析

参考资料

  • Axios 官方文档
  • uni-axios GitHub
  • Pinia 官方文档

互动:你的项目中如何处理网络请求?是否实现了 Token 刷新?欢迎分享你的方案!

转载请说明出处内容投诉
CSS教程网 » 第七篇:网络请求与状态管理:封装 Axios + Pinia 实战

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买