uni 封装request请求 + refreshToken无缝换token

本文介绍如何在uniApp中封装request请求,并结合refreshToken实现无感知的token刷新,确保用户会话的连续性。涉及到的文件包括utils目录下的interceptors.js, request/simple.js, request/index.js, checkAuth.js, deepMerge.js和deepClone.js,以及/api/modules/content.js, /api/index.js和main.js的使用方法。" 113399895,10538282,Linux环境下实时监控MySQL操作,"['数据库监控', 'MySQL', 'Kafka', 'Maxwell', 'Linux命令']

uni 封装request请求

  1. utils/interceptors.js
import { simpleHttp } from './request/simple'
import { baseUrl } from '../config'
import store from '@/store'
import { authNav } from './checkAuth'
// 1.请求拦截器
// 2.在响应的时候,处理data数据
// 3.统一的错误处理
// 这里的vm,就是我们在vue文件里面的this,所以我们能在这里获取vuex的变量,比如存放在里面的token变量
const install = (Vue, vm) => {
  // 此为自定义配置参数,具体参数见上方说明
  Vue.prototype.$u.http.setConfig({
    baseUrl,
    dataType: 'json',
    showLoading: true, // 是否显示请求中的loading
    loadingText: '请求中...', // 请求loading中的文字提示
    loadingTime: 800, // 在此时间内,请求还没回来的话,就显示加载中动画,单位ms
    originalData: false, // 是否在拦截器中返回服务端的原始数据
    loadingMask: true, // 展示loading的时候,是否给一个透明的蒙层,防止触摸穿透
    // 配置请求头信息
    header: {
      'content-type': 'application/json;charset=UTF-8'
    },
    // options - 即为上一次请求的所有的config配置
    // instance - 即为原先的request实例 -> token已经被更新回来了,所以可以继续使用
    errorHandle: async (err, { options, instance }) => {
      if (err.statusCode === 401) {
        // todo 4.业务 —> refreshToken -> 请求响应401 -> 刷新token
        try {
          const { code, token } = await simpleHttp({
            method: 'POST',
            url: baseUrl + '/login/refresh'
          }, {
            header: {
              Authorization: 'Bearer ' + uni.getStorageSync('refreshToken')
            }
          })
          // console.log('🚀 ~ file: interceptors.js ~ line 30 ~ errorHandle: ~ token', token)
          if (code === 200) {
            // refreshToken请求成功
            // 1.设置全局的token
            store.commit('setToken', token)
            // 2.重新发起请求
            const newResult = await instance.request(options)
            return newResult
          }
        } catch (error) {
          // 代表refreshToken已经失效
          // 清除本地的token
          store.dispatch('logout')
          // 导航到用户的登录页面
          authNav()
        }
        uni.showToast({
          icon: 'none',
          title: '鉴权失败,请重新登录',
          duration: 2000
        })
      } else {
        // 其他的错误
        // showToast提示用户
        // 3.对错误进行统一的处理 -> showToast
        const { data: { msg } } = err
        uni.showToast({
          icon: 'none',
          title: msg || '请求异常,请重试',
          duration: 2000
        })
      }
    }
  })

  // 请求拦截,配置Token等参数
  Vue.prototype.$u.http.interceptor.request = (config) => {
  // 引用token
  // 1.在头部请求的时候,token带上 -> 请求拦截器
    const publicArr = [/\/public/, /\/login/]
    // local store -> uni.getStorageSync('token')
    let isPublic = false
    publicArr.forEach(path => {
      isPublic = isPublic || path.test(config.url)
    })
    const token = uni.getStorageSync('token')
    if (!isPublic && token) {
      config.header.Authorization = 'Bearer ' + token
    }
    // 最后需要将config进行return
    return config
  // 如果return一个false值,则会取消本次请求
  // if(config.url === '/user/rest') return false; // 取消某次请求
  }

  // 响应拦截,判断状态码是否通过
  Vue.prototype.$u.http.interceptor.response = (res) => {
    // console.log('🚀 ~ file: request.js ~ line 46 ~ install ~ res', res)
    return res
  }
}

