RequestService
使用示例
ts
import { RequestService } from '.'
import type { RequestConfig } from '.'
export const service = new RequestService({
retry: {
maxCount: 2,
delay: 500,
timing: 'linear', // 'linear' | 'exponential'
retryIf(error, count) {
// 全局自定义重试条件
console.log('global retry', error, count)
return true
},
onRetry(error, count, delay) {
console.log('global retry', error, count, delay)
},
},
cache: {
storage: 'memory', // 'memory' | 'session' | 'local'
expireTime: 5 * 60 * 1000,
},
queue: {
maxConcurrent: 5, // 最大并发数
},
})
service.interceptors.request.use((config) => {
console.log('request', config)
if (config.withToken) {
if (!config.headers) config.headers = {}
config.headers['Authorization'] =
'Bearer ' + localStorage.getItem('token')
}
return config
})
service.interceptors.response.use((response) => {
console.log('response', response)
const config = response.config as RequestConfig
// 明确不处理 response 或 responseType 不是 json 时,直接返回 response
if (!config.handleResponse || config.responseType !== 'json') {
return response
}
const { code, msg, data } = response.data
switch (code) {
case 200:
return data
case 401:
alert('未授权')
// 处理未授权
break
default:
// 处理其他错误
alert(msg)
break
}
return data
})
/**
* 使用示例
********************************/
// 不带 token
service.get('/login', { withToken: false })
// 不处理 response
service.get('/api', { handleResponse: false })
// 启用缓存
service.get('/api', { cache: true })
// 设置优先级
service.get('/api', { priority: 9 })
// 阻止重复请求
// 取消新的请求
service.get('/api', { preventDuplicate: 'cancel-new' })
// 取消旧的请求
service.get('/api', { preventDuplicate: 'cancel-old' })
// 将新的请求链接到旧的请求,新的请求直接使用旧的请求的响应数据
service.get('/api', { preventDuplicate: 'link' })
// 请求重试
// 启用重试,如果配置了重试,则使用默认启用重试
service.get('/api', { retry: true })
// 禁用重试
service.get('/api', { retry: false })
// 启用重试,并设置重置配置
service.get('/api', {
retry: {
maxCount: 3,
delay: 1000,
timing: 'exponential',
retryIf: (error) => error.code === 'ECONNABORTED',
onRetry: (error, retryCount, delay) => {
console.log('retry', retryCount, delay)
},
},
})封装实现
ts
export * from './service'
export type * from './types'ts
import {
AxiosInterceptorManager,
AxiosRequestConfig,
AxiosResponse,
CreateAxiosDefaults,
} from 'axios'
export interface RequestServiceConfig extends CreateAxiosDefaults {
/**
* 默认重试配置
*
* 配置此属性后,所有请求都会启用重试机制
*/
retry?: RetryConfig
/**
* 默认请求缓存配置
*/
cache?: RequestCacheConfig
/**
* 请求队列配置
*/
queue?: RequestQueueConfig
}
export interface RequestConfig<D = any> extends AxiosRequestConfig<D> {
/**
* 请求优先级 0-9,值越大,优先级越高
*
* @default 5
*/
priority?: number
/**
* 阻止重复请求策略
*
* 'cancel-new': 取消新的请求,继续使用旧的请求
* 'cancel-old': 取消旧的请求,使用新的请求
* 'link': 将新的请求链接到旧的请求,新的请求直接使用旧的请求的响应数据
*/
preventDuplicate?: 'cancel-new' | 'cancel-old' | 'link'
/**
* 重试配置,会覆盖默认配置
*/
retry?: RetryConfig | boolean
/**
* 重试次数
*/
retryCount?: number
/**
* 是否启用缓存
*/
cache?: boolean
/**
* 是否处理response
*
* @default true
*/
handleResponse?: boolean
/**
* 是否携带token
*
* @default true
*/
withToken?: boolean
}
export interface RetryConfig {
/**
* 最大重试次数
*
* @default 2
*/
maxCount?: number
/**
* 重试间隔时间 (ms)
*
* @default 500
*/
delay?: number
/**
* 时间曲线
*
* @default 'linear'
*/
timing?: 'linear' | 'exponential'
/**
* 是否重试
*
* 返回 true 时执行重试
*
* 执行顺序:
* 1. `RequestServiceConfig.retry.retryIf` 这里用于控制所有请求是否重试
* 2. `RequestConfig.retry.retryIf` 这里用于控制单个请求是否重试
*/
retryIf?: (error: any, count: number) => boolean
/**
* 重试事件
*
* 执行顺序:
* 1. `RequestServiceConfig.retry.onRetry`
* 2. `RequestConfig.retry.onRetry`
*/
onRetry?: (error: any, count: number, delay: number) => void
}
export interface RequestQueueConfig {
/**
* 最大并发数
*
* @default 5
*/
maxConcurrent?: number
}
export interface RequestCacheConfig {
/**
* 缓存存储位置
*
* @default 'memory'
*/
storage?: 'memory' | 'session' | 'local'
/**
* 缓存过期时间
*
* @default 5 * 60 * 1000
*/
expireTime?: number
}
export interface RequestError extends Error {
code?: string
config: RequestConfig
response?: AxiosResponse
isAxiosError: boolean
retryCount?: number
}
export interface BaseResponse<T = any> {
data: T
code: number
msg: string
}
export type ServiceInterceptors = {
request: AxiosInterceptorManager<RequestConfig>
response: AxiosInterceptorManager<AxiosResponse>
}ts
import axios, { AxiosError } from 'axios'
import { RequestQueue } from './queue'
import type { AxiosInstance } from 'axios'
import type {
BaseResponse,
RequestConfig,
RequestServiceConfig,
RetryConfig,
ServiceInterceptors,
} from './types'
import { isFunction, isNumber, isObject, sleep } from './utils'
import { RequestCacher } from './cache'
export class RequestService {
private readonly retryConfig?: RetryConfig
private readonly axiosInstance: AxiosInstance
private readonly queue: RequestQueue
private readonly cacher: RequestCacher
constructor(config?: RequestServiceConfig) {
const {
retry: retryConfig,
queue: queueConfig,
cache: cacheConfig,
...axiosConfig
} = config || {}
this.axiosInstance = axios.create(axiosConfig)
this.queue = new RequestQueue(queueConfig)
this.cacher = new RequestCacher(cacheConfig)
if (retryConfig) {
this.retryConfig = {
maxCount: 2,
delay: 500,
timing: 'linear',
...retryConfig,
}
}
}
get interceptors() {
return this.axiosInstance.interceptors as ServiceInterceptors
}
/**
* 生成请求ID
*/
private generateRequestID(config: RequestConfig) {
const { method, url, params, data } = config
const compose: string[] = []
if (method) compose.push(method.toLocaleUpperCase())
if (url) compose.push(url)
if (params) compose.push(JSON.stringify(params))
if (data) compose.push(JSON.stringify(data))
return compose.join('_')
}
/**
* 判断是否需要重试
*/
private shouldRetry(error: unknown): boolean {
if (error instanceof AxiosError) {
const { code, config, response } = error
const retryConfig = (config as RequestConfig)?.retry
const retryCount = (config as RequestConfig)?.retryCount
if (retryConfig === false) {
// 明确不重试
return false
}
if (
isObject(retryConfig) &&
isNumber(retryCount) &&
retryCount >= (retryConfig.maxCount || 0)
) {
// 超过最大重试次数
return false
}
if (
this.retryConfig?.retryIf &&
!this.retryConfig.retryIf(error, retryCount!)
) {
// 全局自定义重试条件不满足
return false
}
if (
isObject(retryConfig) &&
isFunction(retryConfig.retryIf) &&
this.retryConfig?.retryIf !== retryConfig.retryIf && // 避免重复判断全局条件
!retryConfig.retryIf(error, retryCount!)
) {
// 自定义重试条件不满足
return false
}
if (code === 'ECONNABORTED') {
// 超时
// return false
}
// 网络错误
if (code === 'NETWORK_ERROR') {
// return false
}
// 请求被取消
if (axios.isCancel(error)) {
return false
}
if (response) {
if (response.status >= 500 && response.status < 600) {
// 服务器错误
return false
}
if (response.status === 401) {
// 未授权
return false
}
if (response.status === 404) {
// 未找到资源
return false
}
}
}
if (!this.retryConfig) {
// 没有配置重试,默认不重试
return false
}
return true
}
/**
* 请求重试
*/
private async requestRetry<
R extends BaseResponse = BaseResponse,
D = any,
>(config: RequestConfig, error: unknown) {
if (config.retryCount === void 0) {
config.retryCount = 1
} else {
config.retryCount++
}
const retryConfig = config.retry as RetryConfig
let delay = retryConfig.delay!
if (retryConfig.timing === 'exponential') {
delay = delay * Math.pow(2, config.retryCount - 1)
}
if (retryConfig.onRetry) {
retryConfig.onRetry(error, config.retryCount, delay)
if (
this.retryConfig &&
this.retryConfig.onRetry &&
retryConfig.onRetry !== this.retryConfig.onRetry
) {
this.retryConfig.onRetry(error, config.retryCount, delay)
}
}
await sleep(delay)
return this.request<R, D>(config)
}
/**
* 处理错误
*/
private handleError(error: unknown, config: RequestConfig) {
return error
}
/**
* 取消请求
*/
cancel(id: string, reason = 'canceled') {
this.queue.remove(id, reason)
}
/**
* 取消所有请求
*/
cancelAll(reason = 'canceled') {
this.queue.clear(reason)
}
/**
* 发起请求
*/
async request<R extends BaseResponse = BaseResponse, D = any>(
config: RequestConfig<D>,
): Promise<R> {
const mergedConfig: RequestConfig = {
priority: RequestQueue.DEFAULT_PRIORITY,
handleResponse: true,
withToken: true,
...config,
}
if (mergedConfig.retry === void 0 && mergedConfig.retry === true) {
mergedConfig.retry = this.retryConfig
} else if (isObject(mergedConfig.retry)) {
mergedConfig.retry = {
...this.retryConfig,
...mergedConfig.retry,
}
}
const id = this.generateRequestID(mergedConfig)
const cache = this.cacher.get(id)
// 有缓存,直接返回
if (cache) return cache
const source = axios.CancelToken.source()
mergedConfig.cancelToken = source.token
const task = () => this.axiosInstance.request<any, R, D>(mergedConfig)
try {
const response = await this.queue.add(
id,
mergedConfig,
task,
source.cancel,
)
// 缓存
this.cacher.cache(id, mergedConfig, response)
return response
} catch (error) {
if (this.shouldRetry(error)) {
return this.requestRetry(mergedConfig, error)
}
throw this.handleError(error, mergedConfig)
}
}
/**
* 发起 GET 请求
*/
get<R extends BaseResponse = BaseResponse, D = any>(
url: string,
config?: RequestConfig<D>,
) {
return this.request<R, D>({
url,
method: 'get',
...config,
})
}
/**
* 发起 POST 请求
*/
post<R extends BaseResponse = BaseResponse, D = any>(
url: string,
data?: D,
config?: RequestConfig<D>,
) {
return this.request<R, D>({
url,
method: 'post',
data,
...config,
})
}
/**
* 发起 PUT 请求
*/
put<R extends BaseResponse = BaseResponse, D = any>(
url: string,
data?: D,
config?: RequestConfig<D>,
) {
return this.request<R, D>({
url,
method: 'put',
data,
...config,
})
}
/**
* 发起 DELETE 请求
*/
delete<R extends BaseResponse = BaseResponse, D = any>(
url: string,
config?: RequestConfig<D>,
) {
return this.request<R, D>({
url,
method: 'delete',
...config,
})
}
/**
* 发起 PATCH 请求
*/
patch<R extends BaseResponse = BaseResponse, D = any>(
url: string,
data?: D,
config?: RequestConfig<D>,
) {
return this.request<R, D>({
url,
method: 'patch',
data,
...config,
})
}
/**
* 发起 HEAD 请求
*/
head<R extends BaseResponse = BaseResponse, D = any>(
url: string,
config?: RequestConfig<D>,
) {
return this.request<R, D>({
url,
method: 'head',
...config,
})
}
/**
* 发起 OPTIONS 请求
*/
options<R extends BaseResponse = BaseResponse, D = any>(
url: string,
config?: RequestConfig<D>,
) {
return this.request<R, D>({
url,
method: 'options',
...config,
})
}
}ts
import { CanceledError, Canceler } from 'axios'
import { RequestConfig, RequestQueueConfig } from './types'
export class RequestQueue {
static readonly DEFAULT_PRIORITY = 5
static readonly DEFAULT_MAX_CONCURRENT = 5
private readonly pending: TaskContext[] = []
private readonly concurrent: TaskContext[] = []
private readonly maxConcurrent: number
constructor(config: RequestQueueConfig = {}) {
this.maxConcurrent =
config.maxConcurrent ?? RequestQueue.DEFAULT_MAX_CONCURRENT
}
private process() {
if (this.pending.length === 0) return
// 计算可用并行量
const available = this.maxConcurrent - this.currentConcurrent
if (available <= 0) return
// 取出需要并行执行任务
const tasksToRun = this.pending.splice(0, available)
for (const context of tasksToRun) {
this.concurrent.push(context)
context.isRunning = true
context
.task()
.then(context.resolve)
.catch(context.reject)
.finally(() => {
context.isRunning = false
// 移除已结束任务
this.concurrent.splice(this.concurrent.indexOf(context), 1)
// 递归处理后续任务
this.process()
})
}
}
/**
* 查找任务
*
* @param id 任务id
*/
find(id: string): TaskContext | undefined {
for (const context of this.pending) {
if (context.id === id) {
return context
}
}
for (const context of this.concurrent) {
if (context.id === id) {
return context
}
}
}
/**
* 当前并发数
*/
get currentConcurrent(): number {
return this.concurrent.length
}
/**
* 添加任务
*
* @param id 任务id
* @param priority 任务优先级
* @param task 任务
* @param cancel 任务取消函数
*/
add<T = any>(
id: string,
config: RequestConfig,
task: () => Promise<T>,
cancel: Canceler,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const context = new TaskContext(
id,
config,
task,
resolve,
reject,
cancel,
)
// 处理重复请求
switch (config.preventDuplicate) {
case 'cancel-new':
{
if (this.find(id)) {
context.reject(new CanceledError('task duplicate'))
}
}
break
case 'cancel-old':
{
const old = this.find(id)
if (old) {
if (old.isRunning) {
old.cancel('task canceled')
this.concurrent.splice(this.concurrent.indexOf(old), 1)
} else {
old.reject(new CanceledError('task duplicate'))
this.pending.splice(this.pending.indexOf(old), 1)
}
}
}
break
case 'link':
{
const old = this.find(id)
if (old) {
old.link(context)
this.process()
return
}
}
break
}
if (this.pending.length > 0) {
let index = this.pending.length
for (let i = this.pending.length - 1; i > 0; i--) {
if (this.pending[i].priority >= context.priority) {
break
}
index = i
}
this.pending.splice(index, 0, context)
} else {
this.pending.push(context)
}
this.process()
})
}
/**
* 移除任务
*
* @param id 任务id
* @returns 移除的数量
*/
remove(id: string, reason = 'task removed') {
const indexes: number[] = []
for (let i = 0; i < this.pending.length; i++) {
const context = this.pending[i]
if (context.id === id) {
// 倒序插入,以免 splice 删除的索引位置错误
indexes.unshift(i)
context.reject(new CanceledError(reason))
}
}
for (const index of indexes) {
this.pending.splice(index, 1)
}
return indexes.length
}
/**
* 清空队列
*/
clear(reason = 'task removed') {
for (const context of this.pending) {
context.reject(new CanceledError(reason))
}
const count = this.pending.length
this.pending.length = 0
return count
}
}
export class TaskContext<T = any> {
readonly id: string
readonly config: RequestConfig
readonly priority: number
readonly task: () => Promise<T>
readonly cancel: Canceler
readonly links: TaskContext[] = []
/**
* 是否正在运行
*/
isRunning = false
/**
* 是否已经完成
*/
private isFinished = false
private readonly _reject: (reason?: any) => void
private readonly _resolve: (value: T) => void
constructor(
id: string,
config: RequestConfig,
task: () => Promise<T>,
resolve: (value: T) => void,
reject: (reason?: any) => void,
cancel: Canceler,
) {
this.id = id
this.config = config
this.priority = config.priority ?? RequestQueue.DEFAULT_PRIORITY
this.task = task
this.cancel = cancel
this._resolve = resolve
this._reject = reject
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
}
resolve(value: T) {
if (this.isFinished) return
this._resolve(value)
if (this.links && this.links.length > 0) {
// 有链接的任务
for (const link of this.links) {
link.resolve(value)
}
}
this.links.length = 0
this.isFinished = true
}
reject(reason?: any) {
if (this.isFinished) return
this._reject(reason)
if (this.links && this.links.length > 0) {
// 有链接的任务
for (const link of this.links) {
link.reject(reason)
}
}
this.links.length = 0
this.isFinished = true
}
link(content: TaskContext) {
this.links.push(content)
}
}ts
import {
LocalStorage,
MemoryStorage,
RequestCacheStorage,
SessionStorage,
} from './storage'
import { RequestCacheConfig, RequestConfig } from './types'
export class RequestCacher {
readonly storage: RequestCacheStorage
constructor(config: RequestCacheConfig = {}) {
const { storage, expireTime = 5 * 60 * 1000 } = config
switch (storage) {
case 'memory':
this.storage = new MemoryStorage({ expireTime })
break
case 'session':
this.storage = new SessionStorage({ expireTime })
break
case 'local':
this.storage = new LocalStorage({ expireTime })
break
default:
this.storage = new MemoryStorage({ expireTime })
}
}
get(id: string) {
return this.storage.get(id)
}
remove(id: string) {
this.storage.remove(id)
}
set(id: string, value: any) {
this.storage.set(id, value)
}
clear() {
this.storage.clear()
}
cache(id: string, config: RequestConfig, response: any) {
if (config.cache) {
this.set(id, response)
}
}
}ts
import { deepClone } from './utils'
export interface RequestCacheStorageConfig {
expireTime: number
}
export interface RequestCacheStorageItem {
value: any
expireTime: number
}
export interface RequestCacheStorage {
readonly expireTime: number
get(key: string): any
set(key: string, value: any): void
remove(key: string): void
clear(): void
}
export class MemoryStorage implements RequestCacheStorage {
readonly expireTime: number
private storage: Record<string, RequestCacheStorageItem> = {}
constructor(config: RequestCacheStorageConfig) {
this.expireTime = config.expireTime
}
get(key: string) {
const item = this.storage[key]
if (!item) return null
if (Date.now() > item?.expireTime) {
this.remove(key)
return null
}
return deepClone(item.value)
}
set(key: string, value: any) {
this.storage[key] = {
value: deepClone(value),
expireTime: Date.now() + this.expireTime,
}
}
remove(key: string) {
delete this.storage[key]
}
clear(): void {
this.storage = {}
}
}
export class SessionStorage implements RequestCacheStorage {
readonly expireTime: number
constructor(config: RequestCacheStorageConfig) {
this.expireTime = config.expireTime
}
get(key: string): any {
try {
const itemStr = sessionStorage.getItem(key)
if (!itemStr) return null
const item: RequestCacheStorageItem = JSON.parse(itemStr)
if (Date.now() > item.expireTime) {
this.remove(key)
return null
}
return item.value
} catch (e) {
return null
}
}
set(key: string, value: any): void {
const item: RequestCacheStorageItem = {
value,
expireTime: Date.now() + this.expireTime,
}
sessionStorage.setItem(key, JSON.stringify(item))
}
remove(key: string): void {
sessionStorage.removeItem(key)
}
clear(): void {
sessionStorage.clear()
}
}
export class LocalStorage implements RequestCacheStorage {
readonly expireTime: number
constructor(config: RequestCacheStorageConfig) {
this.expireTime = config.expireTime
}
get(key: string): any {
try {
const itemStr = localStorage.getItem(key)
if (!itemStr) return null
const item: RequestCacheStorageItem = JSON.parse(itemStr)
if (Date.now() > item.expireTime) {
this.remove(key)
return null
}
return item.value
} catch (e) {
return null
}
}
set(key: string, value: any): void {
const item: RequestCacheStorageItem = {
value,
expireTime: Date.now() + this.expireTime,
}
localStorage.setItem(key, JSON.stringify(item))
}
remove(key: string): void {
localStorage.removeItem(key)
}
clear(): void {
localStorage.clear()
}
}ts
export function isObject(
val: any,
): val is Record<string | number | symbol, any> {
return val !== null && typeof val === 'object'
}
export function isNumber(val: any): val is number {
return typeof val === 'number'
}
export function isFunction(val: any): val is Function {
return typeof val === 'function'
}
export function sleep(ms?: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
export function deepClone<T = any>(value: T): T {
// 如果是 null 或者不是对象,直接返回
if (value === null || typeof value !== 'object') {
return value
}
// 检查是否为二进制数据类型(ArrayBuffer, Blob等)
if (
value instanceof ArrayBuffer ||
(typeof Blob !== 'undefined' && value instanceof Blob)
) {
throw new Error('Binary data is not supported')
}
// 处理 Array 类型
if (Array.isArray(value)) {
const clonedArray: any[] = []
for (let i = 0; i < value.length; i++) {
clonedArray[i] = deepClone(value[i])
}
return clonedArray as any
}
// 处理普通对象
if (isObject(value)) {
const clonedObj: Record<string | number | symbol, any> = {}
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
clonedObj[key] = deepClone(value[key])
}
}
return clonedObj as any
}
// 其他情况直接返回原值
return value
}