uni-app 封装http请求

news2024/9/21 4:30:13

1.引言

前面一篇文章写了使用Pinia进行全局状态管理。

这篇文章主要介绍一下封装http请求,发送数据请求到服务端进行数据的获取。

感谢:

1.yudao-mall-uniapp: 芋道商城,基于 Vue + Uniapp 实现,支持分销、拼团、砍价、秒杀、优惠券、积分、会员等级、小程序直播、页面 DIY 等功能,100% 开源

2.3.x文档 | luch-request

3.Day1-01-uni-app小兔鲜儿导学视频_哔哩哔哩_bilibili

2.token过期后的重新获取思路

在进行登录后,通过本地缓存,存储获取到的accessToken与refreshToken,accessToken的过期时间为30分钟,refreshToken过期时间为30天。在每次发送请求时,通过http的请求拦截器,放入accessToken进入header中,后端进行校验,当accessToken过期后,后端返回的封装中,code为401,此时应该用refreshToken无感知刷新accessToken继续本次的请求,当refreshToken也过期后,就需要用户重新进行登录。

3.代码

代码主要介绍三个部分,第一部分是自定义http的请求拦截器与响应拦截器,第二部分是封装http的请求,第三部分是如何发送具体的请求。

1.自定义拦截器

请求拦截器主要定义发送请求时的参数,响应拦截器主要处理返回时各种情况。具体可查看文档

import { getRefreshToken, getAccessToken, setAccessToken } from '@/utils/auth'
import { platform } from '@/utils/platform'
import { useUserStore } from '@/store'
import Request from 'luch-request'
import * as authApi from '@/api/auth'

const options = {
  // 显示操作成功消息 默认不显示
  showSuccess: false,
  // 成功提醒 默认使用后端返回值
  successMsg: '',
  // 显示失败消息 默认显示
  showError: true,
  // 失败提醒 默认使用后端返回信息
  errorMsg: '',
  // 显示请求时loading模态框 默认显示
  showLoading: true,
  // loading提醒文字
  loadingMsg: '加载中',
  // 需要授权才能请求 默认放开
  auth: false,
  // ...
}

// Loading全局实例
const LoadingInstance = {
  target: null,
  count: 0,
}

/**
 * 关闭loading
 */
function closeLoading() {
  if (LoadingInstance.count > 0) LoadingInstance.count--
  if (LoadingInstance.count === 0) uni.hideLoading()
}
/**
 * @description 请求基础配置 可直接使用访问自定义请求
 */
const http = new Request({
  // 请求基准地址
  baseURL: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL,
  timeout: 8000,
  header: {
    Accept: '*/*',
    'Content-Type': 'application/json;charset=UTF-8',
    platform,
  },
  // #ifdef APP-PLUS
  sslVerify: false,
  // #endif
  // #ifdef H5
  // 跨域请求时是否携带凭证(cookies)仅H5支持(HBuilderX 2.6.15+)
  withCredentials: false,
  // #endif
  custom: options,
})

/**
 * @description 请求拦截器
 */