export default {
  install
}
  1. utils/request/simple.js
export const simpleHttp = (options, { header = {}, callback }) => {
  const result = new Promise((resolve, reject) => {
    uni.request(Object.assign({
      timeout: 10 * 1000
    }, options, {
      header,
      success: (res) => {
        // 请求成功
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(res.data)
        } else {
          reject(res)
        }
      },
      fail: (err) => {
        reject(err)
      },
      complete: () => {
        callback && callback()
      }
    }))
  })
  return result
}

  1. utils/request/index.js
import deepMerge from '../function/deepMerge'
class Request {
  // 设置全局默认配置
  setConfig (customConfig) {
    // 深度合并对象,否则会造成对象深层属性丢失
    this.config = deepMerge(this.config, customConfig)
  }

  // 主要请求部分
  request (options = {}) {
    // 检查请求拦截
    if (this.interceptor.request && typeof this.interceptor.request === 'function') {
      const interceptorRequest = this.interceptor.request(options)
      if (interceptorRequest === false) {
        // 返回一个处于pending状态中的Promise,来取消原promise,避免进入then()回调
        return new Promise(() => { })
      }
      this.options = interceptorRequest
    }
    options.dataType = options.dataType || this.config.dataType
    options.responseType = options.responseType || this.config.responseType
    options.url = options.url || ''
    options.params = options.params || {}
    options.header = Object.assign({}, this.config.header, options.header)
    options.method = options.method || this.config.method
    const errCallback = { options, instance: this }
    return new Promise((resolve, reject) => {
      options.complete = (response) => {
        const { errorHandle } = this.config
        // 请求返回后,隐藏loading(如果请求返回快的话,可能会没有loading)
        uni.hideLoading()
        // 清除定时器,如果请求回来了,就无需loading
        clearTimeout(this.config.timer)
        this.config.timer = null
        // 判断用户对拦截返回数据的要求,如果originalData为true,返回所有的数据(response)到拦截器,否则只返回response.data
        if (this.config.originalData) {
          // 判断是否存在拦截器
          if (this.interceptor.response && typeof this.interceptor.response === 'function') {
            const resInterceptors = this.interceptor.response(response)
            // 如果拦截器不返回false,就将拦截器返回的内容给this.$u.post的then回调
            if (resInterceptors !== false) {
              resolve(resInterceptors)
            } else {
              // 如果拦截器返回false,意味着拦截器定义者认为返回有问题,直接接入catch回调
              errorHandle(response, errCallback)
              reject(response)
            }
          } else {
            // 如果要求返回原始数据,就算没有拦截器,也返回最原始的数据
            resolve(response)
          }
        } else {
          if (response.statusCode === 200) {
            if (this.interceptor.response && typeof this.interceptor.response === 'function') {
              const resInterceptors = this.interceptor.response(response.data)
              if (resInterceptors !== false) {
                resolve(resInterceptors)
              } else {
                errorHandle(response, errCallback)
                reject(response.data)
              }
            } else {
              // 如果不是返回原始数据(originalData=false),且没有拦截器的情况下,返回纯数据给then回调
              resolve(response.data)
            }
          } else {
            // 不返回原始数据的情况下,服务器状态码不为200,modal弹框提示
            // if(response.errMsg) {
            //  uni.showModal({
            //   title: response.errMsg
            //  });
            // }
            errorHandle(response, errCallback)
            reject(response)
          }
        }
      }

      // 判断用户传递的URL是否/开头,如果不是,加上/,这里使用了uView的test.js验证库的url()方法
      // 情景一:如果是使用的官方的uview组件库 -> 创建新的reg -> url
      // 情况二:如果是自己定义的request工具js -> 替换正则
      options.url = /^https?:\/\/.*/.test(options.url)
        ? options.url
        : (this.config.baseUrl + (options.url.indexOf('/') === 0
            ? options.url
            : '/' + options.url))
      // console.log('🚀 ~ file: index.js ~ line 82 ~ Request ~ returnnewPromise ~ options.url', options.url, /^https?:\/\/.*/.test(options.url))

      // 是否显示loading
      // 加一个是否已有timer定时器的判断,否则有两个同时请求的时候,后者会清除前者的定时器id
      // 而没有清除前者的定时器,导致前者超时,一直显示loading
      if (this.config.showLoading && !this.config.timer) {
        this.config.timer = setTimeout(() => {
          uni.showLoading({
            title: this.config.loadingText,
            mask: this.config.loadingMask
          })
          this.config.timer = null
        }, this.config.loadingTime)
      }
      uni.request(options)
    })
    // .catch(res => {
    //  // 如果返回reject(),不让其进入this.$u.post().then().catch()后面的catct()
    //  // 因为很多人都会忘了写后面的catch(),导致报错捕获不到catch
    //  return new Promise(()=>{});
    // })
  }

