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',timeout: 10000,showLoading: true, // 默认显示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, mask: true }) } }// 隐藏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({request: async (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({response: async (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', { userId: 123 })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', {}, {showLoading: false,needSign: false })}
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 签名算法与后端不一致
签名错误是最常见的问题,核心原因是前后端规则未对齐,比如参数排序、空值处理、编码方式。建议:
-
与后端一起确定签名规则,写成文档 -
将签名函数独立出来,写单元测试验证 -
空值参数不参与签名,避免后端过滤后签名不一致
5.4 跨平台兼容问题
UniApp 适配多端,要注意这些细节:
-
微信小程序有10 个并发请求限制,计数器不受影响,业务层按需控制即可 -
H5 端的 uni.showLoading不支持过长的 title,loading 文字简洁为主 -
部分小程序平台不支持请求头下划线,建议用中划线 / 驼峰命名 -
uni.getSystemInfoSync实测所有主流端都支持,可放心使用
六、常见错误及解决方案
除了核心难点,这些低级错误也容易踩,整理了对应的现象和解决办法,快速排障:❌ 错误 1:try/catch 捕获不到接口错误原因:响应拦截器未正确抛出 Promise.reject解决方案:确保拦截器中错误处理都返回Promise.reject(new Error())
❌ 错误 2:loading 一直显示不消失原因:请求计数器错乱,比如网络错误未执行_hideLoading解决方案:所有请求结束(成功 / 失败)都要执行 loading 隐藏,计数器加判断if (this.requestCount > 0)
❌ 错误 3:响应拦截器修改原始数据原因:直接操作 res.data,导致后续需要原始数据时丢失解决方案:不修改原始响应对象,直接返回处理后的业务数据
❌ 错误 4:小程序请求头报错原因:部分平台(如支付宝小程序)不支持请求头下划线解决方案:统一将请求头字段改为中划线,如token、time-stamp
七、进阶扩展:让封装工具更强大
我们实现的封装是基础版,满足 90% 的日常开发需求,在此基础上,还可以根据项目需要扩展这些实用功能,让工具更完善:
-
请求重试:对网络错误、500 服务端错误实现自动重试,可配置重试次数 -
请求缓存:对 GET 请求做本地缓存,避免重复请求,提升性能 -
上传下载封装:基于 uni.uploadFile/uni.downloadFile封装,复用拦截器和 loading 逻辑 -
离线请求队列:网络断开时,将请求加入队列,网络恢复后自动重发 -
请求取消:实现类似 axios 的取消请求,解决页面跳转后请求还在执行的问题 -
请求日志:开发环境打印详细的请求 / 响应日志,生产环境关闭,方便调试
写在最后
好的代码封装,从来不是把代码藏起来,而是把复杂度藏起来,让开发人员专注于业务逻辑,而不是重复的底层操作。
一个优雅的全局请求封装,不仅能让你的 UniApp 项目代码更整洁,还能提升开发效率、降低维护成本,这也是从 “初级开发” 到 “中级开发” 的重要一步 —— 学会用架构思维解决问题,而不是埋头敲重复代码。
希望这篇实战教程能帮你告别混乱的请求代码,让你的 UniApp 开发之路更顺畅。后续还会分享更多 UniApp 跨平台开发的实战技巧,从入门到精通,带你搞定小程序、APP、H5 全端开发!
关注我,不迷路,下次开发少踩坑~
夜雨聆风