http.interceptors.request.use(
  (config) => {
    // 自定义处理【loading 加载中】:如果需要显示 loading,则显示 loading
    if (config.custom.showLoading) {
      LoadingInstance.count++
      LoadingInstance.count === 1 &&
        uni.showLoading({
          title: config.custom.loadingMsg,
          mask: true,
          fail: () => {
            uni.hideLoading()
          },
        })
    }
    // 添加 token 请求头标识
    const token = getAccessToken()
    if (token) {
      config.header.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  },
)

/**
 * @description 响应拦截器
 */
http.interceptors.response.use(
  (response) => {
    // 自定处理【loading 加载中】:如果需要显示 loading,则关闭 loading
    response.config.custom.showLoading && closeLoading()
    // 返回结果:包括 code + data + msg
    const resData = response.data
    const code = resData.code
    if (code === 200) {
      return Promise.resolve(response.data)
    } else if (code === 401) {
      return refreshToken(response.config)
    } else {
      uni.showToast({
        title: resData.message || '出错啦!',
        icon: 'none',
        mask: true,
      })
    }
  },
  (error) => {
    let errorMessage = '网络请求出错'
    if (error !== undefined) {
      switch (error.statusCode) {
        case 400:
          errorMessage = '请求错误'
          break
        case 401:
          errorMessage = '请登录'
          // 正常情况下,后端不会返回 401 错误,所以这里不处理 handleAuthorized
          break
        case 403:
          errorMessage = '拒绝访问'
          break
        case 404:
          errorMessage = '请求出错'
          break
        case 408:
          errorMessage = '请求超时'
          break
        case 429:
          errorMessage = '请求频繁, 请稍后再访问'
          break
        case 500:
          errorMessage = '服务器开小差啦,请稍后再试~'
          break
        case 501:
          errorMessage = '服务未实现'
          break
        case 502:
          errorMessage = '网络错误'
          break
        case 503:
          errorMessage = '服务不可用'
          break
        case 504:
          errorMessage = '网络超时'
          break
        case 505:
          errorMessage = 'HTTP 版本不受支持'
          break
      }
      if (error.errMsg.includes('timeout')) errorMessage = '请求超时'
      // #ifdef H5
      if (error.errMsg.includes('Network'))
        errorMessage = window.navigator.onLine ? '服务器异常' : '请检查您的网络连接'
      // #endif
    }

    if (error && error.config) {
      if (error.config.custom.showError === false) {
        uni.showToast({
          title: error.data?.msg || errorMessage,
          icon: 'none',
          mask: true,
        })
      }
      error.config.custom.showLoading && closeLoading()
    }

    return false
  },
)

// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
let requestList = [] // 请求队列
let isRefreshToken = false // 是否正在刷新中
const refreshToken = async (config) => {
  // 如果当前已经是 refresh-token 的 URL 地址,并且还是 401 错误,说明是刷新令牌失败了,直接返回 Promise.reject(error)
  if (config.url.indexOf('/auth/refresh-token') >= 0) {
    isRefreshToken = false
    uni.navigateTo({ url: '/pages/login/index' })
    return Promise.reject(new Error('error'))
  }

  console.log('过期', config)
  // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
  if (!isRefreshToken) {
    isRefreshToken = true
    // 1. 如果获取不到刷新令牌,则只能执行登出操作
    const refreshToken = getRefreshToken()
    if (!refreshToken) {
      return handleAuthorized()
    }
    // 2. 进行刷新访问令牌
    const refreshTokenData = reactive({
      refreshToken: getRefreshToken(),
      clientId: import.meta.env.VITE_CLIENT_ID,
    })
    const res = await authApi.refreshToken(refreshTokenData)
    console.log(res)
    setAccessToken(res.data.accessToken)
    try {
      // 2.1 刷新成功,则回放队列的请求 + 当前请求
      config.header.Authorization = 'Bearer ' + getAccessToken()
      requestList.forEach((cb) => {
        cb()
      })
      requestList = []
      return request(options)
    } catch (e) {
      // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
      // 2.2 刷新失败,只回放队列的请求
      requestList.forEach((cb) => {
        cb()
      })
      // 提示是否要登出。即不回放当前请求!不然会形成递归
      return handleAuthorized()
    } finally {
      requestList = []
      isRefreshToken = false
    }
  } else {
    // 添加到队列,等待刷新获取到新的令牌
    return new Promise((resolve) => {
      console.log('重试', config)
      requestList.push(() => {
        config.header.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
        resolve(request(options))
      })
    })
  }
}

/**
 * 处理 401 未登录的错误
 */
const handleAuthorized = () => {
  const userStore = useUserStore()
  userStore.userLogout()
  isRefreshToken = false
  // 是否进入登录页
  uni.showModal({
    title: '提示',
    content: '重新登录?',
    success: function (res) {
      if (res.confirm) {
        uni.navigateTo({ url: '/pages/login/index' })
      }
    },
  })

  // 登录超时
  return new Promise<IResData<boolean>>((resolve, reject) => {
    const res: IResData<boolean> = {
      code: 401,
      message: '请重新登录',
      data: false,
    }
    reject(res)
  })
}

const request = (config) => {
  return http.middleware(config)
}

export default request
auth.ts

/**
 * 存储用户身份信息令牌
 */

export const CACHE_KEY = {
  ACCESS_TOKEN: 'access_token',
  REFRESH_TOKEN: 'refresh_token',
}

// 存储访问令牌
export const setAccessToken = (accessToken: string) => {
  uni.setStorageSync(CACHE_KEY.ACCESS_TOKEN, accessToken)
}

// 存储刷新令牌
export const setRefreshToken = (refreshToken: string) => {
  uni.setStorageSync(CACHE_KEY.REFRESH_TOKEN, refreshToken)
}

// 获取访问令牌
export const getAccessToken = () => {
  return uni.getStorageSync(CACHE_KEY.ACCESS_TOKEN)
}

// 获取刷新令牌
export const getRefreshToken = () => {
  return uni.getStorageSync(CACHE_KEY.REFRESH_TOKEN)
}

// 清理本地所有缓存
export const clearStorage = () => {
  uni.clearStorageSync()
}

2.封装http请求

/**
 * 封装不同类型的restful请求
 */

import request from './request'


// 全局要用的类型放到这里

type IResData<T> = {
  code: number
  message: string
  data: T
}

export default {
  get: async <T = any>(options: any) => {
    const res = await request({ method: 'GET', ...options })
    return res as unknown as IResData<T>
  },
  post: async <T = any>(option: any) => {
    const res = await request({ method: 'POST', ...option })
    return res as unknown as IResData<T>
  },
  postOriginal: async (option: any) => {
    const res = await request({ method: 'POST', ...option })
    return res
  },
  delete: async <T = any>(option: any) => {
    const res = await request({ method: 'DELETE', ...option })
    return res as unknown as IResData<T>
  },
  put: async <T = any>(option: any) => {
    const res = await request({ method: 'PUT', ...option })
    return res as unknown as IResData<T>
  },
  download: async <T = any>(option: any) => {
    const res = await request({ method: 'GET', responseType: 'blob', ...option })
    return res as unknown as Promise<T>
  },
  upload: async <T = any>(option: any) => {
    option.headersType = 'multipart/form-data'
    const res = await request({ method: 'POST', ...option })
    return res as unknown as Promise<T>
  },
}

3.定义请求

import http from '@/service/http'

/** 用户登录 */
export const login = (data: LoginReqVO) => {
  return http.post({ url: '/auth/login', data })
}

4.写在最后

在本项目开始,使用了uni.request来发送http请求,通过uni-app的拦截器配置请求拦截器,后面学习研究的时候发现了luch-request,通过文档然后参考yudao-mall-uniapp项目,封装http请求,通过测试,发现能满足实际需用需求。

当然,本篇文章写的比较简陋,水平有限,欢迎共同探讨指教。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1908400.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

搞不清啊?伦敦金与上海金区别是?

进入黄金市场的朋友&#xff0c;有可能会被各式各样的黄金交易品种带得眼花缭乱&#xff0c;其实各品种虽然都以黄金作为投资标的物&#xff0c;但是也是各有不同的&#xff0c;下面我们就来比较一下相似的投资品种——伦敦金和上海金。 首先在比较之前&#xff0c;我们要搞清楚…

计算机毕业设计Django+Vue.js考研推荐系统 考研分数线预测 中公考研爬虫 混合神经网络推荐算法 考研可视化 机器学习 深度学习 大数据毕业设计

Python数据分析与可视化期末项目报告 项目名称&#xff1a; 考研推荐系统数据分析与可视化 学 号&#xff1a; 姓 名&#xff1a; …

Spire.PDF for .NET【文档操作】演示:以特在 C# 中创建 PDF/A-1a 文件

PDF/A-1 标准为 PDF 文件指定了两个符合性级别&#xff1a;PDF/A-1a&#xff08;符合 A 级&#xff09;和 PDF/A-1b&#xff08;符合 B 级&#xff09;。使用 Spire.PDF&#xff0c;您可以轻松创建 PDF/A-1a 和 PDF/A-1b 文件。本文演示了如何使用 Spire.PDF 创建 PDF/A-1a 文件…

MySQL第三次练习

作业三 一 先创建DB abc&#xff0c;创建table student 1、插入一条记录 2、添加多条记录 3、添加部分记录 4、加0.5 5、删除成绩为空的记录 二 1、创建一个用户test1使他只能本地登录拥有查询student表的权限。 2、查询用户test1的权限。 3、删除用户test1. 全在一张图上…

刀客网源码账号合租平台

最新租号平台系统源码&#xff0c;支持单独租用或合租使用 这是一款租号平台源码&#xff0c;采用常见的租号模式。 平台的主要功能如下&#xff1a; 支持单独租用或采用合租模式&#xff0c;采用易支付通用接口进行支付&#xff0c;添加邀请返利功能&#xff0c;以便站长更好…

ubuntu22.04搭建mysql5.7

1.1 下载mysql安装包 MySQL下载地址&#xff1a;MySQL :: Download MySQL Community Server (Archived Versions) #下载wget https://cdn.mysql.com/archives/mysql-5.7/mysql-server_5.7.29-1ubuntu18.04_amd64.deb-bundle.tar#解压tar -xvf ./mysql-server_5.7.29-1ubuntu18…

排序学习笔记

1.什么是排序 1.1排序的概念 概念&#xff1a;排序的概念其实非常简单&#xff0c;本质上就是将一堆记录按照从大到小(降序)&#xff0c;从小到大(升序)来进行排序。我们日常生活中每天都有着不同的排序&#xff0c;比如年龄大小排序&#xff0c;身高的排序等等。 稳定性&am…

如何把已经上传到gitlab的代码或者文件夹从git上删掉

有小伙伴不小心把缓存文件上传到了git&#xff0c;跑来问我&#xff0c;要怎么把这些文件给删掉&#xff0c;这里一共有两种方式&#xff0c; 先说第一种&#xff0c;通过命令删除&#xff0c;终端进入存在这个缓存文件的目录&#xff0c;执行命令ls&#xff0c;可以看到确实有…

STM32学习历程(day5)

EXTI外部中断 中断 中断就是在主程序运行过程中 出现了特定的中断触发条件&#xff08;中断源&#xff09;&#xff0c;CPU会暂停当前的程序&#xff0c;去处理中断程序 处理完会返回被暂停的位置 继续运行原来的程序。 中断优先级 当有多个中断源同时申请中断时 CPU会根据…

如何安全隐藏IP地址,防止网络攻击?

当您想在互联网上保持隐私或匿名时&#xff0c;您应该做的第一件事就是隐藏您的 IP 地址。您的 IP 地址很容易被追踪到您&#xff0c;并被用来了解您的位置。下面的文章将教您如何隐藏自己&#xff0c;不让任何试图跟踪您的活动的人发现。 什么是 IP 地址&#xff1f; 首先&am…

初中生物知识点总结(人教版)

第一章 认识生物 一、 生物的特征&#xff1a; 1&#xff0e; 生物的生活需要营养 2&#xff0e; 生物能进行呼吸 3&#xff0e; 生物能排出身体内产生的废物 4&#xff0e; 生物能对外界的刺激做出反应 5&#xff0e; 生物能生长和繁殖 除病毒以外&#xff0c;生物都是由细胞构…

python中getattr/setattr/hasattr/delattr函数都是干什么的?

目录 1、getattr&#xff1a;动态获取属性 &#x1f50d; 1.1 动态获取属性 1.2 默认值处理技巧 1.3 实战案例&#xff1a;配置文件动态加载 2、setattr&#xff1a;动态设置属性 &#x1f6e0; 2.1 修改对象属性 2.2 新增属性场景 2.3 应用场景&#xff1a;类的动态配置…

零基础也能成为产品册设计高手

​在当今数字化时代&#xff0c;产品册设计已成为企业营销的重要手段之一。过去&#xff0c;人们认为只有专业人士才能设计出精美的产品册&#xff0c;然而&#xff0c;随着设计工具的普及和在线学习资源的丰富&#xff0c;零基础的你也能成为产品册设计高手。本文将带你走进这…

一文清晰了解HTML

有这样一个txt记事本文件和一张图片&#xff1a; txt文本内容是这样的&#xff1a; <html><head><title>HTML学习</title></head><body><h1>hello HTML</h1><img src"高清修复.png"/></body> </html…

泫雅甜蜜官宣爱情开花

【标题】泫雅甜蜜官宣&#xff1a;爱情花开&#xff0c;携手龙俊亨共赴婚姻殿堂&#xff0c;邀您共鉴幸福时刻&#xff01;在这个金秋送爽、爱意弥漫的季节里&#xff0c;韩国娱乐圈迎来了一则振奋的消息——我们的“小野马”泫雅&#xff0c;正式宣布了她人生中的重大决定&…

MyBatis是如何分页的及原理

MyBatis 是一种持久层框架&#xff0c;支持通过配置文件和注解将 SQL 映射为 Java 对象。在实际开发中&#xff0c;查询数据时经常需要进行分页处理。 MyBatis 也提供了支持分页的方案&#xff0c;其主要思路是使用 Limit 偏移量和限制个数&#xff0c;来获取指定数量的数据。下…

全网视频下载之IDM下载安装,软破解

全网视频下载之IDM下载安装&#xff0c;软破解 介绍![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c94f612f7a8845c8a649f74f6b18fd70.png)下载安装配置浏览器Google浏览器Ddge浏览器 界面如何下载不破解如何重复使用总结 介绍 今天给大家分享一个更加简便的全网视…

【Python】已解决:FileNotFoundError: [Errno 2] No such file or directory: ‘D:\1. PDF’

文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决&#xff1a;FileNotFoundError: [Errno 2] No such file or directory: ‘D:\1. PDF’ 一、分析问题背景 在Python编程中&#xff0c;当你尝试打开一个不存在的文件时&…

放大镜案例

放大镜 <!DOCTYPE html> <html lang"zh-cn"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>商品放大镜</title><link rel&qu…

【C语言】五子棋(c语言实现)

这里写目录标题 最终效果菜单打印函数棋盘的初始化和打印人人对战落子判空函数悔棋函数判胜负函数人人对战 人机对战一是将直接调用rand生成随机值&#xff0c;这就不可控二是根据棋子赢面来判断哪里落子最好 如果选择退出程序直接exit就行主函数调用逻辑源代码 最终效果 五子棋…