refactor(http): 重构 HTTP 请求模块使用 axios 替代 fetch

- 替换 fetch 为 axios 以提供更好的请求拦截和错误处理能力
- 添加请求拦截器自动处理 Authorization 头和时间戳
- 实现响应拦截器统一处理业务错误码和状态码
- 优化错误提示和认证失败后的跳转逻辑
- 新增 await-to-js 和 axios 依赖
This commit is contained in:
houakang
2026-04-10 10:05:54 +08:00
parent 22fe6e069c
commit d7d089cc2a
3 changed files with 137 additions and 42 deletions

View File

@@ -45,6 +45,8 @@
"element-plus": "^2.13.6", "element-plus": "^2.13.6",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.32", "vue": "^3.5.32",
"vue-router": "^4.6.4" "vue-router": "^4.6.4",
"await-to-js": "^3.0.0",
"axios": "^1.13.2"
} }
} }

View File

@@ -1,38 +1,140 @@
import { getBaseUrl } from './url.js' import axios from 'axios'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getBaseUrl } from './url.js'
async function request(path, options = {}) { // baseURL 由主进程动态分配端口,通过 getBaseUrl() 运行时获取
const { headers = {}, silent = false, ...rest } = options const axiosInstance = axios.create({
const url = `${getBaseUrl()}${path}` baseURL: getBaseUrl(),
timeout: 300000,
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
responseType: 'json',
})
const config = { // 请求拦截
headers: { axiosInstance.interceptors.request.use((config) => {
'Content-Type': 'application/json', config.headers = config.headers || {}
...headers, let Authorization = localStorage.getItem('Authorization')
}, // 优先使用本地持久化的 Authorization 头(完整值)
...rest, config.headers.Authorization = Authorization || ''
if ('get' === config?.method?.toLowerCase()) {
if (config.params) {
config.params.timestamp = new Date().getTime()
}
} }
// 移除敏感信息日志
// console.log(config, 'axios request.use config')
return config
})
axiosInstance.interceptors.response.use(
(response) => {
// 移除敏感信息日志
// console.log(response, 'response response')
// 若请求为二进制下载blob直接透传响应交由调用方自行处理
try {
const isBlob = response?.config?.responseType === 'blob'
if (isBlob) {
// 仍然尝试持久化可能返回的 Authorization
const respHeaders = response?.headers || {}
const newAuthorization = respHeaders['authorization'] || respHeaders['Authorization']
if (newAuthorization && typeof newAuthorization === 'string') {
localStorage.setItem('Authorization', newAuthorization)
}
return response
}
} catch (e) {
console.log(e)
// 忽略检查失败
}
// 如果响应头里带有 Authorization则使用 useStorage 持久化到 localStorage
// 以便后续请求自动携带该请求头
try {
const respHeaders = response?.headers || {}
const newAuthorization = respHeaders['authorization'] || respHeaders['Authorization']
if (newAuthorization && typeof newAuthorization === 'string') {
localStorage.setItem('Authorization', newAuthorization)
}
} catch (e) {
// 忽略持久化失败,避免影响主流程
console.warn('持久化 Authorization 失败:', e)
}
if (response.status === 200) {
const res = response.data || {}
const code = res.code
const msg = res.message || res.msg
try { // 明确的 200 成功,但需要按业务码再判断
const res = await fetch(url, config) if (code === 0) {
// 业务成功
return Promise.resolve(res)
}
if (!res.ok) { // 特殊业务码处理
const msg = await res.text().catch(() => '') if (code === 401) {
const errMsg = `请求失败: ${res.status}${msg ? ' - ' + msg : ''}` // 清除持久化的 Authorization避免后续使用失效的头部
if (!silent) ElMessage.error(errMsg) localStorage.removeItem('Authorization')
return Promise.reject(new Error(errMsg)) sessionStorage.removeItem('Token')
// 延迟跳转,确保消息显示
setTimeout(() => {
window.location.href = '/#/login'
}, 500)
return Promise.reject(new Error('认证失败,请重新登录'))
}
// 其余非 0 的业务码统一拦截提示,但不在这里显示 ElMessage
// 交由业务层使用 await-to-js 处理
return Promise.reject(new Error(msg || '请求失败'))
} }
// 空响应 // 非 2xx 按错误分支处理(通常会进入 error 拦截器)
const text = await res.text() return Promise.reject(new Error('请求失败'))
if (!text) return Promise.resolve(null) },
(error) => {
console.error('请求错误:', error)
const data = JSON.parse(text) if (error.response) {
return Promise.resolve(data) // 服务器响应错误
} catch (err) { const status = error.response.status
if (!silent) ElMessage.error(err.message || '网络连接失败') const message = error.response.data?.message || error.response.data?.msg || '请求失败'
return Promise.reject(err)
}
}
export default request switch (status) {
case 400:
ElMessage.error(`无效的请求参数:${message}`)
break
case 401:
// 清除持久化的 Authorization避免后续使用失效的头部
localStorage.removeItem('Authorization')
ElMessage.error('未授权访问或登录已过期,请重新登录')
break
case 403:
ElMessage.error('访问被拒绝')
break
case 404:
ElMessage.error('资源未找到')
break
case 500:
ElMessage.error('服务器内部错误')
break
case 502:
case 503:
case 504:
ElMessage.error('服务暂时不可用,请稍后重试')
break
default:
ElMessage.error(`请求失败: ${message}`)
}
} else if (error.request) {
// 网络错误
ElMessage.error('网络连接失败,请检查网络连接')
} else {
// 其他错误
ElMessage.error('请求发送失败')
}
return Promise.reject(error)
},
)
export default axiosInstance

View File

@@ -1,26 +1,17 @@
import request from './index.js' import request from './index.js'
export function getAction(url, params) { export function getAction(url, params) {
const query = params ? '?' + new URLSearchParams(params).toString() : '' return request({ url, method: 'GET', params })
return request(`${url}${query}`, { method: 'GET' })
} }
export function postAction(url, data, headers = {}) { export function postAction(url, data, headers = {}) {
return request(url, { return request({ url, method: 'POST', data, headers })
method: 'POST',
body: JSON.stringify(data),
headers,
})
} }
export function putAction(url, data) { export function putAction(url, data) {
return request(url, { return request({ url, method: 'PUT', data })
method: 'PUT',
body: JSON.stringify(data),
})
} }
export function deleteAction(url, params) { export function deleteAction(url, params) {
const query = params ? '?' + new URLSearchParams(params).toString() : '' return request({ url, method: 'DELETE', params })
return request(`${url}${query}`, { method: 'DELETE' })
} }