uni-app企业级网络请求封装:统一拦截、超时配置与全局错误处理

前言

在 uni-app 跨端项目开发中,官方原生 uni.request API 虽开箱即用、适配多端,但多数开发者习惯在页面中直接裸写请求代码。这种开发方式在项目初期看似高效,随着业务迭代、页面增多、接口膨胀,会暴露出大量工程化问题:代码高度冗余、接口无法统一管理、Token 挂载逻辑混乱、无统一超时策略、弱网异常体验差、报错提示不统一、按钮重复请求、Token 失效重复跳转登录等问题。

尤其在同时兼容小程序、H5、App 的多端项目中,原生裸写请求无法统一各端逻辑,极易出现多端表现不一致、异常漏洞多、维护成本极高的情况。

为解决以上痛点,本文从零实现一套企业级、高可用、可直接上线的 uni-app 网络请求封装。涵盖多环境配置、统一拦截、全局超时、防重请求、Loading 防抖、分级错误处理、登录态锁、静默请求、开发日志全套能力,代码完整可直接落地项目。

一、原生 uni.request 裸写的开发痛点

  1. 代码极度冗余,每个页面重复写请求头、超时、错误提示
  2. 多环境域名硬编码,上线切换极易出错
  3. Token 挂载不统一,部分接口鉴权失败
  4. 无统一超时控制,弱网体验参差不齐
  5. 快速点击会触发重复请求,造成数据错乱
  6. 401/404/500 / 超时异常分散处理,体验混乱
  7. Token 过期多接口同时报错,重复跳转登录页
  8. 无统一日志,线上线下调试困难
  9. 无架构分层,后期扩展成本极高

二、本次封装设计思路

  1. 多环境隔离:dev/test/prod 三套域名一键切换
  2. 统一请求规范:统一请求头、超时、返回结构
  3. 防重复请求:通过 url+method + 参数唯一标识拦截重复请求
  4. 登录态锁:防止 Token 失效多次跳转登录页
  5. 分级错误处理:区分 HTTP 错误、业务错误、网络超时
  6. Loading 防抖:避免加载框闪烁抖动
  7. 静默请求:适配埋点、轮询无需弹窗场景
  8. 模块化架构:支持大型项目接口分层管理

三、完整核心源码 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})
}

七、封装亮点

  1. 多环境一键切换,适配开发 / 测试 / 生产
  2. 自动拦截重复请求,防止多次点击 BUG
  3. Token 自动携带、过期单点跳转登录页
  4. 统一超时 8 秒,弱网友好
  5. Loading 防抖,无闪烁
  6. 分级错误处理:超时、断网、401、404、500 全覆盖
  7. 支持静默请求、自定义请求头、灵活配置
  8. 结构清晰、可扩展、完全工程化

八、总结

本套 uni-app 请求封装是企业级标准工程化方案,彻底解决原生请求杂乱、难维护、多端不一致、异常处理混乱等问题,代码开箱即用、注释完善、适配全平台,适合作业、毕设、商业项目直接使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值