  constructor () {
    this.config = {
      baseUrl: '', // 请求的根域名
      // 默认的请求头
      header: {},
      method: 'POST',
      // 设置为json,返回后uni.request会对数据进行一次JSON.parse
      dataType: 'json',
      // 此参数无需处理,因为5+和支付宝小程序不支持,默认为text即可
      responseType: 'text',
      showLoading: true, // 是否显示请求中的loading
      loadingText: '请求中...',
      loadingTime: 800, // 在此时间内,请求还没回来的话,就显示加载中动画,单位ms
      timer: null, // 定时器
      originalData: false, // 是否在拦截器中返回服务端的原始数据,见文档说明
      loadingMask: true // 展示loading的时候,是否给一个透明的蒙层,防止触摸穿透
    }

    // 拦截器
    this.interceptor = {
      // 请求前的拦截
      request: null,
      // 请求后的拦截
      response: null
    }

    // get请求
    this.get = (url, data = {}, header = {}) => {
      return this.request({
        method: 'GET',
        url,
        header,
        data
      })
    }

    // post请求
    this.post = (url, data = {}, header = {}) => {
      return this.request({
        url,
        method: 'POST',
        header,
        data
      })
    }

    // put请求,不支持支付宝小程序(HX2.6.15)
    this.put = (url, data = {}, header = {}) => {
      return this.request({
        url,
        method: 'PUT',
        header,
        data
      })
    }

    // delete请求,不支持支付宝和头条小程序(HX2.6.15)
    this.delete = (url, data = {}, header = {}) => {
      return this.request({
        url,
        method: 'DELETE',
        header,
        data
      })
    }
  }
}
export default new Request()
  1. utils/checkAuth.js
import store from '@/store'

export const authNav = (title = '登录已失效', duration = 2000) => {
  global.ctrl && clearTimeout(global.ctrl)
  // 1.给用户一个轻提示
  uni.showToast({
    icon: 'none',
    title,
    duration
  })
  // 2.2s之后跳转到登录页面
  global.ctrl = setTimeout(() => {
    uni.navigateTo({
      url: '/subcom-pkg/auth/auth'
    })
  }, 2000)
}

export const checkSession = async () => {
  try {
    await uni.checkSession()
    return true
  } catch (error) {
    return false
  }
}

export const checkToken = async () => {
  let flag = true
  const token = uni.getStorageSync('token')
  const checked = await checkSession()
  if (!store.state.token || !token || !checked) {
    flag = false
    uni.showModal({
      title: '您未登录',
      content: '需要登录才能操作,确定登录吗?',
      success: function (res) {
        if (res.confirm) {
          uni.navigateTo({
            url: '/subcom-pkg/auth/auth'
          })
        }
      }
    })
  }
  return flag
}

