feat:新增请求逻辑封装
This commit is contained in:
parent
6841fa17d3
commit
a9580ca2c2
7
env/.env
vendored
7
env/.env
vendored
@ -10,7 +10,7 @@ VITE_WX_APPID = 'wx8e64a91083684628'
|
||||
VITE_APP_PUBLIC_BASE=/
|
||||
|
||||
# 后台请求地址
|
||||
VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
|
||||
VITE_SERVER_BASEURL = 'https://www.cx.cdzhuojing.cn/ebike'
|
||||
# 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api
|
||||
|
||||
# 注意,如果是微信小程序,还有一套请求地址的配置,根据 develop、trial、release 分别设置上传地址,见 `src/utils/index.ts`。
|
||||
@ -21,10 +21,13 @@ VITE_APP_PROXY_ENABLE = false
|
||||
VITE_APP_PROXY_PREFIX = '/fg-api'
|
||||
|
||||
# 第二个请求地址 (目前alova中可以使用)
|
||||
VITE_SERVER_BASEURL_SECONDARY = 'https://ukw0y1.laf.run'
|
||||
VITE_SERVER_BASEURL_SECONDARY = 'https://www.cx.cdzhuojing.cn/ebike'
|
||||
|
||||
# 认证模式,'single' | 'double' ==> 单token | 双token
|
||||
VITE_AUTH_MODE = 'single'
|
||||
|
||||
# 原生插件资源复制开关,控制是否启用 copy-native-resources 插件
|
||||
VITE_COPY_NATIVE_RES_ENABLE = false
|
||||
|
||||
# sm2加密key
|
||||
VITE_SM2_KEY = '04f5084ee12767d932f293508e30e3b0100185042ec0f061dedaf92b793b93f79fd6179d5e47e25b7aec98e00cf90dd56df1f8191012537187e7bbfd2d1de299fc'
|
||||
@ -115,10 +115,13 @@
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
|
||||
"abortcontroller-polyfill": "^1.7.8",
|
||||
"alova": "^3.3.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "1.11.10",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "2.0.36",
|
||||
"pinia-plugin-persistedstate": "3.2.1",
|
||||
"sm-crypto": "^0.3.13",
|
||||
"vue": "^3.4.21",
|
||||
"z-paging": "2.8.7"
|
||||
},
|
||||
@ -187,4 +190,4 @@
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
|
||||
import { navigateToInterceptor } from '@/router/interceptor'
|
||||
import CacheManager from './utils/CacheManager'
|
||||
|
||||
onLaunch((options) => {
|
||||
console.log('App Launch', options)
|
||||
})
|
||||
onShow((options) => {
|
||||
// 测试本地缓存token
|
||||
CacheManager.set('token', 'fdSeMVT0SxdGuq8aqAL0CARmFdybUegn')
|
||||
console.log('App Show', options)
|
||||
// 处理直接进入页面路由的情况:如h5直接输入路由、微信小程序分享后进入等
|
||||
// https://github.com/unibest-tech/unibest/issues/192
|
||||
|
||||
@ -4,7 +4,7 @@ export type AuthMode = 'single' | 'double'
|
||||
// 单Token响应类型
|
||||
export interface ISingleTokenRes {
|
||||
token: string
|
||||
expiresIn: number // 有效期(秒)
|
||||
expiresIn?: number // 有效期(秒)
|
||||
}
|
||||
|
||||
// 双Token响应类型
|
||||
|
||||
142
src/http/http.ts
142
src/http/http.ts
@ -1,20 +1,23 @@
|
||||
import type { IDoubleTokenRes } from '@/api/types/login'
|
||||
import type { CustomRequestOptions, IResponse } from '@/http/types'
|
||||
import { nextTick } from 'vue'
|
||||
import { LOGIN_PAGE } from '@/router/config'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { isDoubleTokenMode } from '@/utils'
|
||||
import { ResultEnum } from './tools/enum'
|
||||
import { encryptData, encryptQuery, getHeader } from './tools/httpTool'
|
||||
|
||||
// 刷新 token 状态管理
|
||||
let refreshing = false // 防止重复刷新 token 标识
|
||||
let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
|
||||
// 刷新 token 状态管理 (暂时没有考虑token刷新逻辑)
|
||||
// let refreshing = false // 防止重复刷新 token 标识
|
||||
// let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
|
||||
|
||||
export function http<T>(options: CustomRequestOptions) {
|
||||
// 1. 返回 Promise 对象
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const _header = getHeader(options)
|
||||
const _query = encryptQuery(options)
|
||||
const _data = encryptData(options)
|
||||
|
||||
uni.request({
|
||||
...options,
|
||||
header: _header,
|
||||
query: _query,
|
||||
data: _data,
|
||||
dataType: 'json',
|
||||
// #ifndef MP-WEIXIN
|
||||
responseType: 'json',
|
||||
@ -27,70 +30,71 @@ export function http<T>(options: CustomRequestOptions) {
|
||||
// 检查是否是401错误(包括HTTP状态码401或业务码401)
|
||||
const isTokenExpired = res.statusCode === 401 || code === 401
|
||||
|
||||
if (isTokenExpired) {
|
||||
const tokenStore = useTokenStore()
|
||||
if (!isDoubleTokenMode) {
|
||||
// 未启用双token策略,清理用户信息,跳转到登录页
|
||||
tokenStore.logout()
|
||||
uni.navigateTo({ url: LOGIN_PAGE })
|
||||
return reject(res)
|
||||
}
|
||||
// 业务当前暂时没有考虑token刷新
|
||||
// if (isTokenExpired) {
|
||||
// const tokenStore = useTokenStore()
|
||||
// if (!isDoubleTokenMode) {
|
||||
// // 未启用双token策略,清理用户信息,跳转到登录页
|
||||
// tokenStore.logout()
|
||||
// uni.navigateTo({ url: LOGIN_PAGE })
|
||||
// return reject(res)
|
||||
// }
|
||||
|
||||
/* -------- 无感刷新 token ----------- */
|
||||
const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
|
||||
// token 失效的,且有刷新 token 的,才放到请求队列里
|
||||
if (refreshToken) {
|
||||
taskQueue.push(() => {
|
||||
resolve(http<T>(options))
|
||||
})
|
||||
}
|
||||
// /* -------- 无感刷新 token ----------- */
|
||||
// const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
|
||||
// // token 失效的,且有刷新 token 的,才放到请求队列里
|
||||
// if (refreshToken) {
|
||||
// taskQueue.push(() => {
|
||||
// resolve(http<T>(options))
|
||||
// })
|
||||
// }
|
||||
|
||||
// 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
|
||||
if (refreshToken && !refreshing) {
|
||||
refreshing = true
|
||||
try {
|
||||
// 发起刷新 token 请求(使用 store 的 refreshToken 方法)
|
||||
await tokenStore.refreshToken()
|
||||
// 刷新 token 成功
|
||||
refreshing = false
|
||||
nextTick(() => {
|
||||
// 关闭其他弹窗
|
||||
uni.hideToast()
|
||||
uni.showToast({
|
||||
title: 'token 刷新成功',
|
||||
icon: 'none',
|
||||
})
|
||||
})
|
||||
// 将任务队列的所有任务重新请求
|
||||
taskQueue.forEach(task => task())
|
||||
}
|
||||
catch (refreshErr) {
|
||||
console.error('刷新 token 失败:', refreshErr)
|
||||
refreshing = false
|
||||
// 刷新 token 失败,跳转到登录页
|
||||
nextTick(() => {
|
||||
// 关闭其他弹窗
|
||||
uni.hideToast()
|
||||
uni.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none',
|
||||
})
|
||||
})
|
||||
// 清除用户信息
|
||||
await tokenStore.logout()
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({ url: LOGIN_PAGE })
|
||||
}, 2000)
|
||||
}
|
||||
finally {
|
||||
// 不管刷新 token 成功与否,都清空任务队列
|
||||
taskQueue = []
|
||||
}
|
||||
}
|
||||
// // 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
|
||||
// if (refreshToken && !refreshing) {
|
||||
// refreshing = true
|
||||
// try {
|
||||
// // 发起刷新 token 请求(使用 store 的 refreshToken 方法)
|
||||
// await tokenStore.refreshToken()
|
||||
// // 刷新 token 成功
|
||||
// refreshing = false
|
||||
// nextTick(() => {
|
||||
// // 关闭其他弹窗
|
||||
// uni.hideToast()
|
||||
// uni.showToast({
|
||||
// title: 'token 刷新成功',
|
||||
// icon: 'none',
|
||||
// })
|
||||
// })
|
||||
// // 将任务队列的所有任务重新请求
|
||||
// taskQueue.forEach(task => task())
|
||||
// }
|
||||
// catch (refreshErr) {
|
||||
// console.error('刷新 token 失败:', refreshErr)
|
||||
// refreshing = false
|
||||
// // 刷新 token 失败,跳转到登录页
|
||||
// nextTick(() => {
|
||||
// // 关闭其他弹窗
|
||||
// uni.hideToast()
|
||||
// uni.showToast({
|
||||
// title: '登录已过期,请重新登录',
|
||||
// icon: 'none',
|
||||
// })
|
||||
// })
|
||||
// // 清除用户信息
|
||||
// await tokenStore.logout()
|
||||
// // 跳转到登录页
|
||||
// setTimeout(() => {
|
||||
// uni.navigateTo({ url: LOGIN_PAGE })
|
||||
// }, 2000)
|
||||
// }
|
||||
// finally {
|
||||
// // 不管刷新 token 成功与否,都清空任务队列
|
||||
// taskQueue = []
|
||||
// }
|
||||
// }
|
||||
|
||||
return reject(res)
|
||||
}
|
||||
// return reject(res)
|
||||
// }
|
||||
|
||||
// 处理其他成功状态(HTTP状态码200-299)
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
|
||||
71
src/http/tools/httpTool.ts
Normal file
71
src/http/tools/httpTool.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import type { CustomRequestOptions } from '@/http/types'
|
||||
import CacheManager from '@/utils/CacheManager'
|
||||
import SM2 from '@/utils/sm2'
|
||||
import { ContentTypeEnum } from './enum'
|
||||
|
||||
const sm2Key = import.meta.env.VITE_SM2_KEY
|
||||
|
||||
/**
|
||||
* 获取请求头
|
||||
*/
|
||||
export function getHeader(options: CustomRequestOptions) {
|
||||
// 获取token
|
||||
const token = CacheManager.get('token')
|
||||
const { noToken, contentTypeUrlEncoded, header } = options
|
||||
|
||||
// 自定义header
|
||||
if (header && Object.keys(header).length > 0) {
|
||||
return { ...header }
|
||||
}
|
||||
|
||||
let _header = {}
|
||||
if (!noToken) {
|
||||
_header = {
|
||||
Authorization: token,
|
||||
}
|
||||
}
|
||||
_header = {
|
||||
..._header,
|
||||
'Content-Type': contentTypeUrlEncoded ? ContentTypeEnum.FORM_URLENCODED : ContentTypeEnum.JSON,
|
||||
}
|
||||
return _header
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密query参数
|
||||
*/
|
||||
export function encryptQuery(options: CustomRequestOptions) {
|
||||
const { query } = options
|
||||
// 如果query存在且不是空对象,遍历对象的值进行SM2加密
|
||||
if (query && Object.keys(query).length > 0) {
|
||||
for (const key in query) {
|
||||
if (Object.prototype.hasOwnProperty.call(query, key)) {
|
||||
query[key] = SM2.sm2Encrypt(query[key], sm2Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密data参数
|
||||
*/
|
||||
export function encryptData(options: CustomRequestOptions) {
|
||||
const { data, contentTypeUrlEncoded } = options
|
||||
let _data: any = null
|
||||
|
||||
if (data) {
|
||||
_data = SM2.sm2Encrypt(JSON.stringify(data), sm2Key)
|
||||
}
|
||||
|
||||
if (contentTypeUrlEncoded && data) {
|
||||
const formData = new FormData()
|
||||
Object.keys(data).forEach((key) => {
|
||||
const enKeyData = SM2.sm2Encrypt(data[key], sm2Key)
|
||||
formData.append(key, enKeyData)
|
||||
})
|
||||
_data = formData
|
||||
}
|
||||
|
||||
return _data
|
||||
}
|
||||
@ -5,6 +5,10 @@ export type CustomRequestOptions = UniApp.RequestOptions & {
|
||||
query?: Record<string, any>
|
||||
/** 出错时是否隐藏错误提示 */
|
||||
hideErrorToast?: boolean
|
||||
/** 请求是否不需要token */
|
||||
noToken?: boolean
|
||||
/** 内容型是否为x-www-form-urlencoded */
|
||||
contentTypeUrlEncoded?: boolean
|
||||
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
|
||||
|
||||
export interface HttpRequestResult<T> {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { getBikecodeAPI } from '@/api/test'
|
||||
import { safeAreaInsets } from '@/utils/systemInfo'
|
||||
|
||||
defineOptions({
|
||||
@ -21,6 +22,9 @@ console.log('index/index 首页打印了')
|
||||
|
||||
onLoad(() => {
|
||||
console.log('测试 uni API 自动引入: onLoad')
|
||||
getBikecodeAPI('060002').then((res) => {
|
||||
console.log('getBikecodeAPI res: ', res)
|
||||
})
|
||||
})
|
||||
|
||||
// #region gotoAbout
|
||||
|
||||
108
src/utils/CacheManager.ts
Normal file
108
src/utils/CacheManager.ts
Normal file
@ -0,0 +1,108 @@
|
||||
interface CacheItem<T> {
|
||||
value: T
|
||||
expiry?: number // 过期时间戳(毫秒),可选
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用缓存管理类(支持过期时间,不设 TTL 即为永久缓存)
|
||||
*/
|
||||
class CacheManager {
|
||||
/**
|
||||
* 设置缓存
|
||||
* @param key 缓存键
|
||||
* @param value 缓存值
|
||||
* @param ttl 过期时间(秒)。若为 undefined、null 或 <=0,则视为永久缓存(永不过期)
|
||||
*/
|
||||
static set<T>(key: string, value: T, ttl?: number | null): void {
|
||||
try {
|
||||
const cacheItem: CacheItem<T> = { value }
|
||||
|
||||
// 仅当 ttl 为正数时才设置过期时间
|
||||
if (ttl != null && ttl > 0) {
|
||||
cacheItem.expiry = Date.now() + ttl * 1000
|
||||
}
|
||||
// 否则 cacheItem.expiry 保持 undefined,表示永久缓存
|
||||
|
||||
uni.setStorageSync(key, cacheItem)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('CacheManager.set error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存(自动清除过期项)
|
||||
* @param key 缓存键
|
||||
* @param defaultValue 默认值(缓存不存在、过期或出错时返回)
|
||||
* @returns 缓存值或默认值
|
||||
*/
|
||||
static get<T>(key: string, defaultValue: T = null as unknown as T): T {
|
||||
try {
|
||||
const raw = uni.getStorageSync(key)
|
||||
if (!raw)
|
||||
return defaultValue
|
||||
|
||||
const cacheItem = raw as CacheItem<T>
|
||||
|
||||
// 若有 expiry 且已过期,则清除并返回默认值
|
||||
if (cacheItem.expiry !== undefined && Date.now() > cacheItem.expiry) {
|
||||
uni.removeStorageSync(key)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return cacheItem.value
|
||||
}
|
||||
catch (e) {
|
||||
console.error('CacheManager.get error:', e)
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定缓存
|
||||
*/
|
||||
static remove(key: string): void {
|
||||
try {
|
||||
uni.removeStorageSync(key)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('CacheManager.remove error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
static clear(): void {
|
||||
try {
|
||||
uni.clearStorageSync()
|
||||
}
|
||||
catch (e) {
|
||||
console.error('CacheManager.clear error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在有效(未过期)缓存
|
||||
*/
|
||||
static has(key: string): boolean {
|
||||
try {
|
||||
const raw = uni.getStorageSync(key)
|
||||
if (!raw)
|
||||
return false
|
||||
|
||||
const cacheItem = raw as CacheItem<unknown>
|
||||
if (cacheItem.expiry !== undefined && Date.now() > cacheItem.expiry) {
|
||||
uni.removeStorageSync(key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
catch (e) {
|
||||
console.error('CacheManager.has error:', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CacheManager
|
||||
@ -121,9 +121,9 @@ export function getEnvBaseUrl() {
|
||||
let baseUrl = import.meta.env.VITE_SERVER_BASEURL
|
||||
|
||||
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
|
||||
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = baseUrl
|
||||
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = baseUrl
|
||||
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = baseUrl
|
||||
|
||||
// 微信小程序端环境区分
|
||||
if (isMpWeixin) {
|
||||
@ -157,3 +157,19 @@ export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double'
|
||||
* 通常为 /pages/index/index
|
||||
*/
|
||||
export const HOME_PAGE = `/${(pages as PageMetaDatum[]).find(page => page.type === 'home')?.path || (pages as PageMetaDatum[])[0].path}`
|
||||
|
||||
/**
|
||||
* 获取 url 的参数
|
||||
*/
|
||||
export function getUrlQuery(url: string) {
|
||||
const queryStr = url.split('?')[1]
|
||||
if (!queryStr) {
|
||||
return {}
|
||||
}
|
||||
const query: Record<string, string> = {}
|
||||
queryStr.split('&').forEach((item) => {
|
||||
const [key, value] = item.split('=')
|
||||
query[key] = value
|
||||
})
|
||||
return query
|
||||
}
|
||||
|
||||
10
src/utils/sm2.ts
Normal file
10
src/utils/sm2.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import smcrypto from 'sm-crypto'
|
||||
|
||||
const SM2 = {
|
||||
sm2Encrypt(text: string, publicKey: string) {
|
||||
const sm2 = smcrypto.sm2
|
||||
return sm2.doEncrypt(text, publicKey)
|
||||
},
|
||||
}
|
||||
|
||||
export default SM2
|
||||
@ -186,7 +186,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
: undefined,
|
||||
},
|
||||
esbuild: {
|
||||
drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : ['debugger'],
|
||||
drop: VITE_DELETE_CONSOLE === 'true' ? ['console'] : [],
|
||||
},
|
||||
build: {
|
||||
sourcemap: false,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user