Axios
我自用的axios
单例模式封装,思路参考自 UmiJS 4
ts
// @ts-expect-error
import utils from 'axios/unsafe/utils'
import type {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
AxiosError,
InternalAxiosRequestConfig,
} from 'axios'
import axios from 'axios'
import requestConfig from './config'
type RequestError = AxiosError | Error
/**
* 请求参数的类型定义
* @param skipErrorHandler 是否跳过错误处理
* @param getResponse 是否获取原始ajax响应
* @param requestInterceptors 局部请求拦截器
* @param responseInterceptors 局部响应拦截器
*/
interface IRequestOptions extends AxiosRequestConfig {
/** 是否跳过错误处理 */
skipErrorHandler?: boolean
/** 是否获取原始ajax响应 */
getResponse?: boolean
/** 局部请求拦截器 */
requestInterceptors?: IRequestInterceptorTuple[]
/** 局部响应拦截器 */
responseInterceptors?: IResponseInterceptorTuple[]
[key: string]: any
}
/** 基础请求方法的类型定义 */
interface IRequest {
<T = any>(url: string, opts?: IRequestOptions): Promise<T>
}
/** 上传方法的类型定义 */
interface IUpload {
<T = any, D = any>(url: string, data: D, opts?: IRequestOptions): Promise<T>
}
interface IErrorHandler {
(error: RequestError, opts: IRequestOptions): void
}
type MaybePromise<T> = T | Promise<T>
/** 请求拦截器的类型定义 */
type IRequestInterceptor = (
config: IRequestOptions & InternalAxiosRequestConfig
) => MaybePromise<IRequestOptions & InternalAxiosRequestConfig>
/** 响应拦截器的类型定义 */
type IResponseInterceptor = (response: AxiosResponse) => MaybePromise<AxiosResponse>
/** 错误拦截器的类型定义 */
type IErrorInterceptor = (error: RequestError) => Promise<RequestError>
// 拦截器数组的类型定义
type IRequestInterceptorTuple =
| [IRequestInterceptor, IErrorInterceptor]
| [IRequestInterceptor]
| IRequestInterceptor
type IResponseInterceptorTuple =
| [IResponseInterceptor, IErrorInterceptor]
| [IResponseInterceptor]
| IResponseInterceptor
/**
* 请求实例的扩展配置
* @param errorConfig.errorHandler 错误处理器
* @param errorConfig.errorThrower 用来拦截错误重新包装后抛出
* @param requestInterceptors 全局请求拦截器
* @param responseInterceptors 全局响应拦截器
*/
interface RequestConfig<T = any> extends AxiosRequestConfig {
/** 错误处理配置 */
errorConfig?: {
errorHandler?: IErrorHandler
errorThrower?: (res: T) => void
}
/** 全局请求拦截器 */
requestInterceptors?: IRequestInterceptorTuple[]
/** 全局响应拦截器 */
responseInterceptors?: IResponseInterceptorTuple[]
}
const singletonEnforcer = Symbol()
class AxiosRequest {
private static _instance: AxiosRequest
private readonly service: AxiosInstance
private config: RequestConfig = {
// TODO 改成你的基础路径
baseURL: 'http://localhost',
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
}
constructor(enforcer: any) {
if (enforcer !== singletonEnforcer) {
throw new Error('Cannot initialize Axios client single instance')
}
this.mergeConfig()
this.service = axios.create(this.config)
// 请求拦截
this.config?.requestInterceptors?.forEach((interceptor) => {
interceptor instanceof Array
? this.service.interceptors.request.use(interceptor[0], interceptor[1])
: this.service.interceptors.request.use(interceptor)
})
// 响应拦截
this.config?.responseInterceptors?.forEach((interceptor) => {
interceptor instanceof Array
? this.service.interceptors.response.use(interceptor[0], interceptor[1])
: this.service.interceptors.response.use(interceptor)
})
}
/**
* 创建唯一实例
*/
static get instance() {
// 如果已经存在实例则直接返回, 否则实例化后返回
return this._instance || (this._instance = new AxiosRequest(singletonEnforcer))
}
/**
* 合并请求参数
*/
private mergeConfig() {
this.config = utils.merge(this.config, requestConfig)
}
/**
* 收集使用完毕的局部拦截器
* @param opts
*/
private getInterceptorsEject(opts: {
requestInterceptors?: IRequestInterceptorTuple[]
responseInterceptors?: IResponseInterceptorTuple[]
}) {
const { requestInterceptors, responseInterceptors } = opts
const requestInterceptorsToEject = requestInterceptors?.map((interceptor) => {
return interceptor instanceof Array
? this.service.interceptors.request.use(interceptor[0], interceptor[1])
: this.service.interceptors.request.use(interceptor)
})
const responseInterceptorsToEject = (responseInterceptors as IResponseInterceptorTuple[])?.map(
(interceptor) => {
return interceptor instanceof Array
? this.service.interceptors.response.use(interceptor[0], interceptor[1])
: this.service.interceptors.response.use(interceptor)
}
)
return { requestInterceptorsToEject, responseInterceptorsToEject }
}
/**
* 移除局部拦截器,避免影响其他请求
* @param opts
*/
private removeInterceptors(opts: {
requestInterceptorsToEject?: number[]
responseInterceptorsToEject?: number[]
}) {
const { requestInterceptorsToEject, responseInterceptorsToEject } = opts
requestInterceptorsToEject?.forEach((interceptor) => {
this.service.interceptors.request.eject(interceptor)
})
responseInterceptorsToEject?.forEach((interceptor) => {
this.service.interceptors.response.eject(interceptor)
})
}
/**
* 基础请求
* @param url 接口地址
* @param opts 请求参数
*/
request: IRequest = (url: string, opts = { method: 'GET' }) => {
const { getResponse = false, requestInterceptors, responseInterceptors } = opts
const { requestInterceptorsToEject, responseInterceptorsToEject } = this.getInterceptorsEject({
requestInterceptors,
responseInterceptors,
})
return new Promise((resolve, reject) => {
this.service
.request({ ...opts, url })
.then((res) => {
this.removeInterceptors({ requestInterceptorsToEject, responseInterceptorsToEject })
resolve(getResponse ? res : res.data)
})
.catch((error) => {
this.removeInterceptors({ requestInterceptorsToEject, responseInterceptorsToEject })
try {
const handler = this.config?.errorConfig?.errorHandler
if (handler) handler(error, opts)
} catch (e) {
reject(e)
} finally {
reject(error) // 如果不想把错误传递到方法调用处的话就去掉这个 finally
}
})
})
}
/**
* 上传
* @param url 接口地址
* @param opts 请求参数
*/
upload: IUpload = (url: string, data, opts = {}) => {
opts.headers = opts.headers ?? { 'Content-Type': 'multipart/form-data' }
const { getResponse = false, requestInterceptors, responseInterceptors } = opts
const { requestInterceptorsToEject, responseInterceptorsToEject } = this.getInterceptorsEject({
requestInterceptors,
responseInterceptors,
})
return new Promise((resolve, reject) => {
this.service
.post(url, data, opts)
.then((res) => {
this.removeInterceptors({ requestInterceptorsToEject, responseInterceptorsToEject })
resolve(getResponse ? res : res.data)
})
.catch((error) => {
this.removeInterceptors({ requestInterceptorsToEject, responseInterceptorsToEject })
try {
const handler = this.config?.errorConfig?.errorHandler
if (handler) handler(error, opts)
} catch (e) {
reject(e)
} finally {
reject(error) // 如果不想把错误传递到方法调用处的话就去掉这个 finally
}
})
})
}
/**
* 下载
* @param url 资源地址
* @param opts 请求参数
*/
download: IRequest = (url: string, opts = {}) => {
opts.responseType = opts.responseType ?? 'blob'
const { getResponse = false, requestInterceptors, responseInterceptors } = opts
const { requestInterceptorsToEject, responseInterceptorsToEject } = this.getInterceptorsEject({
requestInterceptors,
responseInterceptors,
})
return new Promise((resolve, reject) => {
this.service
.get(url, opts)
.then((res) => {
this.removeInterceptors({ requestInterceptorsToEject, responseInterceptorsToEject })
resolve(getResponse ? res : res.data)
})
.catch((error) => {
this.removeInterceptors({ requestInterceptorsToEject, responseInterceptorsToEject })
try {
const handler = this.config?.errorConfig?.errorHandler
if (handler) handler(error, opts)
} catch (e) {
reject(e)
} finally {
reject(error) // 如果不想把错误传递到方法调用处的话就去掉这个 finally
}
})
})
}
}
const requestInstance = AxiosRequest.instance
const request = requestInstance.request
const upload = requestInstance.upload
const download = requestInstance.download
export { request, upload, download }
export type {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
RequestError,
RequestConfig,
IResponseInterceptor as ResponseInterceptor,
IRequestOptions as RequestOptions,
IRequest as Request,
IUpload as Upload,
}
ts
import type { AxiosResponse, AxiosError } from 'axios'
import type { RequestConfig } from './http'
// 错误处理方案:错误类型
enum ErrorShowType {
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 3,
REDIRECT = 9,
}
// 与后端约定的响应数据格式
interface ResponseStructure<T = any> {
code: number | string
message: string
success: boolean
data?: T
url?: string
[key: string]: any
}
/**
* 业务错误处理
*/
function bizErrorHandler(error: any) {
if (error.info) {
const { errorMessage, errorCode, showType } = error.info
switch (showType) {
case ErrorShowType.SILENT:
// do nothing
break
case ErrorShowType.WARN_MESSAGE:
// TODO
break
case ErrorShowType.ERROR_MESSAGE:
// TODO
break
case ErrorShowType.NOTIFICATION:
// TODO
break
case ErrorShowType.REDIRECT:
// TODO
break
default:
// TODO
console.error(errorMessage)
}
}
}
const requestConfig: RequestConfig<ResponseStructure> = {
errorConfig: {
// 抛出错误
errorThrower: (res) => {
const { success, data, code, message, errorCode, errorMessage, showType } = res
if (!success) {
const error: any = new Error(errorMessage || message)
// 给错误对象挂载自定义属性,表明这是业务层的错误
error.name = 'BizError'
error.info = {
errorCode: errorCode ?? code,
errorMessage: errorMessage ?? message,
showType,
data,
}
throw error // 抛出自定义的错误,请求方法中的 .catch 部分会捕获
}
},
// 错误接收及处理
errorHandler: (error: any, opts) => {
if (opts?.skipErrorHandler) return
// 自定义错误的处理
if (error.name === 'BizError') {
bizErrorHandler(error)
} else if (error.name === 'AxiosError') {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
const { status, data } = error.response as AxiosResponse
// TODO 在这里处理HTTP错误
} else if (error.request) {
// 请求已经成功发起,但没有收到响应
// error.request 在浏览器中是 XMLHttpRequest 的实例
// 而在node.js中是 http.ClientRequest 的实例
// TODO
console.error('None response! Please retry.')
} else {
// 发送请求时出了点问题
// TODO
console.error('Request error, please retry')
}
},
},
requestInterceptors: [
[
(config) => {
// 拦截请求配置,进行个性化处理。
// TODO
return { ...config }
},
(error: AxiosError) => {
return Promise.reject(error)
},
],
],
// 状态码 2xx 的时候才会进入响应拦截,其他情况已经在请求方法中的.catch部分处理了
responseInterceptors: [
(response) => {
const { data } = response
// 请求失败
if (!data.success) {
requestConfig.errorConfig?.errorThrower?.(data)
}
return response
},
],
}
export default requestConfig
助手函数
ts
type AnyObj = Record<string, any>
const { getPrototypeOf } = Object
const kindOf = ((cache) => (thing: any) => {
const str = toString.call(thing)
return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase())
})(Object.create(null))
function kindOfTest(type: string) {
type = type.toLowerCase()
return (thing: any) => kindOf(thing) === type
}
const typeOfTest = (type: string) => (thing: any) => typeof thing === type
function findKey(obj: object, key: string) {
key = key.toLowerCase()
const keys = Object.keys(obj)
let i = keys.length
let _key
while (i-- > 0) {
_key = keys[i]
if (key === _key.toLowerCase()) return _key
}
return null
}
const _global = (() => {
if (typeof globalThis !== 'undefined') return globalThis
return typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : global
})()
const singletonEnforcer = Symbol()
// 助手函数写这里
class Shared {
private static _instance: Shared
constructor(enforcer: any) {
if (enforcer !== singletonEnforcer) throw new Error('Cannot initialize single instance')
}
static get instance() {
// 如果已经存在实例则直接返回, 否则实例化后返回
return this._instance || (this._instance = new Shared(singletonEnforcer))
}
/** @description 是否为数组 */
isArray = Array.isArray
/** @description 是否为 undefined */
isUndefined = typeOfTest('undefined')
/** @description 是否为对象 */
isObject(thing: any) {
return thing !== null && typeof thing === 'object'
}
/** @description 是否为函数 */
isFunction = typeOfTest('function')
/** @description 是否为数字 */
isNumber = typeOfTest('number')
/** @description 是否为布尔值 */
isBoolean(thing: any) {
return thing === true || thing === false
}
/** @description 是否为字符串 */
isString = typeOfTest('string')
/** @description 是否为 Date 对象 */
isDate = kindOfTest('Date')
/** @description 是否为 File 对象 */
isFile = kindOfTest('File')
/** @description 是否为 FileList 对象 */
isFileList = kindOfTest('FileList')
/** @description 是否为 Blob 对象 */
isBlob = kindOfTest('Blob')
/** @description 是否为 Stream流 */
isStream(val: any) {
return this.isObject(val) && this.isFunction(val.pipe)
}
/** @description 是否为 URLSearchParams 对象 */
isURLSearchParams = kindOfTest('URLSearchParams')
/** @description 是否为 HTMLFormElement 对象 */
isHTMLForm = kindOfTest('HTMLFormElement')
/** @description 是否为 ArrayBuffer 对象 */
isArrayBuffer = kindOfTest('ArrayBuffer')
/** @description 是否为 RegExp 对象 */
isRegExp = kindOfTest('RegExp')
/** @description 是否为异步函数 */
isAsyncFn = kindOfTest('AsyncFunction')
/** @description 是否存在上下文对象 */
isContextDefined(context: any) {
return !this.isUndefined(context) && context !== _global
}
/** @description 是否为 Buffer 对象 */
isBuffer(val: any): boolean {
return (
val !== null &&
!this.isUndefined(val) &&
val.constructor !== null &&
!this.isUndefined(val.constructor) &&
this.isFunction(val.constructor.isBuffer) &&
val.constructor.isBuffer(val)
)
}
/** @description 是否为 ArrayBuffer 对象 */
isArrayBufferView(val: any): boolean {
let result: any
if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView) result = ArrayBuffer.isView(val)
else result = val && val.buffer && this.isArrayBuffer(val.buffer)
return result
}
/** @description 是否为 plain object */
isPlainObject(val: any) {
if (kindOf(val) !== 'object') return false
const prototype = getPrototypeOf(val)
return (
(prototype === null ||
prototype === Object.prototype ||
Object.getPrototypeOf(prototype) === null) &&
!(Symbol.toStringTag in val) &&
!(Symbol.iterator in val)
)
}
/** @description 是否为 FormData 对象 */
isFormData(thing: any) {
let kind: any
return (
thing &&
((typeof FormData === 'function' && thing instanceof FormData) ||
(this.isFunction(thing.append) &&
((kind = kindOf(thing)) === 'formdata' ||
// detect form-data instance
(kind === 'object' &&
this.isFunction(thing.toString) &&
thing.toString() === '[object FormData]'))))
)
}
/** @description 是否为 FormData 对象 */
isSpecCompliantForm(thing: any) {
return !!(
thing &&
this.isFunction(thing.append) &&
thing[Symbol.toStringTag] === 'FormData' &&
thing[Symbol.iterator]
)
}
/** @description 是否有 then 方法 */
isThenable(thing: any) {
return (
thing &&
(this.isObject(thing) || this.isFunction(thing)) &&
this.isFunction(thing.then) &&
this.isFunction(thing.catch)
)
}
/** @description 是否绝对地址 */
isAbsoluteURL(url: string) {
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
}
/** @description 去除字符串首尾的空白符 */
trim(str: string) {
return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '')
}
/** @description 去除字符串中的 BOM */
stripBOM(content: string) {
if (content.charCodeAt(0) === 0xfeff) content = content.slice(1)
return content
}
/** @description 把 横线、下划线、空格 连接起来的字符串转为小驼峰字符串 */
toCamelCase(str: string) {
return str.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g, (m, p1, p2) => {
return p1.toUpperCase() + p2
})
}
/** @description 判断对象是否有某属性 */
hasOwnProperty = (
({ hasOwnProperty }) =>
(obj: object, prop: string) =>
hasOwnProperty.call(obj, prop)
)(Object.prototype)
/** @description 把baseURL和relativeURL组合起来 */
combineURLs(baseURL: string, relativeURL: string) {
return relativeURL
? `${baseURL.replace(/\/+$/, '')}/${relativeURL.replace(/^\/+/, '')}`
: baseURL
}
/** @description 将类数组对象转为真正的数组 */
toArray(thing: any) {
if (!thing) return null
if (this.isArray(thing)) return thing
let i = thing.length
if (!this.isNumber(i)) return null
const arr = new Array(i)
while (i-- > 0) arr[i] = thing[i]
return arr
}
/** @description 迭代数组或对象 */
forEach(obj: AnyObj | Array<any>, fn: (...args: any[]) => void) {
if (obj === null || typeof obj === 'undefined') return
if (typeof obj !== 'object') obj = [obj]
if (this.isArray(obj)) {
for (let i = 0, l = obj.length; i < l; i++) fn.call(null, obj[i], i, obj)
} else {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) fn.call(null, obj[key], key, obj)
}
}
}
/** @description 对象合并 */
merge<T = AnyObj>(...args: object[]): T {
// @ts-expect-error ?
const { caseless } = (this.isContextDefined(this) && this) || {}
const result = {}
const assignValue = (val: any, key: string) => {
const targetKey = (caseless && findKey(result, key)) || key
if (this.isPlainObject(result[targetKey]) && this.isPlainObject(val))
result[targetKey] = this.merge(result[targetKey], val)
else if (this.isPlainObject(val)) result[targetKey] = this.merge({}, val)
else if (this.isArray(val)) result[targetKey] = val.slice()
else result[targetKey] = val
}
for (let i = 0, l = arguments.length; i < l; i++) args[i] && this.forEach(args[i], assignValue)
return result as T
}
/** @description 将文件对象转为URL */
readBlob2Url(blob: Blob, cb: (url: any) => void) {
if (!this.isBlob(blob)) throw new Error('is not Blob')
new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.onerror = (error) => reject(error)
reader.readAsDataURL(blob)
}).then(cb)
}
/** @description 洗牌算法 */
shuffle<T = any>(arr: T[]) {
const res: T[] = []
let random = void 0
while (arr.length > 0) {
random = Math.floor(Math.random() * arr.length)
res.push(arr.splice(random, 1)[0])
}
return res
}
/** @description 深拷贝 */
deepClone<T = any>(source: T, cache = new WeakMap()): T {
if (typeof source !== 'object' || source === null) return source
if (cache.has(source)) return cache.get(source)
const target = Array.isArray(source) ? [] : {}
Reflect.ownKeys(source).forEach((key) => {
const val = source[key]
if (typeof val === 'object' && val !== null) target[key] = this.deepClone(val, cache)
else target[key] = val
})
return target as T
}
}
export const Utils = Shared.instance