前言
在 uni-app 跨端项目开发中,官方原生 uni.request API 虽开箱即用、适配多端,但多数开发者习惯在页面中直接裸写请求代码。这种开发方式在项目初期看似高效,随着业务迭代、页面增多、接口膨胀,会暴露出大量工程化问题:代码高度冗余、接口无法统一管理、Token 挂载逻辑混乱、无统一超时策略、弱网异常体验差、报错提示不统一、按钮重复请求、Token 失效重复跳转登录等问题。
尤其在同时兼容小程序、H5、App 的多端项目中,原生裸写请求无法统一各端逻辑,极易出现多端表现不一致、异常漏洞多、维护成本极高的情况。
为解决以上痛点,本文从零实现一套企业级、高可用、可直接上线的 uni-app 网络请求封装。涵盖多环境配置、统一拦截、全局超时、防重请求、Loading 防抖、分级错误处理、登录态锁、静默请求、开发日志全套能力,代码完整可直接落地项目。
一、原生 uni.request 裸写的开发痛点
- 代码极度冗余,每个页面重复写请求头、超时、错误提示
- 多环境域名硬编码,上线切换极易出错
- Token 挂载不统一,部分接口鉴权失败
- 无统一超时控制,弱网体验参差不齐
- 快速点击会触发重复请求,造成数据错乱
- 401/404/500 / 超时异常分散处理,体验混乱
- Token 过期多接口同时报错,重复跳转登录页
- 无统一日志,线上线下调试困难
- 无架构分层,后期扩展成本极高
二、本次封装设计思路
- 多环境隔离:dev/test/prod 三套域名一键切换
- 统一请求规范:统一请求头、超时、返回结构
- 防重复请求:通过 url+method + 参数唯一标识拦截重复请求
- 登录态锁:防止 Token 失效多次跳转登录页
- 分级错误处理:区分 HTTP 错误、业务错误、网络超时
- Loading 防抖:避免加载框闪烁抖动
- 静默请求:适配埋点、轮询无需弹窗场景
- 模块化架构:支持大型项目接口分层管理
三、完整核心源码 utils/request.js
javascript
运行
// ===================== 一、环境与全局配置 =====================
// 环境切换:dev 开发 / test 测试 / prod 生产
const ENV = 'dev'
// 多环境接口域名统一管理
const baseUrlMap = {
dev: 'https://dev-api.xxx.com', // 开发环境接口
test: 'https://test-api.xxx.com', // 测试环境接口
prod: 'https://prod-api.xxx.com' // 生产环境接口
}
const baseUrl = baseUrlMap[ENV]
// 全局统一请求超时时间:8000ms
const REQUEST_TIMEOUT = 8000
// 存储进行中的请求标识,用于拦截重复请求
const pendingRequest = new Set()
// 登录状态锁:防止Token失效后重复跳转登录页
let isToLogin = false
// ===================== 二、通用工具函数 =====================
/**
* 生成请求唯一标识(地址+请求方式+参数),用于防重复请求
* @param {Object} config 请求配置
* @returns {String} 唯一标识字符串
*/
function generateReqKey(config) {
const { url, method, data } = config
return `${url}-${method}-${JSON.stringify(data || {})}`
}
/**
* Loading 防抖关闭,避免加载框一闪而过、页面抖动
*/
let loadingTimer = null
function hideLoading() {
if (loadingTimer) clearTimeout(loadingTimer)
loadingTimer = setTimeout(() => {
uni.hideLoading()
}, 200)
}
// ===================== 三、核心请求主函数 =====================
/**
* 统一请求入口
* @param {Object} config 请求配置
*/
function request(config) {
// 解构配置项,设置默认值
const {
url,
data = {},
method = 'GET',
header = {},
loading = true, // 是否展示加载框
silent = false // 是否静默请求(不弹出错误提示、无loading)
} = config
// 生成请求唯一标识,拦截重复请求
const reqKey = generateReqKey(config)
if (pendingRequest.has(reqKey)) {
return Promise.reject('检测到重复请求,已拦截')
}
pendingRequest.add(reqKey)
// 拼接完整接口地址
const fullUrl = baseUrl + url
// 初始化默认请求头
const defaultHeader = {
'Content-Type': 'application/json;charset=UTF-8'
}
// 统一自动挂载登录凭证Token
const token = uni.getStorageSync('token')
if (token) {
defaultHeader.Authorization = `Bearer ${token}`
}
// 合并自定义请求头,支持个性化配置
const mergeHeader = { ...defaultHeader, ...header }
// 展示全局加载框,开启遮罩防止点击穿透
if (loading) {
uni.showLoading({
title: '请求中...',
mask: true
})
}
// 返回Promise对象,支持async/await优雅调用
return new Promise((resolve, reject) => {
uni.request({
url: fullUrl,
data,
method,
header: mergeHeader,
timeout: REQUEST_TIMEOUT,
// 网络层面请求成功(状态码 2xx)
success: (res) => {
// 清除请求记录、关闭加载框
pendingRequest.delete(reqKey)
hideLoading()
const { statusCode, data: resData } = res
// 开发环境打印请求日志,方便调试排错
if (ENV === 'dev') {
console.log(`【${method}】${fullUrl} 响应数据:`, resData)
}
// 1. 处理HTTP状态码异常
if (statusCode < 200 || statusCode >= 300) {
if (!silent) {
switch (statusCode) {
case 401:
// 未授权/Token过期,单点跳转登录页
if (!isToLogin) {
isToLogin = true
uni.showToast({ title: '登录已失效,请重新登录', icon: 'none' })
uni.removeStorageSync('token')
// 延迟跳转,保证提示框展示完成
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' })
isToLogin = false
}, 1000)
}
break
case 404:
uni.showToast({ title: '接口地址不存在', icon: 'none' })
break
case 500:
uni.showToast({ title: '服务器内部错误,请稍后重试', icon: 'none' })
break
default:
uni.showToast({ title: `请求错误 ${statusCode}`, icon: 'none' })
}
}
reject(resData)
return
}
// 2. 处理后端自定义业务状态码
const { code, data, msg } = resData
if (code === 200) {
// 业务请求成功,返回核心数据
resolve(data)
} else {
// 业务请求失败,统一弹窗提示
if (!silent) {
uni.showToast({ title: msg || '业务请求失败', icon: 'none' })
}
reject(resData)
}
},
// 网络层面请求失败:断网、超时、跨域、服务器无响应
fail: (err) => {
pendingRequest.delete(reqKey)
hideLoading()
const errMsg = err.errMsg || ''
// 开发环境打印错误日志,快速定位问题
if (ENV === 'dev') {
console.error(`【${method}】${fullUrl} 请求失败:`, err)
}
// 分级网络异常提示
if (!silent) {
if (errMsg.includes('timeout')) {
uni.showToast({ title: '请求超时,请检查网络', icon: 'none' })
} else {
uni.showToast({ title: '网络连接异常,请稍后重试', icon: 'none' })
}
}
reject(err)
}
})
})
}
// ===================== 四、导出常用请求方法 =====================
// 统一封装GET/POST/PUT/DELETE请求,简化页面调用
export default {
get(url, data, options = {}) {
return request({ url, data, method: 'GET', ...options })
},
post(url, data, options = {}) {
return request({ url, data, method: 'POST', ...options })
},
put(url, data, options = {}) {
return request({ url, data, method: 'PUT', ...options })
},
delete(url, data, options = {}) {
return request({ url, data, method: 'DELETE', ...options })
}
}
四、全局挂载 main.js
javascript
运行
import Vue from 'vue'
import App from './App'
import request from './utils/request.js'
Vue.prototype.$http = request
const app = new Vue({
...App
})
app.$mount()
五、进阶:API 模块化管理
新建 api/user.js
javascript
运行
import request from '../utils/request.js'
export function login(data) {
return request.post('/user/login', data)
}
export function getUserInfo() {
return request.get('/user/info')
}
六、多场景调用示例
1. 基础请求(带 loading)
javascript
运行
async getList() {
try {
const res = await this.$http.get('/article/list', {page:1})
console.log(res)
} catch (err) {
console.log(err)
}
}
2. 关闭 loading(下拉刷新)
javascript
运行
async onPullDownRefresh() {
const res = await this.$http.get('/article/list', {}, {loading:false})
uni.stopPullDownRefresh()
}
3. 静默请求(埋点)
javascript
运行
async report() {
await this.$http.post('/log', {}, {loading:false,silent:true})
}
七、封装亮点
- 多环境一键切换,适配开发 / 测试 / 生产
- 自动拦截重复请求,防止多次点击 BUG
- Token 自动携带、过期单点跳转登录页
- 统一超时 8 秒,弱网友好
- Loading 防抖,无闪烁
- 分级错误处理:超时、断网、401、404、500 全覆盖
- 支持静默请求、自定义请求头、灵活配置
- 结构清晰、可扩展、完全工程化
八、总结
本套 uni-app 请求封装是企业级标准工程化方案,彻底解决原生请求杂乱、难维护、多端不一致、异常处理混乱等问题,代码开箱即用、注释完善、适配全平台,适合作业、毕设、商业项目直接使用。

1106

被折叠的 条评论
为什么被折叠?



