Axion: 一次搞定axios请求库的各种能力封装

背景

前端开发场景下总避不开网络接口请求,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);
  }
}

请求重试

在当前中间件模式下,我们将借助中间件机制来添加请求重试的能力

我们先给全局 ServiceConfigRequestConfig 类型添加类型重试相关的配置定义

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,
    };
  }
}

现在我们来创建重试中间件,该中间件主要执行以下操作

  1. 检查重试参数
  2. 发起首次请求
  3. 请求成功直接返回
  4. 请求失败,根据条件判断是否重试 - 发起重试调用
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值,这里将通过请求数据中的 urlmethodparamsdata 数据序列化构成一个字符串,然后对该字符串进行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 的类来管理重复的请求,该类主要实现:

  1. 注册一个请求到缓存
  2. 检查是否存在相同的请求(根据requestId,缺省的情况下按照上述 generateRequestId 进行生成)
  3. 请求防抖处理
  4. 取消防抖处理的请求
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 的类来完成请求调度工作,通过控制请求的并发数和优先级队列,完成请求调度任务,该类主要完成以下几个工作:

  1. 维护一个优先级队列,并实现请求的入队出队
  2. 管理当前进行中的请求,请求的取消等
  3. 发起请求任务,按照优先级进行请求调度

我们先来定义一下请求队列的每个原子请求数据类型定义:

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 查看完整代码,如果对你有帮助,可以帮忙点个 ✨ 哦!

相关推荐
小着1 小时前
vue项目页面最底部出现乱码
前端·javascript·vue.js·前端框架
lichenyang4534 小时前
React ajax中的跨域以及代理服务器
前端·react.js·ajax
呆呆的小草4 小时前
Cesium距离测量、角度测量、面积测量
开发语言·前端·javascript
一 乐5 小时前
民宿|基于java的民宿推荐系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·源码
testleaf5 小时前
前端面经整理【1】
前端·面试
好了来看下一题6 小时前
使用 React+Vite+Electron 搭建桌面应用
前端·react.js·electron
啃火龙果的兔子6 小时前
前端八股文-react篇
前端·react.js·前端框架
小前端大牛马6 小时前
react中hook和高阶组件的选型
前端·javascript·vue.js
刺客-Andy6 小时前
React第六十二节 Router中 createStaticRouter 的使用详解
前端·javascript·react.js
萌萌哒草头将军8 小时前
🚀🚀🚀VSCode 发布 1.101 版本,Copilot 更全能!
前端·vue.js·react.js