export const checkAuth = async () => {
  let flag = true
  const token = uni.getStorageSync('token')
  const checked = await checkSession()
  if (!store.state.token || !token || !checked) {
    flag = false
  }
  return flag
}

export const gotoGuard = async (path) => {
  const flag = await checkAuth()
  if (!flag) {
    await authNav()
  } else {
    uni.navigateTo({
      url: path
    })
  }
}
  1. utils/funciton/deepMerge.js
import deepClone from "./deepClone";

// JS对象深度合并
function deepMerge(target = {}, source = {}) {
	target = deepClone(target);
	if (typeof target !== 'object' || typeof source !== 'object') return false;
	for (var prop in source) {
		if (!source.hasOwnProperty(prop)) continue;
		if (prop in target) {
			if (typeof target[prop] !== 'object') {
				target[prop] = source[prop];
			} else {
				if (typeof source[prop] !== 'object') {
					target[prop] = source[prop];
				} else {
					if (target[prop].concat && source[prop].concat) {
						target[prop] = target[prop].concat(source[prop]);
					} else {
						target[prop] = deepMerge(target[prop], source[prop]);
					}
				}
			}
		} else {
			target[prop] = source[prop];
		}
	}
	return target;
}

export default deepMerge;
  1. utils/function/deepClone.js
// 判断arr是否为一个数组,返回一个bool值
function isArray (arr) {
    return Object.prototype.toString.call(arr) === '[object Array]';
}

// 深度克隆
function deepClone (obj) {
	// 对常见的“非”值,直接返回原来值
	if([null, undefined, NaN, false].includes(obj)) return obj;
    if(typeof obj !== "object" && typeof obj !== 'function') {
		//原始类型直接返回
        return obj;
    }
    var o = isArray(obj) ? [] : {};
    for(let i in obj) {
        if(obj.hasOwnProperty(i)){
            o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];
        }
    }
    return o;
}

export default deepClone;

使用方式

  • /api/modules/content.js
import store from '@/store'
import Vue from 'vue'

const axios = Vue.prototype.$u
const getList = (options) => {
  return axios.get('/public/list', options)
}
export {
  getList
}
  • /api/index.js
const req = require.context('./modules', false, /\.js$/)

const install = (Vue) => {
  let api = Vue.prototype.$u.api || {}
  req.keys().forEach(item => {
    const module = req(item)
    // 1.取得所有的方法 -> key值,
    const keys = Object.keys(module)
    keys.forEach(key => {
      api = {
        ...api,
        // 2.取得所有方法对应的function -> value值
        // 3.把上面的对象 -> $u.api
        [key]: module[key]
      }
    })
    // 4.把它封装成为一个插件,以便在后面的方法中使用 this.$u.api.方法名(参数).then(res) ....
  })
  Vue.prototype.$u.api = api
}

export default {
  install
}
  • main.js
