调整请求拦截

This commit is contained in:
chenweiqiang 2025-04-30 17:11:28 +08:00
parent 3e8c56b5c5
commit bee838d474
14 changed files with 370 additions and 231 deletions

1
.env
View File

@ -1,3 +1,4 @@
# 公共配置
VITE_APP_NAME = GroupBuying
VITE_APP_VERSION = 1.0.0
VITE_API_BASE = ""

View File

@ -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

View File

@ -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

View File

@ -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')

0
src/api/auth/login.js Normal file
View File

View File

@ -2,8 +2,10 @@ 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,
};

View File

@ -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
}
}

View File

@ -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()

View File

@ -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
})
})
}

View File

@ -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
})
}

View File

@ -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()

View File

@ -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}`

View File

@ -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()

View File

@ -5,4 +5,7 @@ export default defineConfig({
plugins: [
uni(),
],
define: {
'import.meta.env.VITE_DEBUG_MODE': JSON.stringify(process.env.NODE_ENV === 'development')
}
})