乐于分享
好东西不私藏

UniApp 全局请求封装实战:告别屎山代码,让接口调用优雅到飞起

UniApp 全局请求封装实战:告别屎山代码,让接口调用优雅到飞起

做 UniApp 跨平台开发的小伙伴,是不是总被网络请求的代码折磨?明明只是调个接口,却要反复写 token、时间戳,每个页面都要处理一遍错误码,多个请求并发时 loading 闪个不停……

直接用uni.request确实能实现功能,但真实项目里这么写,代码用不了多久就会变成乱糟糟的 “屎山”,后期维护全是坑。AI 编程盛行的当下,我们拼的不只是敲代码的速度,更是把控全局的架构设计能力 —— 而一个优雅的全局请求封装,就是移动端跨平台开发的基础必修课。

今天就带大家从零搭建一个适配 H5 / 小程序 / App的 UniApp 全局请求封装工具,把公共参数、加签验签、错误处理、loading 管理这些重复工作全统一搞定,让接口调用从此变简单!

一、为啥一定要封装?这些痛点你绝对踩过

先看看没封装时,一个普通的接口调用要写多少冗余代码:

uni.request({url'https://api.example.com/user/info',method'GET',header: {'token': uni.getStorageSync('token'),'timestamp'Date.now(),'sign': generateSign(params)  },success(res) => {if (res.data.code === 200) {// 成功处理    } elseif (res.data.code === 401) {// 跳转登录    } else {      uni.showToast({ title: res.data.message })    }  },fail(err) => {    uni.showToast({ title'网络错误' })  },complete() => {    uni.hideLoading()  }})

token 要手动取、错误码要挨个判断、loading 要手动关,每个接口都重复这套操作,不仅效率低,还容易出漏子。

而做好请求封装,能一次性解决这些核心痛点:✅ 统一处理公共参数,自动加 token、时间戳、设备信息✅ 统一加签 / 验签,防止请求参数被篡改✅ 统一加解密,保障敏感数据传输安全✅ 统一错误码处理,自动提示、无感跳转登录✅ 智能 loading 管理,并发请求只显一个,自动关闭✅ 极致代码复用,一处修改,所有接口同步生效

二、核心设计思路:抄 axios 的拦截器,真香!

我们的封装核心思路很简单 —— 模仿 axios 的请求 / 响应拦截器模式,在请求发出前、响应返回后插入自定义逻辑,让整个请求流程可配置、可扩展。

再搭配一个请求计数器,解决并发 loading 的管理问题,整个请求流程清晰可控:发起请求 → 请求拦截器(加公共参数 / 加签 / 显 loading) → uni.request 原生请求 → 响应拦截器(解错误码 / 解密 / 隐 loading) → 纯业务数据返回给调用者

这种模式的好处是解耦性极强,后续要加新功能,只需要新增拦截器,不用动原有业务代码。

三、从零实现:核心代码一步到位

我们的封装代码都放在utils/request.js中,全程面向对象开发,结构清晰,易扩展。先搭骨架,再填功能,新手也能跟着敲。

3.1 基础结构:搭建请求类的核心骨架

先创建 Request 类,初始化拦截器容器、全局配置、请求计数器,实现核心的请求方法和 loading 管理方法:

classRequest{constructor() {// 拦截器容器:请求/响应this.interceptors = {request: [],response: []    }// 全局默认配置this.config = {baseURL'https://api.example.com',timeout10000,showLoadingtrue// 默认显示loadingloadingText'加载中...',header: {'Content-Type''application/json'      }    }this.requestCount = 0// 请求计数器,控制loadingthis._initDefaultInterceptors() // 初始化默认拦截器  }// 注册拦截器  use(interceptor) {if (interceptor.request) {this.interceptors.request.push(interceptor.request)    }if (interceptor.response) {this.interceptors.response.push(interceptor.response)    }  }// 显示loading:计数器为1时才显示,避免并发闪烁  _showLoading(text) {this.requestCount++if (this.requestCount === 1) {      uni.showLoading({ title: text || this.config.loadingText, masktrue })    }  }// 隐藏loading:计数器归0时才隐藏  _hideLoading() {if (this.requestCount > 0) {this.requestCount--    }if (this.requestCount === 0) {      uni.hideLoading()    }  }// 核心请求方法:执行拦截器链+原生请求async request(options) {// 合并全局配置和当前请求配置const mergedOptions = { ...this.config, ...options }// 构建请求拦截器链let chain = Promise.resolve(mergedOptions)this.interceptors.request.forEach(interceptor => {      chain = chain.then(interceptor)    })// 执行原生uni.request    chain = chain.then(config => {returnnewPromise((resolve, reject) => {        uni.request({url: config.baseURL + config.url,method: config.method || 'GET',data: config.data,header: config.header,timeout: config.timeout,success(res) => {            res.config = config // 把配置挂载到响应,方便拦截器使用            resolve(res)          },fail(err) => {// 网络错误也要隐藏loadingif (config.showLoading) {this._hideLoading()            }            reject(err)          }        })      })    })// 构建响应拦截器链this.interceptors.response.forEach(interceptor => {      chain = chain.then(interceptor, (err) => {returnPromise.reject(err)      })    })return chain  }// 快捷请求方法:GET/POST/PUT/DELETE,简化调用get(url, data, options = {}) {returnthis.request({ ...options, url, method'GET', data })  }  post(url, data, options = {}) {returnthis.request({ ...options, url, method'POST', data })  }  put(url, data, options = {}) {returnthis.request({ ...options, url, method'PUT', data })  }delete(url, data, options = {}) {returnthis.request({ ...options, url, method'DELETE', data })  }}exportdefaultnew Request()

3.2 初始化默认拦截器:实现核心公共功能

上面的骨架只是基础,真正的核心功能都在请求 / 响应拦截器里。我们在类中实现_initDefaultInterceptors方法,自动添加公共参数、签名、错误码处理等逻辑,一劳永逸。

// 在Request类中添加该方法,需先安装md5:npm install md5import md5 from'md5'_initDefaultInterceptors() {// 👉 请求拦截器:加公共参数、生成签名this.use({requestasync (config) => {const token = uni.getStorageSync('token') || ''const timestamp = Date.now()// 自动添加公共请求头      config.header = {        ...config.header,'token': token,'timestamp': timestamp,'platform': uni.getSystemInfoSync().platform // 设备平台:微信/APP/H5      }// 自动签名(可通过needSign: false禁用)if (config.needSign !== false) {const sign = this._generateSign(config.data, timestamp, token)        config.header['sign'] = sign      }// 显示loading(可通过showLoading: false禁用)if (config.showLoading) {this._showLoading(config.loadingText)      }return config    }  })// 👉 响应拦截器:统一处理错误码、隐藏loadingthis.use({responseasync (res) => {// 隐藏loadingif (res.config?.showLoading) {this._hideLoading()      }// 处理http网络错误(非200状态码)if (res.statusCode !== 200) {        uni.showToast({ title'网络异常'icon'none' })returnPromise.reject(newError('网络异常'))      }// 处理业务错误码const { code, message, data } = res.dataswitch (code) {case200// 成功:只返回业务数据,简化页面处理return datacase401// 未登录/token过期:跳转登录,避免无限循环          uni.showToast({ title'登录已过期'icon'none' })const pages = getCurrentPages()const currentPage = pages[pages.length - 1]?.routeif (currentPage !== 'pages/login/login') {            setTimeout(() => {              uni.navigateTo({ url'/pages/login/login' })            }, 1500)          }returnPromise.reject(newError('登录过期'))case403// 无权限          uni.showToast({ title'暂无权限'icon'none' })returnPromise.reject(newError('权限不足'))default// 其他业务错误:显示后端提示信息          uni.showToast({ title: message || '系统错误'icon'none' })returnPromise.reject(newError(message))      }    }  })}// 签名生成方法(与后端约定规则,示例为MD5签名)_generateSign(data, timestamp, token) {if (!data) return''// 参数按字典序排序,避免签名不一致const keys = Object.keys(data).sort()let str = ''  keys.forEach(key => {if (data[key] !== undefined && data[key] !== null) {      str += `${key}=${data[key]}&`    }  })// 拼接时间戳和token,生成签名串  str += `timestamp=${timestamp}&key=${token}`return md5(str)}

3.3 完整代码

将上述两部分代码合并,就是完整的utils/request.js,直接复制到项目中,安装md5依赖(npm install md5)即可使用。

四、极简使用:一行代码调接口

封装完成后,页面中调用接口的代码会极度简化,不用再处理任何冗余逻辑,专注业务即可。

4.1 基础调用示例

<!-- pages/index/index.vue --><scriptsetup>import request from'@/utils/request'// 获取用户信息const getUserInfo = async () => {try {// GET请求const data = await request.get('/user/info', { userId123 })console.log('用户信息:', data)// POST请求// const res = await request.post('/user/login', { username: 'test', pwd: '123456' })  } catch (err) {// 异常处理(可选,拦截器已做全局提示)console.error('请求失败:', err)  }}</script>

4.2 灵活配置:禁用 loading / 签名

针对个别请求,可灵活覆盖全局配置,比如不需要显示 loading、不需要签名:

// 禁用loading和签名const fetchData = async () => {const data = await request.get('/home/banner', {}, {showLoadingfalse,needSignfalse  })}

4.3 修改全局配置

项目中需要切换接口域名时,直接修改全局配置即可,所有接口同步生效:

// 比如在app.vue中修改baseURLimport request from'@/utils/request'request.config.baseURL = 'https://new-api.example.com'

五、开发难点:这些坑一定要避开

封装过程中,很多小伙伴会踩一些细节坑,这里整理了核心难点的解决方案,让你的封装工具更健壮。

5.1 并发请求的 loading 管理

用计数器控制 loading 是最优解,但要注意:网络错误时也要执行_hideLoading,否则计数器会错乱,导致 loading 一直显示。我们在代码中已经做了这个处理。

5.2 token 过期的无限循环跳转

如果登录页也调用了需要 token 的接口,401 拦截器会反复跳转登录,解决方案是:判断当前页面是否为登录页,若是则不跳转

5.3 签名算法与后端不一致

签名错误是最常见的问题,核心原因是前后端规则未对齐,比如参数排序、空值处理、编码方式。建议:

  1. 与后端一起确定签名规则,写成文档
  2. 将签名函数独立出来,写单元测试验证
  3. 空值参数不参与签名,避免后端过滤后签名不一致

5.4 跨平台兼容问题

UniApp 适配多端,要注意这些细节:

  1. 微信小程序有10 个并发请求限制,计数器不受影响,业务层按需控制即可
  2. H5 端的uni.showLoading不支持过长的 title,loading 文字简洁为主
  3. 部分小程序平台不支持请求头下划线,建议用中划线 / 驼峰命名
  4. uni.getSystemInfoSync实测所有主流端都支持,可放心使用

六、常见错误及解决方案

除了核心难点,这些低级错误也容易踩,整理了对应的现象和解决办法,快速排障:❌ 错误 1:try/catch 捕获不到接口错误原因:响应拦截器未正确抛出 Promise.reject解决方案:确保拦截器中错误处理都返回Promise.reject(new Error())

❌ 错误 2:loading 一直显示不消失原因:请求计数器错乱,比如网络错误未执行_hideLoading解决方案:所有请求结束(成功 / 失败)都要执行 loading 隐藏,计数器加判断if (this.requestCount > 0)

❌ 错误 3:响应拦截器修改原始数据原因:直接操作 res.data,导致后续需要原始数据时丢失解决方案:不修改原始响应对象,直接返回处理后的业务数据

❌ 错误 4:小程序请求头报错原因:部分平台(如支付宝小程序)不支持请求头下划线解决方案:统一将请求头字段改为中划线,如tokentime-stamp

七、进阶扩展:让封装工具更强大

我们实现的封装是基础版,满足 90% 的日常开发需求,在此基础上,还可以根据项目需要扩展这些实用功能,让工具更完善:

  1. 请求重试:对网络错误、500 服务端错误实现自动重试,可配置重试次数
  2. 请求缓存:对 GET 请求做本地缓存,避免重复请求,提升性能
  3. 上传下载封装:基于uni.uploadFile/uni.downloadFile封装,复用拦截器和 loading 逻辑
  4. 离线请求队列:网络断开时,将请求加入队列,网络恢复后自动重发
  5. 请求取消:实现类似 axios 的取消请求,解决页面跳转后请求还在执行的问题
  6. 请求日志:开发环境打印详细的请求 / 响应日志,生产环境关闭,方便调试

写在最后

好的代码封装,从来不是把代码藏起来,而是把复杂度藏起来,让开发人员专注于业务逻辑,而不是重复的底层操作。

一个优雅的全局请求封装,不仅能让你的 UniApp 项目代码更整洁,还能提升开发效率、降低维护成本,这也是从 “初级开发” 到 “中级开发” 的重要一步 —— 学会用架构思维解决问题,而不是埋头敲重复代码。

希望这篇实战教程能帮你告别混乱的请求代码,让你的 UniApp 开发之路更顺畅。后续还会分享更多 UniApp 跨平台开发的实战技巧,从入门到精通,带你搞定小程序、APP、H5 全端开发!

关注我,不迷路,下次开发少踩坑~