调整请求拦截
This commit is contained in:
parent
3e8c56b5c5
commit
bee838d474
3
.env
3
.env
|
|
@ -1,3 +1,4 @@
|
|||
# 公共配置
|
||||
VITE_APP_NAME = GroupBuying
|
||||
VITE_APP_VERSION = 1.0.0
|
||||
VITE_APP_VERSION = 1.0.0
|
||||
VITE_API_BASE = ""
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
NODE_ENV = development
|
||||
VITE_API_BASE = http://dev-api.example.com
|
||||
VITE_API_BASE = http://api-dev.example.com/
|
||||
VITE_AUTH_REFRESH_URL = /auth/refresh
|
||||
VITE_DEBUG_MODE = true
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
NODE_ENV = production
|
||||
VITE_API_BASE = https://api.example.com
|
||||
VITE_API_BASE = https://api.example.com/
|
||||
VITE_AUTH_REFRESH_URL = /auth/refresh
|
||||
VITE_DEBUG_MODE = false
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@
|
|||
export default {
|
||||
onLaunch: function () {
|
||||
console.log('App Launch')
|
||||
if (import.meta.env.VITE_DEBUG_MODE) {
|
||||
console.log('当前环境变量:', {
|
||||
API_BASE: import.meta.env.VITE_API_BASE,
|
||||
APP_ENV: import.meta.env.VITE_APP_ENV
|
||||
})
|
||||
}
|
||||
},
|
||||
onShow: function () {
|
||||
console.log('App Show')
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import {
|
|||
createSSRApp
|
||||
} from "vue";
|
||||
import App from "./App.vue";
|
||||
import { setupInterceptors } from "@/utils/interceptors";
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
app.use(setupInterceptors)
|
||||
return {
|
||||
app,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
let isRefreshing = false // 刷新状态锁
|
||||
let requestsQueue = [] // 等待队列
|
||||
|
||||
// 核心拦截器
|
||||
export const authInterceptor = async (config) => {
|
||||
// 1. 获取当前 Token
|
||||
let token = uni.getStorageSync('token')
|
||||
const refreshToken = uni.getStorageSync('refreshToken')
|
||||
|
||||
// 2. 检查是否需要跳过认证
|
||||
if (config.skipAuth) return config
|
||||
|
||||
// 3. 自动添加 Authorization 头
|
||||
if (token) {
|
||||
config.header = {
|
||||
...config.header,
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 小程序 HTTPS 强制转换
|
||||
// #ifdef MP-WEIXIN
|
||||
if (config.url.startsWith('http:')) {
|
||||
config.url = config.url.replace('http:', 'https:')
|
||||
}
|
||||
// #endif
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// 刷新 Token 逻辑
|
||||
const refreshTokenRequest = async () => {
|
||||
try {
|
||||
const { data } = await uni.request({
|
||||
url: '/auth/refresh',
|
||||
method: 'POST',
|
||||
data: {
|
||||
refresh_token: uni.getStorageSync('refreshToken')
|
||||
}
|
||||
})
|
||||
|
||||
// 更新存储
|
||||
uni.setStorageSync('token', data.token)
|
||||
uni.setStorageSync('refreshToken', data.refreshToken)
|
||||
return data.token
|
||||
} catch (error) {
|
||||
// 刷新失败跳转登录
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshToken')
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
throw new Error('登录已过期,请重新登录')
|
||||
}
|
||||
}
|
||||
|
||||
// 响应拦截器处理 Token 过期
|
||||
export const authErrorHandler = async (error) => {
|
||||
const { statusCode, config } = error
|
||||
|
||||
// 1. 非 401 错误直接抛出
|
||||
if (statusCode !== 401 || config.skipAuth) throw error
|
||||
|
||||
// 2. 正在刷新时缓存请求
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
requestsQueue.push(() => resolve(http.request(config)))
|
||||
})
|
||||
}
|
||||
|
||||
// 3. 标记刷新状态
|
||||
isRefreshing = true
|
||||
|
||||
try {
|
||||
// 4. 执行 Token 刷新
|
||||
const newToken = await refreshTokenRequest()
|
||||
|
||||
// 5. 更新原请求头
|
||||
config.header.Authorization = `Bearer ${newToken}`
|
||||
|
||||
// 6. 重试所有等待请求
|
||||
requestsQueue.forEach(cb => cb())
|
||||
requestsQueue = []
|
||||
|
||||
// 7. 重试当前请求
|
||||
return http.request(config)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import { authInterceptor, authErrorHandler } from './auth'
|
||||
|
||||
class Request {
|
||||
constructor() {
|
||||
// 全局默认配置
|
||||
this.config = {
|
||||
baseURL: import.meta.env.VITE_API_BASE,
|
||||
timeout: 10000,
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
// 开发环境打印日志
|
||||
if (import.meta.env.VITE_DEBUG_MODE === 'true') {
|
||||
this._enableDebugLog()
|
||||
}
|
||||
|
||||
// 拦截器存储
|
||||
this.interceptors = {
|
||||
request: [authInterceptor],
|
||||
response: [authErrorHandler]
|
||||
}
|
||||
|
||||
// 请求队列(用于取消请求)
|
||||
this.pendingRequests = new Map()
|
||||
}
|
||||
|
||||
// 调试日志
|
||||
_enableDebugLog() {
|
||||
const originalRequest = this.request
|
||||
this.request = async function(config) {
|
||||
console.log('[HTTP Request]', config)
|
||||
const start = Date.now()
|
||||
try {
|
||||
const res = await originalRequest.call(this, config)
|
||||
console.log(`[HTTP Response] ${Date.now() - start}ms`, res)
|
||||
return res
|
||||
} catch (err) {
|
||||
console.error(`[HTTP Error] ${Date.now() - start}ms`, err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 小程序 HTTPS 强制转换
|
||||
_fixUrlProtocol(url) {
|
||||
// #ifdef MP-WEIXIN
|
||||
return url.replace(/^http:/, 'https:')
|
||||
// #else
|
||||
return url
|
||||
// #endif
|
||||
}
|
||||
// 核心请求方法
|
||||
async _request(config) {
|
||||
// 合并配置
|
||||
const mergedConfig = {
|
||||
...this.config,
|
||||
...config,
|
||||
url: this._fixUrlProtocol(config.url)
|
||||
}
|
||||
|
||||
// 生成请求标识
|
||||
const requestKey = `${mergedConfig.method}-${mergedConfig.url}`
|
||||
const controller = new AbortController()
|
||||
this.pendingRequests.set(requestKey, controller)
|
||||
|
||||
// 构建完整URL
|
||||
const fullUrl = `${mergedConfig.baseURL}${mergedConfig.url}`
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
...mergedConfig,
|
||||
url: fullUrl,
|
||||
signal: controller.signal,
|
||||
success: (res) => resolve(res),
|
||||
fail: (err) => reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 执行拦截器链
|
||||
async _runInterceptors(type, value) {
|
||||
let chain = type === 'request'
|
||||
? [...this.interceptors.request, this._request, undefined]
|
||||
: [undefined, ...this.interceptors.response]
|
||||
|
||||
let promise = Promise.resolve(value)
|
||||
for (let i = 0; i < chain.length; i += 2) {
|
||||
const thenFn = chain[i]
|
||||
const catchFn = chain[i + 1]
|
||||
promise = promise.then(thenFn, catchFn)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
// 公开请求方法
|
||||
async request(config) {
|
||||
try {
|
||||
// 执行请求拦截
|
||||
const processedConfig = await this._runInterceptors('request', config)
|
||||
// 发起请求
|
||||
const response = await this._request(processedConfig)
|
||||
// 执行响应拦截
|
||||
return this._runInterceptors('response', response)
|
||||
} catch (error) {
|
||||
// 统一错误处理
|
||||
this._handleError(error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷方法
|
||||
get(url, config = {}) {
|
||||
return this.request({ ...config, url, method: 'GET' })
|
||||
}
|
||||
|
||||
post(url, data, config = {}) {
|
||||
return this.request({ ...config, url, data, method: 'POST' })
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
_handleError(error) {
|
||||
let message = '请求失败'
|
||||
if (error.errMsg?.includes('timeout')) message = '网络超时'
|
||||
if (error.statusCode === 404) message = '接口不存在'
|
||||
uni.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
|
||||
// 取消请求
|
||||
cancelRequest(url, method = 'GET') {
|
||||
const key = `${method}-${url}`
|
||||
const controller = this.pendingRequests.get(key)
|
||||
controller?.abort()
|
||||
this.pendingRequests.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
export const http = new Request()
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
// interceptors/auth.js
|
||||
let isRefreshing = false
|
||||
let pendingRequests = []
|
||||
const API_BASE = import.meta.env.VITE_API_BASE
|
||||
const REFRESH_TOKEN_URL = `${API_BASE}/auth/refresh`
|
||||
|
||||
// 获取存储的token
|
||||
const getToken = () => ({
|
||||
token: uni.getStorageSync('token'),
|
||||
refreshToken: uni.getStorageSync('refreshToken')
|
||||
})
|
||||
|
||||
// 清理认证信息
|
||||
const clearAuth = () => {
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshToken')
|
||||
}
|
||||
|
||||
// 刷新Token逻辑
|
||||
const refreshToken = async () => {
|
||||
const { refreshToken } = getToken()
|
||||
if (!refreshToken) throw new Error('No refresh token available')
|
||||
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: REFRESH_TOKEN_URL,
|
||||
method: 'POST',
|
||||
data: { refresh_token: refreshToken },
|
||||
header: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
uni.setStorageSync('token', res.data.token)
|
||||
uni.setStorageSync('refreshToken', res.data.refreshToken)
|
||||
return res.data.token
|
||||
}
|
||||
throw new Error('Refresh token failed')
|
||||
} catch (error) {
|
||||
clearAuth()
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 处理401错误
|
||||
export const handle401Error = async (error) => {
|
||||
const originalRequest = error.config
|
||||
|
||||
if (isRefreshing) {
|
||||
return new Promise(resolve => {
|
||||
pendingRequests.push(() => resolve(retryRequest(originalRequest)))
|
||||
})
|
||||
}
|
||||
|
||||
isRefreshing = true
|
||||
try {
|
||||
const newToken = await refreshToken()
|
||||
originalRequest.header.Authorization = `Bearer ${newToken}`
|
||||
|
||||
pendingRequests.forEach(cb => cb())
|
||||
pendingRequests = []
|
||||
|
||||
return retryRequest(originalRequest)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重试请求
|
||||
const retryRequest = (config) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
...config,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
// interceptors/index.js
|
||||
import { handle401Error } from './auth'
|
||||
|
||||
// 基础配置
|
||||
const getBaseConfig = () => ({
|
||||
baseURL: import.meta.env.VITE_API_BASE,
|
||||
timeout: Number(import.meta.env.VITE_REQUEST_TIMEOUT) || 10000,
|
||||
defaultHeaders: {
|
||||
'X-App-Env': import.meta.env.VITE_APP_ENV,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截处理
|
||||
const requestInterceptor = (args) => {
|
||||
const config = getBaseConfig()
|
||||
|
||||
// 协议处理
|
||||
// #ifdef MP-WEIXIN
|
||||
if (args.url.startsWith('http:')) {
|
||||
args.url = args.url.replace('http:', 'https:')
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 合并基础路径
|
||||
if (!args.url.startsWith('http')) {
|
||||
args.url = config.baseURL + args.url
|
||||
}
|
||||
|
||||
// 合并headers
|
||||
args.header = {
|
||||
...config.defaultHeaders,
|
||||
...(args.header || {})
|
||||
}
|
||||
|
||||
// 添加认证头
|
||||
if (!args.skipAuth) {
|
||||
const token = uni.getStorageSync('token')
|
||||
if (token) {
|
||||
args.header.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
// 开发环境日志
|
||||
if (import.meta.env.VITE_DEBUG_MODE === 'true') {
|
||||
console.log('[Request]', args)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// 响应拦截处理
|
||||
const responseInterceptor = (res) => {
|
||||
if (import.meta.env.VITE_DEBUG_MODE === 'true') {
|
||||
console.log('[Response]', res)
|
||||
}
|
||||
|
||||
// 业务状态码处理
|
||||
if (res.data?.code !== 0 && res.data?.code !== 200) {
|
||||
return Promise.reject(res.data)
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
const errorHandler = async (error) => {
|
||||
console.error('[Request Error]', error)
|
||||
|
||||
// 网络错误
|
||||
if (error.errMsg?.includes('timeout')) {
|
||||
uni.showToast({ title: '请求超时', icon: 'none' })
|
||||
throw error
|
||||
}
|
||||
|
||||
// HTTP状态码处理
|
||||
switch (error.statusCode) {
|
||||
case 401:
|
||||
if (!error.config.skipAuth) {
|
||||
try {
|
||||
return await handle401Error(error)
|
||||
} catch (refreshError) {
|
||||
uni.showToast({ title: '登录已过期', icon: 'none' })
|
||||
throw refreshError
|
||||
}
|
||||
}
|
||||
break
|
||||
case 404:
|
||||
uni.showToast({ title: '接口不存在', icon: 'none' })
|
||||
break
|
||||
case 500:
|
||||
uni.showToast({ title: '服务器错误', icon: 'none' })
|
||||
break
|
||||
}
|
||||
|
||||
// 业务错误处理
|
||||
if (error.code) {
|
||||
uni.showToast({ title: error.message || '请求失败', icon: 'none' })
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
// 注册拦截器
|
||||
export const setupInterceptors = () => {
|
||||
uni.addInterceptor('request', {
|
||||
invoke: requestInterceptor,
|
||||
success: responseInterceptor,
|
||||
fail: errorHandler
|
||||
})
|
||||
|
||||
uni.addInterceptor('uploadFile', {
|
||||
invoke(args) {
|
||||
return requestInterceptor({
|
||||
...args,
|
||||
method: 'UPLOAD' // 添加特殊标识
|
||||
})
|
||||
},
|
||||
success: responseInterceptor,
|
||||
fail: errorHandler
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
// core/auth.js
|
||||
import { http } from './http'
|
||||
import { REFRESH_TOKEN_URL } from './constants/api'
|
||||
|
||||
const createAuthInterceptor = () => {
|
||||
let isRefreshing = false
|
||||
const queue = []
|
||||
|
||||
const getToken = () => ({
|
||||
token: uni.getStorageSync('token'),
|
||||
refreshToken: uni.getStorageSync('refreshToken')
|
||||
})
|
||||
|
||||
const refreshToken = async () => {
|
||||
const { refreshToken } = getToken()
|
||||
if (!refreshToken) throw new Error('No refresh token')
|
||||
|
||||
try {
|
||||
const { data } = await http.request({
|
||||
url: REFRESH_TOKEN_URL,
|
||||
method: 'POST',
|
||||
data: { refresh_token: refreshToken },
|
||||
skipAuth: true
|
||||
})
|
||||
|
||||
uni.setStorageSync('token', data.token)
|
||||
uni.setStorageSync('refreshToken', data.refreshToken)
|
||||
return data.token
|
||||
} catch (error) {
|
||||
clearAuthStorage()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const clearAuthStorage = () => {
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshToken')
|
||||
}
|
||||
|
||||
const authInterceptor = async (config) => {
|
||||
if (config.skipAuth) return config
|
||||
|
||||
const { token } = getToken()
|
||||
if (token) {
|
||||
config.header = {
|
||||
...config.header,
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
const authErrorHandler = async (error) => {
|
||||
const { statusCode, config } = error
|
||||
|
||||
if (statusCode !== 401 || config.skipAuth) throw error
|
||||
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true
|
||||
try {
|
||||
const newToken = await refreshToken()
|
||||
config.header.Authorization = `Bearer ${newToken}`
|
||||
queue.forEach(cb => cb())
|
||||
return http.request(config)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
queue.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
queue.push(() => resolve(http.request(config)))
|
||||
})
|
||||
}
|
||||
|
||||
return { authInterceptor, authErrorHandler }
|
||||
}
|
||||
|
||||
export const { authInterceptor, authErrorHandler } = createAuthInterceptor()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
// constants/api.js
|
||||
export const API_BASE = import.meta.env.VITE_API_BASE
|
||||
export const REFRESH_TOKEN_URL = `${API_BASE}${import.meta.env.VITE_AUTH_REFRESH_URL}`
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// core/http.js
|
||||
import { authInterceptor, authErrorHandler } from './auth'
|
||||
import { API_BASE } from './constants/api'
|
||||
|
||||
class Request {
|
||||
constructor() {
|
||||
this.config = {
|
||||
baseURL: API_BASE,
|
||||
timeout: Number(import.meta.env.VITE_REQUEST_TIMEOUT) || 10000,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-App-ID': import.meta.env.VITE_APP_ID,
|
||||
'X-App-Env': import.meta.env.VITE_APP_ENV
|
||||
}
|
||||
}
|
||||
|
||||
this._initDebugMode()
|
||||
this._initInterceptors()
|
||||
this.pendingRequests = new Map()
|
||||
}
|
||||
|
||||
_initDebugMode() {
|
||||
if (import.meta.env.VITE_DEBUG_MODE === 'true') {
|
||||
this._enableDebugLog()
|
||||
}
|
||||
}
|
||||
|
||||
_initInterceptors() {
|
||||
this.interceptors = {
|
||||
request: [this._handleMPFix, authInterceptor],
|
||||
response: [authErrorHandler]
|
||||
}
|
||||
}
|
||||
|
||||
// 小程序环境修复处理
|
||||
_handleMPFix = (config) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (config.url.startsWith('http:')) {
|
||||
config.url = config.url.replace('http:', 'https:')
|
||||
}
|
||||
// #endif
|
||||
return config
|
||||
}
|
||||
|
||||
// 新增环境判断方法
|
||||
isProduction() {
|
||||
return import.meta.env.VITE_APP_ENV === 'production'
|
||||
}
|
||||
|
||||
// 错误处理优化
|
||||
_handleError(error) {
|
||||
const errorMap = {
|
||||
401: '登录已过期,请重新登录',
|
||||
404: '请求资源不存在',
|
||||
500: '服务器开小差了',
|
||||
timeout: '请求超时,请检查网络'
|
||||
}
|
||||
|
||||
const message = this.isProduction()
|
||||
? '请求失败,请稍后重试'
|
||||
: errorMap[error.statusCode] || error.errMsg || '未知错误'
|
||||
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
duration: this.isProduction() ? 1500 : 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const http = new Request()
|
||||
|
|
@ -5,4 +5,7 @@ export default defineConfig({
|
|||
plugins: [
|
||||
uni(),
|
||||
],
|
||||
define: {
|
||||
'import.meta.env.VITE_DEBUG_MODE': JSON.stringify(process.env.NODE_ENV === 'development')
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue