一、封装目的
在 uni-app 项目中,接口请求频繁且业务逻辑复杂。
如果每次都直接调用 uni.request,会遇到以下问题:
-
❌ 每个页面都要手动配置 BaseURL、Header 等公共参数;
-
❌ 不能统一处理登录过期、token 校验等逻辑;
-
❌ 无法优雅地控制 loading 状态(并发、闪烁问题);
-
❌ 错误提示风格不一致;
-
❌ 重复代码过多,维护成本高。
为了提升开发体验与可维护性,我们对 uni.request 进行二次封装,
实现以下功能:
| 功能项 | 说明 |
|---|---|
| 全局默认配置 | 统一 BaseURL、超时时间、请求头 |
| 请求/响应拦截器 | 统一 token 注入与响应处理 |
| 登录过期处理 | 自动跳转登录页、清除缓存 |
| loading 控制 | 并发请求只显示一个 loading,避免闪烁 |
| 自定义实例 | 可快速创建带不同配置的请求实例 |
| GET/POST 快捷方法 | 简化调用形式 |
二、代码结构概览
文件结构建议如下:
/utils
├─ env.js // 环境配置(开发/生产)
├─ request.js // 本封装的核心文件
env.js 示例
export default {
baseURL: 'http://123.56.125.118:8300'
};
三、核心实现逻辑
1️⃣ 默认配置管理
defaultProps = {
BaseURL: env.baseURL,
timeout: 10000,
header: {},
isLoading: true, // 是否显示loading
};
-
BaseURL:项目全局接口地址,统一管理。
-
timeout:请求超时时间(毫秒)。
-
header:默认请求头,可在实例化时追加。
-
isLoading:是否在该请求中显示加载提示,可在单个请求中控制。
2️⃣ 请求与响应拦截器
请求拦截(request)
在每次发起请求前自动携带 token。
request: (options) => {
let token = uni.getStorageSync('token');
if (token) options.header['token'] = token;
return options;
}
📘 意义 :
无需在每个请求中手动写入 token,实现统一认证。
响应拦截(response)
对后端返回的数据进行集中处理。
response: (res) => {
if (res.isSuccess) {
let code = res.data.code;
switch (code) {
case 200:
return res.data;
case 208:
case 401:
return this.timeoutHandler(res.data);
default:
uni.showToast({
title: '程序出现错误',
icon: 'none'
});
}
} else {
uni.showToast({
title: '请求失败',
icon: 'none'
});
return Promise.reject(res);
}
}
📘 意义 :
统一处理后端业务状态码,例如:
-
200:成功返回; -
401 / 208:登录过期,触发跳转; -
其他状态:友好提示。
3️⃣ Loading 并发控制与防闪烁
核心机制:
// 当前进行中的请求数量
loadingCount = 0;
// 防止连续请求时闪烁
timer = null;
逻辑说明
| 场景 | 行为 |
|---|---|
| 第一个请求开始 | 显示 Loading |
| 并发多个请求 | loadingCount 递增,不重复显示 |
| 每个请求完成 | loadingCount 递减 |
| 所有请求结束 | 延迟 400ms 后隐藏 Loading(防止闪烁) |
实现代码
if (requestOptions.isLoading) {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.loadingCount++;
if (this.loadingCount === 1) {
uni.showLoading({ title: '加载中', mask: true });
}
}
并在请求的 complete 回调中计数递减:
complete: () => {
if (requestOptions.isLoading) {
this.loadingCount = Math.max(this.loadingCount - 1, 0);
if (this.loadingCount === 0) {
this.timer = setTimeout(() => {
uni.hideLoading();
this.timer = null;
}, 400);
}
}
}
📘 意义 :
解决了「并发多次 loading」与「连续请求闪烁」两大问题,
提升交互流畅度。
4️⃣ 登录过期自动处理
timeoutHandler(res) {
uni.clearStorageSync();
uni.navigateTo({ url: '/pages/login/index' });
uni.showToast({
title: '登录过期,请重新登录',
icon: 'none',
});
return res;
}
📘 意义 :
后端返回 401/208 时,自动清空缓存并跳转登录页,
无需在业务层单独判断。
5️⃣ 提供简洁调用方法
get(options) {
return this.request({ ...options, method: 'GET' });
}
post(options) {
return this.request({ ...options, method: 'POST' });
}
📘 意义 :
简化业务层调用,如:
WxRequest.get({ url: '/user/info' });
WxRequest.post({ url: '/login', data: form });
6️⃣ 自定义实例化
const customInstance = (options) => {
return new WxRequestClass(options);
};
📘 意义 :
当你需要访问不同后端服务或配置(如文件上传、mock 服务)时:
const uploadRequest = customInstance({
BaseURL: 'http://upload.api.com',
header: { 'Content-Type': 'multipart/form-data' }
});
四、使用示例
1️⃣ 基础 GET 请求
WxRequest.get({
url: '/user/profile',
}).then(res => {
console.log('用户资料', res);
});
2️⃣ POST 请求(带参数)
WxRequest.post({
url: '/login',
data: {
username: 'admin',
password: '123456'
}
});
3️⃣ 禁止 loading 的请求
WxRequest.get({
url: '/home/banner',
isLoading: false // 不显示 loading
});
4️⃣ 并发请求示例
WxRequest.all(
WxRequest.get({ url: '/user/info' }),
WxRequest.get({ url: '/user/setting' })
).then(([info, setting]) => {
console.log(info, setting);
});
📘 效果 :
两个请求同时发起,只显示一个"加载中",
等两个请求都完成后再隐藏。
完整代码
import env from './env.js';
class WxRequestClass {
// 存放一些默认配置:根地址、超时时间、请求头,这些属于全局配置一般情况下是不会改变的
defaultProps = {
BaseURL: env.baseURL,
timeout: 10000,
header: {},
isLoading: true, // 是否显示loading效果
};
// 用来解决高并发请求时,从开始请求显示loading,到全部完成隐藏loading
loadingCount = 0;
// 解决连续请求时,看到的闪烁loading问题 (相当于防抖)
timer = null;
// 配置请求、响应拦截器
interceptor = {
request: (options) => {
let token = uni.getStorageSync('token');
if (token) {
options.header['token'] = token;
}
return options;
},
response: (res) => {
if (res.isSuccess) {
let code = res.data.code;
switch (code) {
case 200:
return res.data;
case 208:
return this.timeoutHandler(res.data);
case 401:
return this.timeoutHandler(res.data);
default:
// return uni.showToast({
// title: '程序出现错误,正在修复',
// icon: 'none',
// duration: 2000,
// });
}
} else {
uni.showToast({
title: '请求失败',
icon: 'none',
duration: 2000,
});
return Promise.reject(res);
}
},
};
constructor(defaultProps1) {
if (defaultProps1) {
this.defaultProps = Object.assign({}, this.defaultProps, defaultProps1);
}
}
request(options) {
let requestOptions = Object.assign({}, this.defaultProps, options);
requestOptions.url = requestOptions.BaseURL + (options.url.startsWith('/') ? options.url : '/' + options.url);
let interceptorRequest = this.interceptor.request(requestOptions);
if (requestOptions.isLoading) {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.loadingCount++;
console.log(this.loadingCount, 'this.loadingCount当前数量');
if (this.loadingCount === 1) {
uni.showLoading({
title: '加载中',
mask: true,
});
}
}
return new Promise(async (resolve, reject) => {
try {
uni.request({
...interceptorRequest,
success: (res) => {
let interceptorResponse = this.interceptor.response({
...res,
config: interceptorRequest,
isSuccess: true
});
resolve(interceptorResponse);
},
fail: (err) => {
let interceptorResponse = this.interceptor.response({
...err,
config: interceptorRequest,
isSuccess: false
});
reject(interceptorResponse);
},
complete: () => {
if (requestOptions.isLoading) {
this.loadingCount--;
if (this.loadingCount === 0) {
this.timer = setTimeout(() => {
uni.hideLoading();
this.timer = null;
}, 400);
}
}
},
});
} catch (error) {
reject(error);
}
});
}
// get 请求
get(options) {
let requestOptions = Object.assign({}, options, {
method: 'GET',
});
return this.request(requestOptions);
}
// post 请求
post(options) {
let requestOptions = Object.assign({}, options, {
method: 'POST',
});
return this.request(requestOptions);
}
// 处理并发请求
all(...promises) {
return Promise.all(promises);
}
// 处理登录过期
timeoutHandler(res) {
uni.clearStorageSync();
uni.navigateTo({
url: '/pages/login/index',
});
uni.showToast({
title: '登录过期,请重新登录',
icon: 'none',
duration: 2000,
});
return res;
}
}
// 实例化
let WxRequest = new WxRequestClass();
// 用来自定义配置,想怎么搞就怎么搞
const customInstance = (options) => {
return new WxRequestClass(options);
};
export {
customInstance
};
export default WxRequest;
五、封装优点总结
| 功能点 | 说明 |
|---|---|
| ✅ 全局统一配置 | BaseURL、超时时间、请求头集中管理 |
| ✅ 自动 token 注入 | 登录状态自动携带 |
| ✅ 登录过期处理 | 自动跳转、提示用户 |
| ✅ 并发 loading 管理 | 多请求只显示一个 loading |
| ✅ 防闪烁机制 | 连续请求无闪烁 |
| ✅ 支持无 loading 请求 | 灵活可控 |
| ✅ 支持多实例配置 | 方便扩展不同服务场景 |