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请求,通过测试,发现能满足实际需用需求。
当然,本篇文章写的比较简陋,水平有限,欢迎共同探讨指教。