引言:为什么不能直接用 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,很快就会遇到问题:
-
重复代码:每个请求都要写
baseURL、header、错误处理。 - 缺乏统一拦截:无法统一处理 Token、错误提示、Loading 状态。
- 难以维护:API 地址散落在各处,修改 baseUrl 时要改几十个文件。
- 类型不安全: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 刷新?欢迎分享你的方案!