背景
前端开发场景下总避不开网络接口请求,axios
作为目前最常用的前端接口请求库,其基于 Promise
风格的接口请求调用,以及拦截器,适配器等机制,都极大程度的降低了前端开发网络接口请求调用的成本。但在使用过程中依旧存在以下一些不足,需要用户根据实际场景进行继续处理:
- 请求重试
- 重复请求处理
- 请求缓存机制
- 请求优先级调度
- 错误检测和处理
除了上述实际功能的开发外,axios
的请求拦截器机制用起来也不是很方便,一些常用环境下适配器缺失,因此我们还需要:
- 基于中间件模式封装请求拦截处理
- 扩展小程序等场景下的适配器实现
本文将针对上述点,一次性完成对axios
所有常用的封装实现
采用面向对象封装核心流程
整体的代码实现将采用面向对象进行封装,将通过一个 Service
类完成各种能力的组织和管理。
我们先将一些基础的axios
配置作为全局配置,让Service
类进行管理和组织:
ts
interface ServiceConfig {
// 基础 axios 配置
baseURL?: string;
timeout?: number;
headers?: Record<string, string>;
}
创建 Service
类,管理 axios
实例,执行实际的接口请求调用,以及构建基础的中间件调用机制。接下来我们依次实现他们:
Service 类构建
ts
class Service {
private axiosInstance: AxiosInstance;
private config: ServiceConfig;
constructor(config: ServiceConfig = {}) {
this.config = this.mergeDefaultConfig(config);
// 初始化 axios 实例
this.axiosInstance = axios.create({
baseURL: this.config.baseURL,
timeout: this.config.timeout,
headers: this.config.headers,
});
}
mergeDefaultConfig(config: ServiceConfig): Required<ServiceConfig> {
return {
baseURL: config.baseURL || '',
timeout: config.timeout || 10000,
headers: config.headers || {},
}
}
}
中间件引擎
中间件引擎本质是使用一个队列来管理中间件的调用,每个中间件在被调用时,会给中间件提供一个 next
方法让中间件能够继续执行下一步,执行模式可参考 koa
框架的洋葱模型:
现在我们先来定义一下中间件的类型定义:
ts
export interface MiddlewareContext {
config: RequestConfig;
response?: AxiosResponse;
error?: any;
startTime: number;
}
// next 方法,提供给中间件方法继续执行下一步
export type MiddlewareNext = () => Promise<any>;
export interface MiddlewareFunction {
name: string;
handler: (context: MiddlewareContext, next: MiddlewareNext) => Promise<any>;
priority?: number; // 中间件执行优先级,数字越小越先执行
}
// 中间件的接口定义
export interface MiddlewareManager {
use(middleware: MiddlewareFunction): void; // 添加中间件
remove(name: string): void; // 移除中间件
execute(context: MiddlewareContext): Promise<any>; // 执行中间件
getMiddlewares(): MiddlewareFunction[]; // 获取中间件队列
}
现在我们来创建中间件执行引擎类:
ts
export class MiddlewareEngine implements MiddlewareManager {
private middlewares: MiddlewareFunction[] = [];
use(middleware: MiddlewareFunction): void {
// 检查是否已存在同名中间件
const existingIndex = this.middlewares.findIndex(m => m.name === middleware.name);
if (existingIndex !== -1) {
this.middlewares[existingIndex] = middleware;
} else {
this.middlewares.push(middleware);
}
// 按优先级排序(数字越小优先级越高)
this.middlewares.sort((a, b) => (a.priority || 100) - (b.priority || 100));
}
remove(name: string): void {
const index = this.middlewares.findIndex(m => m.name === name);
if (index !== -1) {
this.middlewares.splice(index, 1);
}
}
getMiddlewares(): MiddlewareFunction[] {
return [...this.middlewares];
}
// 中间件核心执行逻辑
async execute(context: MiddlewareContext): Promise<any> {
// 过滤掉被跳过的中间件
const skipMiddlewares = context.config.middleware?.skip || [];
const activeMiddlewares = this.middlewares.filter(
m => !skipMiddlewares.includes(m.name)
);
if (activeMiddlewares.length === 0) {
// 如果没有中间件,直接执行请求
return this.executeRequest(context);
}
let index = 0;
const next: MiddlewareNext = async () => {
if (index >= activeMiddlewares.length) {
// 所有中间件都执行完毕,执行实际请求
return this.executeRequest(context);
}
const middleware = activeMiddlewares[index++];
return middleware.handler(context, next);
};
return next();
}
// 中间件最终执行的请求函数
private executeRequest: (context: MiddlewareContext) => Promise<any> = async () => {
throw new Error('Request executor not set');
};
setRequestExecutor(executor: (context: MiddlewareContext) => Promise<any>): void {
this.executeRequest = executor;
}
}
将中间件引擎注入 Service
类进行管理:
ts
class Service {
// ...
private middlewareEngine: MiddlewareEngine;
constructor(config: ServiceConfig = {}) {
// ...
this.middlewareEngine = new MiddlewareEngine();
this.middlewareEngine.setRequestExecutor(this.executeAxiosRequest.bind(this));
}
// axios 实际请求函数
private async executeAxiosRequest(context: MiddlewareContext): Promise<any> {
try {
const response: AxiosResponse = await this.axiosInstance.request(context.config);
context.response = response;
return response.data;
} catch (error) {
// Convert AbortError to CanceledError
if (axios.isCancel(error)) {
const canceledError = new Error(`Request ${requestId} was canceled`);
canceledError.name = 'CanceledError';
context.error = canceledError;
throw canceledError;
}
context.error = error;
throw error;
}
}
// 借助中间件引擎执行请求
private async executeRequest(config: RequestConfig): Promise<any> {
const context: MiddlewareContext = {
config,
startTime: Date.now(),
};
return this.middlewareEngine.execute(context);
}
// 中间件管理
use(middleware: MiddlewareFunction): void {
this.middlewareEngine.use(middleware);
}
removeMiddleware(name: string): void {
this.middlewareEngine.remove(name);
}
}
请求重试
在当前中间件模式下,我们将借助中间件机制来添加请求重试的能力
我们先给全局 ServiceConfig
和 RequestConfig
类型添加类型重试相关的配置定义
ts
export interface RetryConfig {
times: number; // 重试次数
delay?: number; // 每一次重试的延时
backoff?: 'linear' | 'exponential'; // 重试延时时间曲线
condition?: (error: any) => boolean; // 根据实际错误判断是否进行重试
onRetry?: (error: any, retryCount: number) => void; // 触发重试回调事件
}
export interface ServiceConfig {
// ...
// 默认重试配置
defaultRetry?: RetryConfig;
}
export interface RequestConfig extends AxiosRequestConfig {
retry?: RetryConfig;
}
请求开始时,对请求参数 config
和全局参数合并,同时为未定义参数添加默认值;这样在实际请求过程中,每个中间件的处理过程都会有必要的参数定义.
ts
class Service {
// ...
private mergeRequestConfig(config: RequestConfig): RequestConfig & { requestId?: string } {
return {
...config,
requestId: config.requestId, // 只使用用户提供的 requestId
retry: config.retry || this.config.defaultRetry,
};
}
}
现在我们来创建重试中间件,该中间件主要执行以下操作
- 检查重试参数
- 发起首次请求
- 请求成功直接返回
- 请求失败,根据条件判断是否重试 - 发起重试调用
ts
export const createRetryMiddleware = (defaultConfig?: Partial<RetryConfig>): MiddlewareFunction => ({
name: 'retry',
priority: 90,
handler: async (context: MiddlewareContext, next: MiddlewareNext) => {
const retryConfig = context.config.retry;
// 检查重试参数
if (!retryConfig || retryConfig.times <= 0) {
return next();
}
// 组装重试参数
const config: Required<RetryConfig> = {
times: retryConfig.times,
delay: retryConfig.delay ?? defaultConfig?.delay ?? 1000,
backoff: retryConfig.backoff ?? defaultConfig?.backoff ?? 'exponential',
condition: retryConfig.condition ?? defaultConfig?.condition ?? isRetryableError,
onRetry: retryConfig.onRetry ?? defaultConfig?.onRetry,
};
let lastError: any;
const retryAttempt = async (attempt: number): Promise<any> => {
try {
// 首次尝试不等待
if (attempt > 0) {
// 根据重试次数和延时,以及延时曲线计算重试等待时间
const delay = calculateDelay(config.delay, attempt - 1, config.backoff);
if (delay > 0) {
await sleep(delay);
}
config.onRetry?.(lastError, attempt);
}
context.retryCount = attempt;
return await next();
} catch (error) {
lastError = error;
// 判断是否继续执行重试: 需要通过自定义的condition判断逻辑,且重试次数小于指定次数
if (!config.condition(error) || attempt >= config.times) {
throw error;
}
return retryAttempt(attempt + 1);
}
};
return retryAttempt(0);
},
});
function isRetryableError(error: any): boolean {
// 网络错误
if (error.code === 'NETWORK_ERROR' || error.code === 'ECONNABORTED') {
return true;
}
// 超时错误
if (error.code === 'ECONNRESET' || error.message?.includes('timeout')) {
return true;
}
// HTTP 状态码错误
if (error.response?.status) {
const status = error.response.status;
// 5xx 服务器错误和部分 4xx 错误可以重试
return status >= 500 || status === 408 || status === 429;
}
return false;
}
function calculateDelay(baseDelay: number, attempt: number, backoff: 'linear' | 'exponential'): number {
let delay: number;
switch (backoff) {
case 'linear':
delay = baseDelay * (attempt + 1);
break;
case 'exponential':
delay = baseDelay * Math.pow(2, attempt);
break;
default:
delay = baseDelay;
}
return Math.min(delay, 30000); // 设置最大延迟时间为30秒
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
现在我们需要将中间件注入到中间件引擎中,这种内置的主要中间件直接以默认中间件的形式注入,用户通过config
配置参数进行触发
ts
class Service {
// 添加默认中间件
private setupDefaultMiddlewares(): void {
this.use(createRetryMiddleware());
}
}
请求数据缓存
对于重复的接口请求,在短时间内重复请求的数据实际是相同,对于这种请求可以通过一次前端缓存来进行避免,降低接口压力,也减少重复请求的时间.
对于数据的缓存,我们将考虑几个因素:
- 数据缓存的时间
- 缓存区域的大小
- 数据清理逻辑
针对上述三个问题,我们将封装一个 MemoryLRUCache
的类来进行缓存的管理,看名字可以知道,这里将使用LRU 算法来进行缓存的清理逻辑,对于缓存大小,次数通过缓存的数据量进行指定(当然也可以使用缓存的数据大小);
ts
export class MemoryLRUCache<K, V> implements LRUCache<K, V> {
private cache = new Map<K, V>();
private accessOrder = new Map<K, number>();
private accessCounter = 0;
constructor(public readonly maxSize: number = 100) {}
get(key: K): V | undefined {
const value = this.cache.get(key);
if (value !== undefined) {
// 记录每个缓存 key 的使用次数
this.accessOrder.set(key, ++this.accessCounter);
}
return value;
}
set(key: K, value: V): void {
if (this.cache.has(key)) {
this.cache.set(key, value);
this.accessOrder.set(key, ++this.accessCounter);
return;
}
// 当缓存的数据量超出阀值时,执行一次清理
if (this.cache.size >= this.maxSize) {
this.evictLRU();
}
this.cache.set(key, value);
this.accessOrder.set(key, ++this.accessCounter);
}
delete(key: K): boolean {
this.accessOrder.delete(key);
return this.cache.delete(key);
}
clear(): void {
this.cache.clear();
this.accessOrder.clear();
this.accessCounter = 0;
}
has(key: K): boolean {
return this.cache.has(key);
}
get size(): number {
return this.cache.size;
}
private evictLRU(): void {
let lruKey: K | undefined;
let lruAccess = Infinity;
for (const [key, access] of this.accessOrder) {
if (access < lruAccess) {
lruAccess = access;
lruKey = key;
}
}
if (lruKey !== undefined) {
this.delete(lruKey);
}
}
keys(): K[] {
return Array.from(this.cache.keys());
}
values(): V[] {
return Array.from(this.cache.values());
}
entries(): [K, V][] {
return Array.from(this.cache.entries());
}
}
这里我们是将缓存的数据直接放在内存中,实际业务中,可以切换这里的缓存方案,比如存入 storage
或者自定义的数据库
接下来将创建一个缓存管理类 CacheManager
用例管理缓存数据的存入取出
ts
export class CacheManager {
private cache: LRUCache<string, CacheItem>;
private config: Required<CacheConfig>;
private stats: CacheStats;
constructor(config: CacheConfig = {}) {
this.config = {
ttl: config.ttl ?? 5 * 60 * 1000, // 5分钟
maxSize: config.maxSize ?? 100,
};
this.cache = new MemoryLRUCache<string, CacheItem>(this.config.maxSize);
// 记录缓存的状态数据
this.stats = {
size: 0,
maxSize: this.config.maxSize,
hitCount: 0,
missCount: 0,
hitRate: 0,
keys: []
};
}
get(key: string): Promise<any | null> {
const item = this.cache.get(key);
// 从缓存中获取数据失败,记录为一次miss
if (!item) {
this.stats.missCount++;
this.updateHitRate();
return null;
}
// 出现数据超时,从缓存中清除数据
const now = Date.now();
if (now - item.timestamp > item.ttl) {
this.cache.delete(key);
this.stats.missCount++;
this.updateHitRate();
this.stats.size = this.cache.size;
return null;
}
item.accessCount++;
item.lastAccessed = now;
this.stats.hitCount++;
this.updateHitRate();
return item.data;
}
set(key: string, data: any, ttl?: number): Promise<void> {
const now = Date.now();
const item: CacheItem = {
data,
timestamp: now,
ttl: ttl ?? this.config.ttl,
accessCount: 1,
lastAccessed: now,
};
this.cache.set(key, item);
this.stats.size = this.cache.size;
}
delete(key: string): Promise<void> {
this.cache.delete(key);
this.stats.size = this.cache.size;
}
clear() {
this.cache.clear();
this.stats.size = 0;
}
getStats(): CacheStats {
return {
...this.stats,
size: this.cache.size,
maxSize: this.config.maxSize,
keys: this.cache.keys(),
};
}
updateConfig(config: Partial<CacheConfig>): void {
const oldConfig = { ...this.config };
this.config = { ...this.config, ...config };
// 更新缓存大小
this.cache.maxSize = config.maxSize ?? this.config.maxSize;
// 如果 TTL 改变,更新所有现有缓存项的 TTL
if (config.ttl && config.ttl !== oldConfig.ttl) {
const entries = this.cache.entries();
for (const [key, item] of entries) {
const remainingTime = item.ttl - (Date.now() - item.timestamp);
if (remainingTime > 0) {
item.ttl = config.ttl;
this.cache.set(key, item);
} else {
this.cache.delete(key);
}
}
this.stats.size = this.cache.size;
}
}
private updateHitRate(): void {
const total = this.stats.hitCount + this.stats.missCount;
this.stats.hitRate = total > 0 ? this.stats.hitCount / total : 0;
}
}
现在我们通过一个中间件来实现请求数据的缓存逻辑,这里我们将实现一个 generateRequestId
方法,该方法用户针对请求数据生成一个唯一ID作为存储的key值,这里将通过请求数据中的 url
, method
,params
,data
数据序列化构成一个字符串,然后对该字符串进行base64计算得出:
ts
/**
* 生成请求的唯一标识符
*/
export function generateRequestId(config: RequestConfig): string {
const { method = 'GET', url = '', params = {}, data = {} } = config;
const key = `${method.toUpperCase()}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`;
return base64Encode(key).replace(/[+/=]/g, '') + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
现在开始来创建缓存中间件:
ts
export const createCacheMiddleware = (cacheManager: CacheManager): MiddlewareFunction => ({
name: 'cache',
priority: 10,
handler: async (context: MiddlewareContext, next: MiddlewareNext) => {
const cacheConfig = context.config.cache;
// 如果没有启用缓存,直接执行下一个中间件
if (!cacheConfig) {
return next();
}
// 只缓存 GET 请求,一般情况下GET请求主要用户数据请求
if (context.config.method?.toUpperCase() !== 'GET') {
return next();
}
const cacheKey = generateRequestId(context.config);
const cachedData = cacheManager.get(cacheKey);
if (cachedData !== null) {
context.fromCache = true;
console.debug('[Axion Cache] 缓存命中:', cacheKey);
return cachedData;
}
console.debug('[Axion Cache] 缓存未命中:', cacheKey);
// 执行请求
const result = await next();
// 缓存成功的响应
const status = context.response?.status;
const isSuccessful = status && status >= 200 && status < 300;
if (isSuccessful && result !== undefined) {
const ttl = typeof cacheConfig === 'object' ? cacheConfig.ttl : undefined;
await cacheManager.set(cacheKey, result, ttl);
}
return result;
},
});
现在将缓存管理类注入到Service
类中进行管理,并将缓存中间件注入,实现接口请求的缓存能力
ts
class Service {
private cacheManager: CacheManager;
constructor(config: ServiceConfig = {}) {
// ...
this.cacheManager = new CacheManager(this.config.cache);
}
private setupDefaultMiddlewares(): void {
// ...
this.use(createCacheMiddleware(this.cacheManager));
}
}
请求防抖
请求防抖主要针对短时间连续多次的重复请求,针对性的进行特定的处理:
- 重复请求复用当前处理中的请求结果
- 请求锁,重复请求直接抛出错误 (但是为了程序的正确性,我们这里忽略这种场景)
- 请求防抖,按照防抖机制,短时间内只处理指定最后一次请求,前面的请求被忽略
现在我们针对上述三种场景中的两种进行程序实现,在此之前,我们也先创建一个 RequestLock
的类来管理重复的请求,该类主要实现:
- 注册一个请求到缓存
- 检查是否存在相同的请求(根据requestId,缺省的情况下按照上述
generateRequestId
进行生成) - 请求防抖处理
- 取消防抖处理的请求
ts
export interface PendingRequest {
promise: Promise<any>;
abortController: AbortController;
}
export class RequestLockManager {
private pendingRequests = new Map<string, PendingRequest>();
private debounceMap = new Map<string, number>();
private debounceDelay: number;
constructor(debounceDelay: number = 300) {
this.debounceDelay = debounceDelay;
}
/**
* 检查是否存在重复请求,如果存在则返回现有的 Promise
*/
checkDuplicateRequest(config: RequestConfig): Promise<any> | null {
if (!config.requestLock) {
return null;
}
const key = config.requestId || generateRequestId(config)
const pending = this.pendingRequests.get(key);
if (pending) {
return pending.promise;
}
return null;
}
/**
* 注册新的请求
*/
registerRequest(config: RequestConfig, promise: Promise<any>): Promise<any> {
const key = config.requestId || generateRequestId(config);
const abortController = new AbortController();
const pendingRequest: PendingRequest = {
promise,
abortController,
};
this.pendingRequests.set(key, pendingRequest);
// 请求完成后清理
promise.finally(() => {
this.pendingRequests.delete(key);
});
return promise;
}
/**
* 防抖处理
*/
async debounceRequest<T>(
config: RequestConfig,
executor: () => Promise<T>
): Promise<T> {
if (!config.debounce) {
return executor();
}
const key = config.requestId || generateRequestId(config);
return new Promise((resolve, reject) => {
// 清除之前的定时器
const existingTimer = this.debounceMap.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
// 设置新的定时器
const timer = setTimeout(async () => {
this.debounceMap.delete(key);
try {
const result = await executor();
resolve(result);
} catch (error) {
reject(error);
}
}, this.debounceDelay);
this.debounceMap.set(key, timer);
});
}
/**
* 取消指定请求
*/
cancelRequest(requestId: string): boolean {
for (const [key, pending] of this.pendingRequests) {
if (key.includes(requestId)) {
pending.abortController.abort();
this.pendingRequests.delete(key);
return true;
}
}
return false;
}
/**
* 取消所有请求
*/
cancelAllRequests(): void {
for (const [_, pending] of this.pendingRequests) {
pending.abortController.abort();
}
this.pendingRequests.clear();
// 清除所有防抖定时器
for (const timer of this.debounceMap.values()) {
clearTimeout(timer);
}
this.debounceMap.clear();
}
/**
* 更新防抖延迟时间
*/
setDebounceDelay(delay: number): void {
this.debounceDelay = delay;
}
}
现在我们将上面的请求防抖的管理类添加到 Service
类中,因为这里请求防抖主要是在请求开始前,对重复请求进行处理和拦截,所以我们就不使用中间件了,直接在请求方法中进行逻辑判断处理
ts
class Service {
//...
private requestLockManager: RequestLockManager;
constructor(config: ServiceConfig = {}) {
// ...
this.requestLockManager = new RequestLockManager();
}
// 请求函数,接口请求将从这里开始
async request<T = any>(config: RequestConfig): Promise<T> {
const mergedConfig = this.mergeRequestConfig(config);
// 检查重复请求 - 直接复用当前未完成的请求
if (mergedConfig.requestLock) {
const duplicatePromise = this.requestLockManager.checkDuplicateRequest(mergedConfig);
if (duplicatePromise) {
return duplicatePromise;
}
// 注册一个新的 Promise
return this.requestLockManager.registerRequest(mergedConfig, new Promise((resolve, reject) => {
this.processRequest(mergedConfig).then(resolve).catch(reject);
}));
}
// 防抖处理
if (mergedConfig.debounce) {
return this.requestLockManager.debounceRequest(mergedConfig, () =>
this.processRequest(mergedConfig)
);
}
return this.processRequest(mergedConfig);
}
private async processRequest<T>(config: RequestConfig): Promise<T> {
return this.executeRequest(config);
}
}
请求调度
关于请求调度我们首先说明一下为什么需要请求调度?
浏览器对于同一域名同时发起的HTTP请求数量有限制,这被称为"并发限制"或"最大并发连接数"。不同浏览器对并发限制的设定有所差异,但通常在6个左右. 当超过这个限制时,额外的请求将被排队,等待连接释放.
在这种场景下就存在一个并发优先级问题: 当一个应用中存在大量的接口请求时,由于最大并发数限制,可能导致一些高优先级的请求任务无法及时被发出,进而影响应用的使用体验。(当然这种场景可能比较少)
因此我们可以通过自定义调度机制,控制并发请求数,同时通过优先级队列,在必要时将高优请求插入队列并及时发出,提高高优任务的处理时效性,这就是我们封装请求调度的原因。
我们将封装一个 RequestQueue
的类来完成请求调度工作,通过控制请求的并发数和优先级队列,完成请求调度任务,该类主要完成以下几个工作:
- 维护一个优先级队列,并实现请求的入队出队
- 管理当前进行中的请求,请求的取消等
- 发起请求任务,按照优先级进行请求调度
我们先来定义一下请求队列的每个原子请求数据类型定义:
ts
export interface RequestTask {
id: string; // 请求ID,实际为 requestId
config: RequestConfig; // 请求配置
priority: number; // 请求优先级
timestamp: number; // 请求发起的时间戳
resolve: (value: any) => void;
reject: (reason: any) => void;
controller: AbortController;
}
现在我们按照上述三点对 RequestQueue
类进行实现:
ts
export class RequestQueue {
private queue: RequestTask[] = [];
private running: Map<string, RequestTask> = new Map();
private maxConcurrent: number;
private maxQueueSize: number;
constructor(maxConcurrent: number = 6, maxQueueSize: number = 100) {
this.maxConcurrent = maxConcurrent;
this.maxQueueSize = maxQueueSize;
}
async add(config: RequestConfig): Promise<any> {
return new Promise((resolve, reject) => {
if (this.queue.length >= this.maxQueueSize) {
reject(new Error('Request queue is full'));
return;
}
const controller = new AbortController();
const task: RequestTask = {
id: config.requestId || generateRequestId(config),
config: {
...config,
signal: controller.signal,
},
priority: config.priority || 5,
timestamp: Date.now(),
resolve,
reject,
controller,
};
this.enqueue(task);
this.processQueue();
});
}
cancelAll(): void {
// 取消队列中的所有请求
this.queue.forEach(task => {
task.controller.abort();
task.reject(new CanceledError());
});
this.queue = [];
// 取消正在执行的所有请求
this.running.forEach(task => {
task.controller.abort();
task.reject(new CanceledError());
});
this.running.clear();
}
cancel(requestId: string): boolean {
// 取消队列中的请求
const queueIndex = this.queue.findIndex(task => task.id === requestId);
if (queueIndex !== -1) {
const task = this.queue[queueIndex];
this.queue.splice(queueIndex, 1);
task.controller.abort();
task.reject(new CanceledError());
return true;
}
// 取消正在执行的请求
const runningTask = this.running.get(requestId);
if (runningTask) {
runningTask.controller.abort();
runningTask.reject(new CanceledError());
this.running.delete(requestId);
this.processQueue(); // 处理下一个请求
return true;
}
return false;
}
getStats() {
return {
pending: this.queue.length,
running: this.running.size,
maxConcurrent: this.maxConcurrent,
maxQueueSize: this.maxQueueSize,
};
}
updateConfig(config: { maxConcurrent?: number, maxQueueSize?: number }): void {
const { maxConcurrent, maxQueueSize } = config;
if (maxConcurrent !== undefined) {
this.maxConcurrent = maxConcurrent;
}
if (maxQueueSize !== undefined) {
this.maxQueueSize = maxQueueSize;
}
this.processQueue();
}
private enqueue(task: RequestTask): void {
// 按优先级插入队列(优先级高的在前面)
let insertIndex = this.queue.length;
for (let i = 0; i < this.queue.length; i++) {
if (this.queue[i].priority < task.priority) {
insertIndex = i;
break;
}
}
this.queue.splice(insertIndex, 0, task);
}
private async processQueue(): Promise<void> {
if (this.maxConcurrent <= 0) return;
// 添加延迟以确保所有请求都有机会进入队列
await new Promise(resolve => setTimeout(resolve, 0));
while (this.running.size < this.maxConcurrent && this.queue.length > 0) {
// 每次都从队列中选择优先级最高的请求 -- 队首任务
const task = this.queue.shift()!;
this.running.set(task.id, task);
this.executeTask(task).finally(() => {
this.running.delete(task.id);
this.processQueue(); // 继续处理队列
});
}
}
private async executeTask(task: RequestTask): Promise<void> {
try {
// 这里需要实际的请求执行逻辑
// 在 Service 类中会注入实际的执行函数
const result = await this.executeRequest(task.config);
task.resolve(result);
} catch (error) {
task.reject(error);
}
}
private executeRequest: (config: RequestConfig) => Promise<any> = async () => {
throw new Error('Request executor not set');
};
setRequestExecutor<T = any>(executor: (config: RequestConfig) => Promise<T>): void {
this.executeRequest = executor;
}
}
现在我们需要在 Service
类中加入请求调度器,因为请求调度的场景在大多数场景下是不需要要的,所以这里我们默认不开启;(如果出于包体积的考虑,这里甚至可以通过参数注入的形式进行按需注入,也能够让不需要调度的场景在打包时能够shaking掉相关的逻辑)
ts
class Service {
private requestQueue?: RequestQueue;
constructor(config: ServiceConfig = {}) {
// ...
// 在开启调度的情况下才初始化请求调度队列
if (config.enableSchedule) {
this.requestQueue = new RequestQueue(
this.config.maxConcurrentRequests,
this.config.maxQueueSize
);
this.requestQueue.setRequestExecutor(this.executeRequest.bind(this));
}
}
// 发起请求时,根据请求队列是否存在,分别进行不同的处理
private async processRequest<T>(config: RequestConfig): Promise<T> {
if (this.requestQueue) {
// 使用请求队列处理
return this.requestQueue.add(config);
}
return this.executeRequest(config);
}
}
统一错误检测和处理
在实际的接口请求中,除了基础的网络错误或者Axios自带的请求错误场景外,在实际业务开发中,更多可能是通过响应体中的 errcode
等自定义的错误状态字段来判断是否出现请求错误的,这时业务就需要根据自己的场景来判断是否出现了请求错误,并控制触发重试等。以及在请求出现错误时,全局统一的进行接口请求的错误上报。
那么这里我们就提供一个自定义错误判断的参数给用户进行配置,同时通过中间件来进行错误分析和抛出:
我们先来定义一下错误校验函数的类型定义:
ts
export interface RequestConfig extends AxiosRequestConfig {
// ...
// 自定义错误验证
validateError?: (response: AxiosResponse) => boolean | Error;
}
export interface ServiceConfig {
// ...
// 全局错误验证函数
globalValidateError?: (response: AxiosResponse) => boolean | Error;
}
实际请求中,将优先使用请求配置中的错误校验函数进行校验,缺省时使用全局的校验函数
ts
export interface ErrorHandlerConfig {
logErrors?: boolean;
}
export const createErrorHandlerMiddleware = (config?: ErrorHandlerConfig): MiddlewareFunction => ({
name: 'errorHandler',
priority: 100,
handler: async (context: MiddlewareContext, next: MiddlewareNext) => {
try {
const result = await next();
// 检查自定义错误验证
if (context.response && typeof context.config.validateError === 'function') {
const validationResult = context.config.validateError(context.response);
if (validationResult === true || (validationResult && typeof validationResult === 'object')) {
const errorMessage = typeof validationResult === 'object' && validationResult.message
? validationResult.message
: 'Custom validation failed';
throw new CustomValidationError(errorMessage, context.response);
}
}
return result;
} catch (error) {
context.error = error;
// 记录错误
if (config?.logErrors !== false) {
logError(error, context);
}
// 包装错误以提供更多信息
throw wrapError(error, context);
}
},
});
将错误处理的中间件添加到Service
类中:
ts
class Service {
private setupDefaultMiddlewares(): void {
// ...
this.use(createErrorHandlerMiddleware({
logErrors: true,
}));
}
}
统一管理请求取消
到目前为止,我们实际的请求过程可能出现三种场景:
- 使用请求调度器,进入请求队列进行管理
- 请求锁,复用当前未完成的请求
- 直接发起请求
对于前两种场景,我们在实现过程中都添加了对应的请求取消的接口,现在我们针对直接请求的情况,通过一个Map来记录这些请求的控制器
ts
class Service {
// 请求管理
private abortControllers = new Map<string, AbortController>();
private async executeAxiosRequest(context: MiddlewareContext): Promise<any> {
const controller = new AbortController();
const requestId = context.config.requestId!;
this.abortControllers.set(requestId, controller);
try {
const response: AxiosResponse = await this.axiosInstance.request({
...context.config,
signal: controller.signal
});
// Cleanup after successful request
this.abortControllers.delete(requestId);
context.response = response;
return response.data;
} catch (error) {
context.error = error;
throw error;
} finally {
// Ensure cleanup
this.abortControllers.delete(requestId);
}
}}
}
添加取消接口请求的方法,这里我们需要分别取消 调度队列中的请求
,RequestLock中的请求
,abortControllers中管理的请求
ts
class Service {
cancelRequest(requestId: string): void {
// Cancel queue position
this.requestQueue?.cancel(requestId);
// Cancel active request
const controller = this.abortControllers.get(requestId);
if (controller) {
controller.abort();
this.abortControllers.delete(requestId);
}
// Cancel request lock
this.requestLockManager.cancelRequest(requestId);
}
cancelAllRequests(): void {
this.requestQueue?.cancelAll();
this.requestLockManager.cancelAllRequests();
const entries = this.abortControllers.entries();
for (const [_, controller] of entries) {
controller.abort();
}
}
}
适配器机制
axios本身是自带了适配器机制来支持多场景的适配的,这里我们直接使用axios自带的适配器机制,扩展一个小程序的适配器为例;
ts
export interface CustomAdapter {
(config: AxiosRequestConfig): Promise<AxiosResponse>;
name?: string;
platform?: string;
}
export const createMiniprogramAdapter = (): CustomAdapter => {
const adapter: CustomAdapter = async (config: AxiosRequestConfig): Promise<AxiosResponse> => {
return new Promise((resolve, reject) => {
// 检查是否在小程序环境
if (typeof globalThis !== 'undefined' && !(globalThis as any).wx) {
reject(new Error('Not in miniprogram environment'));
return;
}
const wx = (globalThis as any).wx;
const { url, method = 'GET', data, headers, timeout } = config;
wx.request({
url: url || '',
method: method.toUpperCase() as any,
data,
header: headers,
timeout,
success: (res: any) => {
const response: AxiosResponse = {
data: res.data,
status: res.statusCode,
statusText: res.statusCode === 200 ? 'OK' : 'Error',
headers: res.header,
config: config as InternalAxiosRequestConfig,
request: {},
};
resolve(response);
},
fail: (error: any) => {
reject(error);
},
});
});
};
adapter.name = 'miniprogram';
adapter.platform = 'miniprogram';
return adapter;
};
至此,我们对于 axios
请求库的各种封装就基本完成了,最后我们只需要基于 Service
类中的 request
方法扩展出各种请求方式调用即可;
本文项目代码以同步之 Github,感兴趣可以前往 axion 查看完整代码,如果对你有帮助,可以帮忙点个 ✨ 哦!