import Vue from 'vue'
import interceptors from '@/common/interceptors'
import apis from '@/api'
Vue.use(interceptors)
Vue.use(apis)
Vue3 结合 TypeScript 的 Uniapp 项目中封装 `uni.request` 网络请求,可以遵循以下步骤实现一个通用且可扩展的封装方案。此方案将包括请求拦截、响应拦截、错误处理、统一接口返回格式等功能。 ### 1. 创建请求工具类 首先,在项目中创建一个 `request.ts` 文件,用于封装 `uni.request` 的基础调用,并添加拦截器和错误处理逻辑。 ```typescript // src/utils/request.ts import { AxiosRequestConfig, AxiosResponse } from 'axios'; // 请求拦截器 const beforeRequest = (config: UniApp.RequestOptions) => { // 添加 token 到 headers const token = uni.getStorageSync('token'); if (token) { config.header = { ...config.header, Authorization: `Bearer ${token}`, }; } return config; }; // 响应拦截器 const afterResponse = <T>(response: UniApp.RequestSuccessCallbackResult<T>) => { // 可以在此统一处理响应数据 return response.data; }; // 错误处理 const handleError = (error: any) => { uni.showToast({ title: '网络请求失败', icon: 'none', }); return Promise.reject(error); }; // 封装 uni.request const request = <T>(options: UniApp.RequestOptions): Promise<T> => { // 默认配置 const defaultOptions: UniApp.RequestOptions = { url: options.url, method: options.method || 'GET', data: options.data || {}, header: options.header || {}, success: (res) => { // 调用响应拦截器 return afterResponse<T>(res); }, fail: (err) => { return handleError(err); }, }; // 调用请求拦截器 const finalOptions = beforeRequest(defaultOptions); return new Promise((resolve, reject) => { uni.request({ ...finalOptions, success: (res) => { resolve(afterResponse<T>(res)); }, fail: (err) => { reject(handleError(err)); }, }); }); }; export default request; ``` ### 2. 创建 API 接口文件 接下来,创建一个 `api.ts` 文件,用于定义具体的业务接口。 ```typescript // src/api/api.ts import request from '@/utils/request'; // 示例接口:获取用户信息 export const getUserInfo = () => { return request({ url: '/api/user/info', method: 'GET', }); }; // 示例接口:登录 export const login = (data: { username: string; password: string }) => { return request({ url: '/api/auth/login', method: 'POST', data, }); }; ``` ### 3. 在页面中使用封装请求Vue3 页面中使用封装好的接口。 ```vue <script setup lang="ts"> import { onMounted } from 'vue'; import { getUserInfo } from '@/api/api'; onMounted(async () => { try { const res = await getUserInfo(); console.log('用户信息:', res); } catch (error) { console.error('请求失败:', error); } }); </script> ``` ### 4. 可选:封装 Token 无感刷新逻辑 如果项目中涉及 Token 过期自动刷新机制,可以在 `request.ts` 中加入 Token 刷新逻辑。具体实现可以参考以下方式: ```typescript // src/utils/request.ts let isRefreshing = false; let refreshSubscribers: ((token: string) => void)[] = []; const onTokenRefreshed = (token: string) => { refreshSubscribers.forEach((callback) => callback(token)); refreshSubscribers = []; }; const addRefreshSubscriber = (callback: (token: string) => void) => { refreshSubscribers.push(callback); }; const refreshToken = async () => { if (isRefreshing) { return new Promise<string>((resolve) => { addRefreshSubscriber((token) => { resolve(token); }); }); } isRefreshing = true; try { const res = await request({ url: '/api/auth/refresh-token', method: 'POST', }); const newToken = res.data.token; uni.setStorageSync('token', newToken); isRefreshing = false; onTokenRefreshed(newToken); return newToken; } catch (error) { isRefreshing = false; uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', }); uni.navigateTo({ url: '/pages/login/login', }); return Promise.reject(error); } }; // 修改 beforeRequest 函数 const beforeRequest = (config: UniApp.RequestOptions) => { const token = uni.getStorageSync('token'); if (token) { config.header = { ...config.header, Authorization: `Bearer ${token}`, }; } return config; }; ``` ### 5. 使用 Token 无感刷新的完整封装 在实际请求中,当检测到 Token 过期时,可以自动触发 Token 刷新逻辑并重试请求。 ```typescript // 修改 request.ts 中的 error 处理逻辑 const handleError = (error: any) => { const originalRequest = error.config; if (error.statusCode === 401 && !originalRequest._retry) { originalRequest._retry = true; return refreshToken().then((token) => { originalRequest.header.Authorization = `Bearer ${token}`; return request(originalRequest); }); } uni.showToast({ title: '网络请求失败', icon: 'none', }); return Promise.reject(error); }; ``` 通过以上封装,可以在 Vue3 + TypeScript 的 Uniapp 项目中实现一个结构清晰、功能完整的 `uni.request` 请求封装,支持 Token 自动刷新、统一错误处理等高级功能。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值