TS二次封装axios学习总结
1.put请求
put请求一般用的很少,主要有两点区别。
1.PUT请求:如果两个请求相同,后一个请求会把第一个请求覆盖掉。(所以PUT用来改资源)
Post请求:后一个请求不会把第一个请求覆盖掉。(所以Post用来增资源)
2.PUT通常指定了资源的存放位置,而POST则没有,POST的数据存放位置由服务器自己决定。
2.ts内置高级对象Record
用法:getHourCheckinList(params?: Record<string, any>){}
Record 是 TS 内置的一个高级类型,是通过映射类型的语法来生成索引类型的,Record<string, any> 也就是 key 为 string 类型,value 为任意类型的索引类型,可以代替 object 来用,用来指定object内部的key value的数据类型。
其他高级内置对象
http.ts 二次封装axios:
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'
import { loginApi } from '@/apis'
import type { ApiResponse, RefreshParams, RefreshData, HttpResponse } from '../../@types/api'
// 定义全局请求配置接口
interface HttpClientOptions {
baseURL?: string
headers?: Record<string, string>
timeout?: number
}
// 定义特殊错误码的处理类型
type CodeHandler = (error: ApiResponse['err']) => void
// 定义重试配置接口
interface RetryConfig {
maxRetries: number; // 最大重试次数
retryDelay: number; // 重试延迟时间
retryAbleStatus: number[]; // 可重试的状态码
}
// 通用请求类
class HttpClient {
private instance: AxiosInstance
private isRefreshing = false // 用于防止同时刷新多个 token
private refreshSubscribers: Array<(token: string) => void> = []
private codeHandlers: Record<number, CodeHandler> = {} // 用于存储特殊错误码的处理器
private customHeaders: Record<string, string | number> = {} // 存储自定义请求头
private retryConfig: RetryConfig = {
maxRetries: 3,
retryDelay: 1000,
retryAbleStatus: [408, 500, 502, 503, 504]
}
// 添加设置自定义请求头的方法
public setCustomHeaders(headers: Record<string, string | number>) {
this.customHeaders = { ...this.customHeaders, ...headers };
}
constructor(options: HttpClientOptions) {
this.instance = axios.create({
baseURL: options.baseURL,
headers: options.headers || {}, // 默认请求头
timeout: options.timeout || 15000, // 请求超时时间(默认10秒)
withCredentials: false // 不携带 cookie,避免 CORS 问题
})
// 请求拦截器
this.instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 获取 token
const token = localStorage.getItem('token')
// 如果有 token,添加到请求头
if (token) {
config.headers.Authorization = `${token}`
}
// 添加通用请求头
config.headers['Content-Type'] = 'application/json'
config.headers.Accept = 'application/json'
// 从 localStorage 获取 tenant-id 和 hotel-id
const tenantId = localStorage.getItem('tenant-id')
const hotelId = localStorage.getItem('hotel-id')
if (tenantId) {
config.headers['tenant-id'] = tenantId
}
if (hotelId) {
config.headers['hotel-id'] = hotelId
}
// 添加自定义请求头
Object.assign(config.headers, this.customHeaders)
// 登录请求不需要带 trace-id
if (config.url?.includes('/api/admin/login')) {
return config
}
// 从本地缓存获取 Trace-Id
const traceId = localStorage.getItem('trace-id')
if (traceId) {
config.headers['trace-id'] = traceId
}
return config
},
(error) => {
ElMessage.error(error)
return Promise.reject(error)
}
)
// 响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const apiResponse = response.data;
// 保存新的 Trace-Id
const newTraceId = response.headers['trace-id']
if (newTraceId) {
localStorage.setItem('trace-id', newTraceId)
}
// 查 API 响应的状态并处理
if (!apiResponse.ok) {
// 如果需要处理 API 错误,以在这里加入逻辑
ElMessage.error(response.data.err?.msg || '操作失败')
return Promise.reject(apiResponse.err);
}
// 返回原始的 AxiosResponse 类型,包裹 apiResponse
return {
...response,
data: apiResponse, // 保持 data 字段为 ApiResponse 类型
};
},
async (error) => {
// 即使请求失败也要保存 Trace-Id
const newTraceId = error.response?.headers?.['trace-id']
if (newTraceId) {
localStorage.setItem('trace-id', newTraceId)
}
const { response } = error;
if (response?.status === 401) {
return this.handle401Error(error);
}
console.error('请求错误:', response?.data || error.message);
return Promise.reject(error);
}
);
}
// 注册特殊错误码的处理器
public registerCodeHandler(code: number, handler: CodeHandler) {
this.codeHandlers[code] = handler
}
// 处理 401 错误
private async handle401Error(originalError: any) {
if (!this.isRefreshing) {
this.isRefreshing = true
try {
// 从 localStorage 取刷新令牌相关信息
const userId = parseInt(localStorage.getItem('userId') || '0')
const refreshToken = localStorage.getItem('refreshToken')
if (!userId || !refreshToken) {
throw new Error('刷新令牌信息不存在')
}
// 调用刷新令牌接口
const refreshParams: RefreshParams = {
user_id: userId,
refresh_token: refreshToken
}
const res = await loginApi.refreshToken(refreshParams) as HttpResponse<RefreshData>
if (res.data.ok) {
// 更新存储的令牌
const { token, refresh_token } = res.data.data
localStorage.setItem('token', token)
localStorage.setItem('refreshToken', refresh_token)
// 通知所有等待的请求
this.refreshSubscribers.forEach(callback => callback(token))
this.refreshSubscribers = []
// 重试原始请求
return this.retryOriginalRequest(originalError, token)
} else {
throw new Error(res.data.err?.msg || '刷新令牌失败')
}
} catch (error) {
// 清除所有认证信息
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
localStorage.removeItem('userId')
// 显示错误消息
ElMessage.error('登录已过期,请重新登录')
// 跳转到登录页
window.location.href = '/login';
return Promise.reject(error)
} finally {
this.isRefreshing = false
}
} else {
// 如果已经在刷新中,将请求加入队列
return new Promise((resolve) => {
this.refreshSubscribers.push((token: string) => {
resolve(this.retryOriginalRequest(originalError, token))
})
})
}
}
// 通用重试机制
private async retryRequest(config: AxiosRequestConfig, retryCount = 0): Promise<any> {
try {
return await this.instance.request(config)
} catch (error: any) {
// 判断是否可以重试
const canRetry = this.retryConfig.retryAbleStatus.includes(error.response?.status)
&& retryCount < this.retryConfig.maxRetries;
if (canRetry) {
// 计算延迟时间
const delay = this.retryConfig.retryDelay * Math.pow(2, retryCount)
console.log(`请求失败,${delay}ms 后进行第 ${retryCount + 1} 次重试`)
// 等待延迟时间
await new Promise(resolve => setTimeout(resolve, delay))
// 递归重试
return this.retryRequest(config, retryCount + 1)
}
// 不能重试则抛出错误
throw error
}
}
// token 刷新重试机制
private retryOriginalRequest(error: any, token: string) {
const config = error.config
config.headers.Authorization = `${token}`
return this.instance.request(config)
}
public get<T>(url: string, params?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> {
return this.instance.get(url, { params, ...config })
}
// POST 请求
public post<T>(url: string, data?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> {
return this.instance.post(url, data, config)
}
// PUT 请求
public put<T>(url: string, data?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> {
return this.instance.put(url, data, config)
}
// DELETE 请求
public delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.delete(url, config)
}
// PATCH 请求
public patch<T>(url: string, data?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> {
return this.instance.patch(url, data, config)
}
}
export default HttpClient
api.ts 统一设置请求头
import HttpClient from './http'
// 根据环境变量设置基础 URL
const baseURL = import.meta.env.VITE_API_URL
const apiClient = new HttpClient({
baseURL,
timeout: 15000, // 添加超时设置
headers: { // 添加通用请求头
'Content-Type': 'application/json'
},
})
// 添加设置请求头的方法
export const setApiHeaders = (tenantId: string | number, hotelId: string | number) => {
// 保存到本地存储
localStorage.setItem('tenant-id', tenantId.toString());
localStorage.setItem('hotel-id', hotelId.toString());
// 设置到请求头
apiClient.setCustomHeaders({
'tenant-id': tenantId,
'hotel-id': hotelId
});
}
export { apiClient }