axios.ts
import axios from 'axios'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import Config, { ApiCode } from '@common/config'
import type { IResponse } from '@/api/services/type'
import router from '@/router'
const { baseURL, timeout } = Config
const $axios = axios.create({ baseURL, timeout })
// 请求拦截器
$axios.interceptors.request.use(async config => {
const { headers = {} } = config
const { hasToken = true } = headers as AxiosRequestConfig['headers'] & { hasToken: boolean }
const token = localStorage.getItem(Config.token) || ''
// 设置token
if (hasToken) {
if (token) {
delete headers[Config.tokenKey]
headers[Config.tokenKey] = `bearer ${token}`
}
}
return config
})
// 返回体拦截器
$axios.interceptors.response.use(
async response => {
const { data = {} } = response as AxiosResponse<any> & IResponse<any>
const { code, msg, RESPONSE, data: _data } = data
// 检查 code 是否合法
if (code === ApiCode.SUCCESS) return [null, _data === null ? undefined : _data, data]
if (code === ApiCode.AUTH) {
localStorage.removeItem(Config.token)
window.$message.error(data.msg)
router.push('/login')
return [data, undefined, data]
}
if (msg) {
window.$message.error(msg)
data.showMessage = true
} else if (RESPONSE) {
window.$message.error(`接口请求异常,${JSON.stringify(RESPONSE)}`)
data.showMessage = true
}
return [`error: ${data.msg}`, _data, data]
},
async error => {
const { message } = error
if (message === Config.requestCancel) return Promise.resolve([error, undefined, undefined])
if (/Network\sError/.test(message)) console.error('请检查您的网络连接...')
else if (/timeout/.test(message)) console.error('网络超时~')
const data = error?.response?.data ?? error
// Message.error('登录失效')
// if (data.message === Code.authFail && !window.IS_DEV) window.location.href = Config.SSOUrl
return Promise.resolve([data, data?.data, data])
}
)
export default $axios
client.ts
import cConfig, { joinParams } from '@common/config'
import type { AxiosRequestConfig } from 'axios'
import axios from './axios'
import type { IDownloadFile, IResponse, IRequestParams as ReqParams, TData } from './type'
export class ApiClient {
static FetchMap = new Map() // 防重复容器
/**
* @param url
* @param params
* @param config axios 的 config
* @param client 配置自定义请求参数
*/
protected request<T = any>(params: ReqParams): Promise<[any, T, IResponse<T>]> {
const { url, params: data, config, client = {} } = params
const axiosConfig = config ?? {}
axiosConfig.headers = {
...(axiosConfig.headers ?? {}),
...client
} as AxiosRequestConfig['headers']
const method = params.method ?? (data === undefined ? 'GET' : 'POST')
return this.createAxios({ url, data, method, ...axiosConfig })
}
private async createAxios(config: AxiosRequestConfig) {
const { noRepeat = true } = config.headers ?? {}
const requestKey = joinParams(config)
// if (/(undefined|null)/.test(config.url)) {
// console.warn(`接口 ${config.url} 存在 undefined 或 null 参数,请查看是否正确`)
// }
if (noRepeat) {
// 防止重复请求逻辑
delete config.headers?.noRepeat
if (ApiClient.FetchMap.has(requestKey)) {
console.warn(`接口重复,已取消接口 ${config.url} 的重复请求, 已返回相同请求数据`)
return await ApiClient.FetchMap.get(requestKey) // 接口请求中,直接返回在请求的 Promise【解决被取消接口没数据返回问题】
}
const axiosRequest = axios({ ...config })
ApiClient.FetchMap.set(requestKey, axiosRequest)
const res = await axiosRequest
noRepeat && ApiClient.FetchMap.delete(requestKey)
return res
}
return await axios({ ...config })
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected downloadFile({ params, method = 'GET', fileName, url }: IDownloadFile) {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest()
req.open(method, /^https?:\/\//.test(url) ? url : `${cConfig.baseURL}${url}`, true)
req.responseType = 'blob'
req.setRequestHeader('Content-Type', 'application/json')
const token = localStorage.getItem(cConfig.token) || ''
req.setRequestHeader(cConfig.tokenKey, `bearer ${token}`)
// console.log(req)
req.onload = () => {
// console.log('----------')
// const { response } = req
// const { type } = response
// console.log({ response })
// TODO 下载文件下载待完成
// if (cConfig.excelKey.has(type)) {
// resolve({ data: 'succees', code: '' })
// loadFileData(response, `${fileName}${getFileSuffix(type)}`)
// } else if (type === 'text/plain') {
// }
let name = ''
// 名称从ResponseHeader取Content-disposition
const fileInfo = req.getResponseHeader('Content-disposition')
if (!fileInfo)
name = fileName ?? '未知名字'
else
name = decodeURI(fileInfo.split('=')[1])
try {
const { response } = req
const elink = document.createElement('a')
elink.download = name
elink.style.display = 'none'
elink.href = window.URL.createObjectURL(response)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href) // 释放URL 对象
document.body.removeChild(elink)
resolve({ data: 'succees', code: '' })
} catch (error) {
reject(error)
}
}
req.onerror = error => reject(error)
req.send(JSON.stringify(params || {}))
})
}
/**
* @param params
* @description 拼接参数
*/
protected serialize(params: TData): string {
return Object.entries(params)
.map(([key, value]) => (value !== undefined && value != null && value !== '' ? `${key}=${value}` : undefined))
.filter(Boolean)
.join('&')
}
}
export default new ApiClient()
type.d.ts
import type { AxiosRequestConfig } from 'axios'
export type TData = Record<string, any>
export interface IResponseList<T> {
count: number
page: number
list: T[]
}
export interface IClient {
/**
* 是否可以开启多次重复请求,默认: true
* 说明:
* 默认情况下,当某个请求发出,在该请求没完成的情况下,再发起同样的请求,后发的请求会被取消
* 如果 noRepeat 配置为 false 则关闭防重复请求功能
* 该配置用于 分包上传文件 等接口
*/
noRepeat?: boolean
/**
* 是否携带 token,默认: true
*/
hasToken?: boolean
}
export interface IResponse<T> {
data: T
code: string
msg?: string
}
export interface IRequestParams {
url: string
params?: TData
client?: IClient
config?: AxiosRequestConfig
method?: AxiosRequestConfig['method']
}
export interface IDownloadFile {
url: string
params?: TData
method?: IRequestParams['method']
fileName?: string
}
export interface PageInfo {
page: number
pageSize: number
}
services->axios->client
使用
import { ApiClient } from './services/client'
class LoginApi extends ApiClient {
// 品牌方用户账号密码登录验证
loginByAccountAndPassword<T>(params: LoginType.LoginByAccountAndPassword) {
return this.request<T>({ url: '/tianyin-service-user/brand/user/loginByAccountAndPassword', params })
}
}
export default new LoginApi()
async function loginByAccountAndPassword(params: LoginType.LoginByAccountAndPassword) {
const [err, data] = await $api.LoginApi.loginByAccountAndPassword<UserToken>(params)
if (err) return
token(data.token)
}