在UniApp开发中,网络请求是不可或缺的一环。每次在页面里写一遍 uni.request ,再单独处理 loading、错误提示、令牌刷新,不仅代码重复,还容易遗漏某些边界情况。尤其是弱网环境下,请求失败后需要自动重试,或者某些公共数据需要缓存以降低服务器压力——这些逻辑如果分散在各个页面,维护起来十分费力。
Vue3 的组合式 API 提供了天然的逻辑聚合能力。本篇文章就动手封装一个功能完备的请求模块,把重试、缓存、令牌注入、统一错误处理都关进一个可复用的组合函数里。同时集成 Pinia 来管理令牌状态,让整个请求层与业务状态自然联动。所有代码可直接在 UniApp Vue3 项目中运行,兼容小程序、H5 和 App 端。
一、项目基础与目录规划
使用 HBuilderX 或 CLI 创建一个默认的 UniApp Vue3 模板。安装 Pinia 用于全局状态管理:
npm install pinia
在 main.js 中注册 Pinia:
import { createSSRApp } from 'vue';
import App from './App.vue';
import { createPinia } from 'pinia';
export function createApp() {
const app = createSSRApp(App);
app.use(createPinia());
return { app };
}
项目目录结构重点如下:
src/
├── api/ # 接口层
│ └── user.js # 用户相关接口示例
├── composables/ # 组合函数
│ └── useRequest.js # 核心请求封装
├── stores/ # Pinia 状态
│ └── user.js # 用户令牌管理
├── utils/
│ ├── http.js # uni.request 基础封装
│ └── storage.js # 跨平台缓存适配
└── pages/
二、跨平台存储适配层
小程序端没有 localStorage ,需要使用 uni.setStorageSync 和 uni.getStorageSync 作为统一接口。为了避免在请求模块里直接耦合 UniApp API,我们先写一个轻量的存储工具。
// utils/storage.js
const storage = {
get(key) {
try {
return uni.getStorageSync(key);
} catch (e) {
return null;
}
},
set(key, value) {
try {
uni.setStorageSync(key, value);
} catch (e) {
console.error('存储失败', e);
}
},
remove(key) {
try {
uni.removeStorageSync(key);
} catch (e) {}
}
};
export default storage;
这个简单的工具层保证了后续缓存功能在各端可用。
三、基础请求封装与拦截器
基于 uni.request 封装一个返回 Promise 的基础请求函数,同时抛出统一的错误格式,避免每次调用都要判断 res.statusCode 。
// utils/http.js
import storage from './storage.js';
// 基础请求,自动携带 token
function request(config) {
const token = storage.get('access_token') || '';
const header = {
'Content-Type': 'application/json',
...config.header
};
if (token) {
header['Authorization'] = `Bearer ${token}`;
}
return new Promise((resolve, reject) => {
uni.request({
...config,
header,
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data);
} else if (res.statusCode === 401) {
// 令牌过期,可在此触发刷新或跳转登录
storage.remove('access_token');
reject({ code: 401, message: '登录已过期' });
} else {
reject({
code: res.statusCode,
message: res.data?.message || '请求失败'
});
}
},
fail(err) {
reject({
code: -1,
message: err.errMsg || '网络异常'
});
}
});
});
}
export default request;
这里已经实现了最简单的拦截器:401 时清除令牌并拒绝。后续可以在 Pinia 中监听状态变化,统一跳转登录页。
四、组合式请求模块:重试与缓存核心
接下来是重头戏——用组合式 API 创建一个 useRequest 函数。它接收请求配置,返回响应式数据、loading 状态以及手动触发的方法。内部集成重试计数器和缓存命中的逻辑。
// composables/useRequest.js
import { ref, unref } from 'vue';
import request from '@/utils/http.js';
import storage from '@/utils/storage.js';
/**
* 请求组合函数
* @param {object|function} configOrFn 静态配置或返回配置的函数
* @param {object} options 额外选项
* @param {number} options.retryCount 最大重试次数,默认2
* @param {number} options.cacheTime 缓存有效期(ms),0表示不缓存
* @param {string} options.cacheKey 缓存键,若不传则根据url+参数自动生成
*/
export function useRequest(configOrFn, options = {}) {
const {
retryCount = 2,
cacheTime = 0,
cacheKey = ''
} = options;
const data = ref(null);
const error = ref(null);
const loading = ref(false);
let retryAttempt = 0;
// 根据参数生成缓存 key
function resolveCacheKey(config) {
if (cacheKey) return cacheKey;
const { url, data } = config;
return `req_cache_${url}_${JSON.stringify(data || {})}`;
}
// 检查缓存是否有效
function getCache(config) {
if (cacheTime cacheTime) {
storage.remove(key);
return null;
}
return cached.data;
}
// 存储缓存
function setCache(config, result) {
if (cacheTime <= 0) return;
const key = resolveCacheKey(config);
storage.set(key, {
data: result,
timestamp: Date.now()
});
}
async function run(overrideConfig = {}) {
const config = typeof configOrFn === 'function'
? { ...configOrFn(), ...overrideConfig }
: { ...configOrFn, ...overrideConfig };
// 先尝试读取缓存
const cached = getCache(config);
if (cached) {
data.value = cached;
return cached;
}
loading.value = true;
error.value = null;
try {
const result = await request(config);
data.value = result;
setCache(config, result);
retryAttempt = 0;
return result;
} catch (err) {
// 自动重试
if (retryAttempt < retryCount) {
retryAttempt++;
console.warn(`请求失败,正在进行第${retryAttempt}次重试...`, err);
return run(overrideConfig); // 递归重试
}
error.value = err;
throw err;
} finally {
loading.value = false;
}
}
// 清除当前缓存
function clearCache() {
const config = typeof configOrFn === 'function' ? configOrFn() : configOrFn;
const key = resolveCacheKey(config);
storage.remove(key);
}
return {
data,
error,
loading,
run,
clearCache
};
}
这个模块的核心思路:请求前先查缓存,如果命中且在有效期内就直接返回,否则发起真实请求。请求失败时自动重试,直到达到 retryCount 上限。缓存键默认根据 URL 和参数自动生成,避免重复请求。同时暴露 clearCache 方法,方便上层在需要时强制刷新。
五、Pinia 令牌管理与业务接口
我们在 Pinia 中维护用户登录后的令牌,并提供登录和退出方法。这样在请求拦截器里就可以实时获取最新令牌。
// stores/user.js
import { defineStore } from 'pinia';
import request from '@/utils/http.js';
import storage from '@/utils/storage.js';
export const useUserStore = defineStore('user', {
state: () => ({
token: storage.get('access_token') || '',
userInfo: null
}),
actions: {
async login(username, password) {
const res = await request({
url: '/api/login',
method: 'POST',
data: { username, password }
});
this.token = res.token;
storage.set('access_token', res.token);
return res;
},
logout() {
this.token = '';
this.userInfo = null;
storage.remove('access_token');
}
}
});
接着基于 useRequest 封装实际的业务接口。比如一个获取用户详情并缓存 60 秒的接口:
// api/user.js
import { useRequest } from '@/composables/useRequest.js';
export function useUserDetail(userId) {
const { data, loading, error, run, clearCache } = useRequest(
() => ({
url: `/api/user/${userId}`,
method: 'GET'
}),
{
retryCount: 1,
cacheTime: 60 * 1000, // 缓存 60 秒
cacheKey: `user_detail_${userId}`
}
);
return { user: data, loading, error, fetch: run, refresh: clearCache };
}
这里用函数形式返回配置,确保每次使用不同的 userId 时都能生成正确的缓存键。
六、页面中的调用示例
在一个用户详情页中,我们使用上面封装好的接口:
<template>
<view class="container">
<view v-if="loading">加载中...</view>
<view v-else-if="error">
<text>加载失败: {{ error.message }}</text>
<button @tap="handleRetry">重试</button>
</view>
<view v-else>
<text>昵称: {{ user?.nickname }}</text>
<text>邮箱: {{ user?.email }}</text>
<button @tap="refresh">强制刷新(清除缓存)</button>
</view>
</view>
</template>
<script setup>
import { onMounted } from 'vue';
import { useUserDetail } from '@/api/user.js';
const userId = '123';
const { user, loading, error, fetch, refresh } = useUserDetail(userId);
onMounted(() => {
fetch();
});
function handleRetry() {
fetch();
}
</script>
这里展示了一个典型的加载、错误、数据三态切换,以及手动重试和清除缓存的功能。在弱网环境下,用户点击“重试”按钮会再次发起请求;点击“强制刷新”会先清空缓存,然后重新拉取最新数据。整个交互逻辑简洁明了,且页面不包含任何 uni.request 的底层细节。
七、小程序端特殊适配
小程序环境有一些独有的限制需要处理:
- 请求超时与并发:小程序
uni.request的并发限制为 10 个,本封装模块可正常使用,但在极端场景下建议通过队列控制数量。 - Storage 容量:单个 key 数据上限为 1MB,总计 10MB。缓存数据应注意大小,避免存储二进制内容。
- 自动重试:弱网下重试次数不宜过多,避免消耗过多请求配额,默认 2 次已经足够。
- 令牌刷新:401 处理逻辑中,如果需要自动刷新令牌,可在
http.js的 401 分支中调用 Pinia 的 refreshToken 方法,但要防止并发请求同时触发多次刷新(可以加一个刷新锁)。
八、总结
通过 Vue3 组合式 API 和 Pinia 的配合,我们构建出了一个高度内聚且可复用的网络请求层。它对外只暴露 data、loading、error 等响应式状态,内部自动处理令牌注入、错误重试和缓存命中,大幅减少了业务代码中的重复逻辑。再加上跨平台的存储适配,这个模块在 UniApp 的各种端上都能平稳运行。
下一次开发 UniApp 项目时,可以直接把这个 useRequest 组合函数和 http.js 复制到新工程里,在接口文件中按模板编写业务请求,你会发现曾经散落在各处的 loading 和错误处理突然变得井井有条。

