{T}

请求封装实践

封装一个通用的网络请求工具,提高开发效率,统一管理接口调用。

概述

在企业级前端项目中,网络请求是核心功能之一。合理的请求封装可以:

  • 统一管理:集中处理请求配置、拦截器、错误处理等
  • 提高效率:减少重复代码,提供一致的 API 调用方式
  • 便于维护:统一的错误处理和日志记录
  • 功能增强:添加缓存、重试、取消等高级功能

核心特性

特性说明优势
统一配置baseURL、timeout、headers 等避免重复配置
拦截器请求/响应拦截统一添加 token、处理响应
错误处理自定义错误类型友好的错误提示
请求缓存GET 请求缓存减少网络请求
请求重试失败自动重试提高请求成功率
请求取消AbortController避免无效请求

架构设计

code
┌─────────────────────────────────────────────────┐
│                 HttpClient                       │
├─────────────────────────────────────────────────┤
│  ┌──────────────┐        ┌──────────────────┐   │
│  │  配置管理     │        │   拦截器链        │   │
│  │  - baseURL   │        │  - 请求拦截       │   │
│  │  - timeout   │        │  - 响应拦截       │   │
│  │  - headers   │        └──────────────────┘   │
│  └──────────────┘                                │
│                                                  │
│  ┌──────────────────────────────────────────┐   │
│  │            核心请求方法                    │   │
│  │  request | get | post | put | delete      │   │
│  └──────────────────────────────────────────┘   │
│                                                  │
│  ┌──────────────┐  ┌──────────────┐            │
│  │  错误处理     │  │  高级功能     │            │
│  │  - HttpError │  │  - 缓存       │            │
│  │  - 超时处理   │  │  - 重试       │            │
│  └──────────────┘  └──────────────┘            │
└─────────────────────────────────────────────────┘

一、基础封装

1.1 配置参数说明

参数类型默认值说明
baseURLstring''请求基础路径,会自动拼接到所有请求 URL 前
timeoutnumber10000请求超时时间(毫秒)
headersobject{}默认请求头,所有请求都会携带

1.2 核心实现

javascript
class HttpClient {
  /**
   * 创建 HTTP 客户端实例
   * @param {Object} config - 配置选项
   * @param {string} [config.baseURL=''] - 基础 URL
   * @param {number} [config.timeout=10000] - 超时时间(ms)
   * @param {Object} [config.headers={}] - 默认请求头
   */
  constructor(config = {}) {
    this.baseURL = config.baseURL || '';
    this.timeout = config.timeout || 10000;
    this.headers = {
      'Content-Type': 'application/json',
      Accept: 'application/json',
      ...config.headers
    };
  }

  /**
   * 发起请求
   * @param {Object} config - 请求配置
   * @param {string} config.url - 请求地址
   * @param {string} [config.method='GET'] - 请求方法
   * @param {Object} [options={}] - 其他选项
   */
  delete(url, options = {}) {
    return this.request({ url, method: 'DELETE', ...options });
  }
}

1.3 使用示例

javascript
// 创建实例
const http = new HttpClient({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  headers: {
    'X-Custom-Header': 'value'
  }
});

// GET 请求 - 获取用户列表
const users = await http.get('/users', { page: 1, size: 10 });
// 实际请求: https://api.example.com/users?page=1&size=10

// POST 请求 - 创建用户
const newUser = await http.post('/users', {
  name: 'Alice',
  email: 'alice@example.com'
});

// DELETE 请求 - 删除用户
await http.delete('/users/123');

// 自定义请求头
const data = await http.get('/protected', null, {
  headers: {
    'Authorization': 'Bearer token123'
  }
});

二、拦截器机制

拦截器可以在请求发送前或响应返回后进行统一处理,是请求封装的核心功能。

2.1 拦截器工作流程

code
请求发起 → 请求拦截器 → 发送请求 → 接收响应 → 响应拦截器 → 返回数据
              ↓                         ↓
          添加 token                  统一处理错误
          修改配置                    转换数据格式

2.2 完整实现

javascript
class HttpClient {
  constructor(config = {}) {
    this.baseURL = config.baseURL || '';
    this.timeout = config.timeout || 10000;
    this.headers = config.headers || {};
    
    // 拦截器队列
    this.interceptors = {
      request: [],  // 请求拦截器数组
      response: [], // 响应拦截器数组
    };
  }

  // 注册请求拦截器
  useRequestInterceptor(onFulfilled, onRejected) {
    this.interceptors.request.push({ onFulfilled, onRejected });
    return this;
  }

  // 注册响应拦截器
  useResponseInterceptor(onFulfilled, onRejected) {
    this.interceptors.response.push({ onFulfilled, onRejected });
    return this;
  }

  // 核心请求方法
  async request({ url, method = 'GET', data, params, headers = {} }) {
    let config = {
      url: this.baseURL + url,
      method,
      data,
      params,
      headers: { ...this.headers, ...headers }
    };

    // 执行请求拦截器(可修改配置)
    for (const { onFulfilled } of this.interceptors.request) {
      config = await onFulfilled(config);
    }

    // 构建请求 URL
    let fullUrl = config.url;
    if (config.params) {
      const query = new URLSearchParams(config.params).toString();
      fullUrl += (fullUrl.includes('?') ? '&' : '?') + query;
    }

    // 发起请求
    const response = await fetch(fullUrl, {
      method: config.method,
      headers: config.headers,
      body: config.data ? JSON.stringify(config.data) : undefined
    });

    // 处理响应
    let result = response.ok ? await response.json() : null;

    // 执行响应拦截器
    for (const { onFulfilled } of this.interceptors.response) {
      result = await onFulfilled({ response, data: result });
    }

    return result;
  }

  post(url, data, options) {
    return this.request({ url, method: 'POST', data, ...options });
  }
}

2.3 拦截器应用场景

场景一:自动添加认证 Token

javascript
const http = new HttpClient({ baseURL: '/api' });

// 请求拦截器:自动添加 token
http.useRequestInterceptor(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers = {
        ...config.headers,
        Authorization: `Bearer ${token}`,
      };
    }
    return config;
  },
  (error) => {
    // 请求配置错误的处理
    console.error('请求配置错误:', error);
    return Promise.reject(error);
  }
);

场景二:统一错误处理

javascript
// 响应拦截器:统一处理响应
http.useResponseInterceptor(
  (response) => {
    // 业务状态码判断
    if (response.code !== 0) {
      throw new Error(response.message || '请求失败');
    }
    // 返回业务数据
    return response.data;
  },
  (error) => {
    // 统一错误提示
    if (error.message.includes('401')) {
      // Token 过期,跳转登录
      window.location.href = '/login';
    } else if (error.message.includes('超时')) {
      alert('网络请求超时,请稍后重试');
    } else {
      alert(error.message || '网络错误');
    }
    return Promise.reject(error);
  }
);

场景三:请求日志记录

javascript
// 请求拦截:记录请求开始
http.useRequestInterceptor((config) => {
  console.log(`[请求开始] ${config.method} ${config.url}`, config);
  config.startTime = Date.now();
  return config;
});

// 响应拦截:记录请求完成
http.useResponseInterceptor(
  (response) => {
    const duration = Date.now() - response.config.startTime;
    console.log(`[请求成功] 耗时 ${duration}ms`, response);
    return response;
  },
  (error) => {
    console.error('[请求失败]', error);
    return Promise.reject(error);
  }
);

2.4 完整使用示例

javascript
// 创建实例
const http = new HttpClient({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

// 添加请求拦截器
http.useRequestInterceptor((config) => {
  // 添加 token
  const token = localStorage.getItem('token');
  if (token) {
    config.headers = {
      ...config.headers,
      Authorization: `Bearer ${token}`
    };
  }
  return config;
});

try {
  const users = await http.get('/users', { page: 1 });
  console.log('用户列表:', users);
} catch (error) {
  console.error('获取失败:', error);
}

三、错误处理

3.1 错误类型说明

错误类型场景处理方式
HttpErrorHTTP 状态码错误根据状态码提示用户
TimeoutError请求超时提示超时或自动重试
NetworkError网络断开提示检查网络连接
BusinessError业务逻辑错误显示后端返回的错误信息

3.2 自定义错误类

javascript
/**
 * HTTP 错误类
 */
class HttpError extends Error {
  /**
   * @param {string} message - 错误消息
   * @param {number} status - HTTP 状态码
   * @param {Object} data - 响应数据
   */
  constructor(message, status, data = {}) {
    super(message);
    this.name = 'HttpError';
    this.status = status;
    this.data = data;
  }
}

/**
 * 业务错误类
 */
class BusinessError extends Error {
  constructor(message, code, data = {}) {
    super(message);
    this.name = 'BusinessError';
    this.code = code;
    this.data = data;
  }
}

3.3 统一错误处理

javascript
class HttpClient {
  // ... 其他代码

  /**
   * 发送请求(带完整错误处理)
   */
  async request(options) {
    try {
      const response = await this._fetch(options);
      return response;
    } catch (error) {
      // 转换为自定义错误
      if (error.name === 'AbortError') {
        throw new Error('请求超时');
      }
      if (error.response) {
        throw new HttpError(error.message, error.response.status, error.response.data);
      }
      throw new NetworkError(error.message);
    }
  }

  // 带超时控制的内部请求
  async _fetch(options) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      const response = await fetch(options.url, {
        ...options,
        signal: controller.signal
      });
      clearTimeout(timeoutId);
      return response;
    } catch (error) {
      clearTimeout(timeoutId);
      throw error;
    }
  }
}

3.4 错误处理最佳实践

javascript
// 创建 HTTP 实例
const http = new HttpClient({
  baseURL: '/api',
  timeout: 10000,
});

// 使用示例
async function fetchUserData(userId) {
  try {
    const data = await http.get(`/users/${userId}`);
    return data;
  } catch (error) {
    // 根据错误类型分别处理
    if (error instanceof HttpError) {
      if (error.status === 401) {
        // 未授权,跳转登录
        redirectToLogin();
      } else if (error.status === 404) {
        showToast('用户不存在');
      }
    } else if (error instanceof TimeoutError) {
      showToast('请求超时,请重试');
    } else {
      showToast('网络异常,请检查连接');
    }
    throw error;
  }
}

// 使用
fetchUserData(123)
  .then(data => console.log('用户数据:', data))
  .catch(() => console.log('获取用户数据失败'));

3.5 全局错误处理

javascript
// 添加响应拦截器统一处理错误
http.useResponseInterceptor(
  (response) => response,
  (error) => {
    // 记录错误日志
    logError(error);
    
    // 根据环境处理
    if (process.env.NODE_ENV === 'development') {
      console.error('[HTTP Error]', error);
    }
    
    // 用户提示
    if (error instanceof HttpError) {
      showErrorMessage(`请求失败(${error.status})`);
    }
    
    // 上报错误监控平台
    reportErrorToMonitor(error);
  }
);

// 错误上报函数
function reportErrorToMonitor(error) {
  // 使用 sendBeacon 发送错误信息
  navigator.sendBeacon('/api/errors', JSON.stringify({
    message: error.message,
    name: error.name,
    stack: error.stack,
    url: window.location.href,
    timestamp: Date.now(),
  }));
}

四、请求缓存

请求缓存可以减少重复的网络请求,提升应用性能和用户体验。

4.1 缓存策略

code
┌─────────────┐
│  发起请求   │
└──────┬──────┘
       │
       ▼
┌──────────────┐    是    ┌──────────┐
│ 缓存是否存在 │────────> │ 返回缓存  │
└──────┬───────┘          └──────────┘
       │ 否
       ▼
┌──────────────┐    是    ┌──────────────┐
│ 缓存是否过期 │────────> │ 重新请求更新 │
└──────┬───────┘          └──────────────┘
       │ 否
       ▼
┌──────────────┐
│  返回缓存数据 │
└──────────────┘

4.2 内存缓存实现

javascript
/**
 * 缓存项
 */
class CacheItem {
  constructor(data, ttl) {
    this.data = data;
    this.timestamp = Date.now();
    this.ttl = ttl; // 过期时间(毫秒)
  }

  // 检查是否过期
  isExpired(entry) {
    return Date.now() - entry.timestamp > entry.ttl;
  }

  // 读取缓存
  get(key) {
    const entry = this.cache.get(key);
    if (!entry) return null;
    if (this.isExpired(entry)) {
      this.cache.delete(key);
      return null;
    }
    return entry.data;
  }

  // 写入缓存
  set(key, data, ttl = this.cacheConfig.ttl) {
    // 超过容量时删除最旧缓存
    if (this.cache.size >= this.cacheConfig.maxSize) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    this.cache.set(key, { data, timestamp: Date.now(), ttl });
  }

  // 带缓存和超时的请求
  async _fetchWithCache(url, params, options) {
    const cacheKey = url + JSON.stringify(params || {});
    if (options.cache !== false) {
      const cached = this.get(cacheKey);
      if (cached) return cached;
    }

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      const response = await fetch(url, { signal: controller.signal });
      const data = await response.json();
      if (options.cache !== false) {
        this.set(cacheKey, data, options.cacheTTL);
      }
      clearTimeout(timeoutId);
      return data;
    } catch (error) {
      clearTimeout(timeoutId);
      throw error;
    }
  }
}

4.3 使用示例

javascript
// 创建带缓存的实例
const http = new HttpClientWithCache({
  baseURL: '/api',
  cacheConfig: {
    enabled: true,
    ttl: 3 * 60 * 1000, // 3分钟
    maxSize: 50,
  }
});

// 使用缓存(默认启用)
const users1 = await http.get('/users', { page: 1 });
// 发送请求并缓存

const users2 = await http.get('/users', { page: 1 });
// 从缓存返回(3分钟内)

// 禁用缓存
const freshData = await http.get('/users', { page: 1 }, { 
  cache: false 
});

// 自定义缓存时间
const longCache = await http.get('/config', null, { 
  cacheTTL: 30 * 60 * 1000 // 30分钟
});

// 手动清除缓存
http.clearCache('/users'); // 清除包含 '/users' 的缓存
http.clearCache();          // 清除所有缓存

4.4 高级缓存策略

javascript
/**
 * 支持多种缓存策略的 HTTP 客户端
 */
class AdvancedHttpClient extends HttpClientWithCache {
  constructor(config) {
    super(config);
    
    // 缓存策略
    this.cacheStrategy = {
      // 仅网络请求
      'network-only': async (url, params, options) => {
        return this.request({ url, method: 'GET', params, ...options });
      },
      // 优先缓存,未命中再请求
      'cache-first': async (url, params, options) => {
        const key = url + JSON.stringify(params || {});
        const cached = this.get(key);
        if (cached) return cached;
        const data = await this.request({ url, method: 'GET', params, ...options });
        this.set(key, data);
        return data;
      },
      // 先缓存,后台重新验证更新
      'stale-while-revalidate': async (url, params, options) => {
        const key = url + JSON.stringify(params || {});
        const cached = this.get(key);
        if (cached) {
          this.request({ url, method: 'GET', params, ...options })
            .then(fresh => this.set(key, fresh));
          return cached;
        }
        const data = await this.request({ url, method: 'GET', params, ...options });
        this.set(key, data);
        return data;
      },
      // 先网络,失败用缓存
      'network-first': async (url, params, options) => {
        try {
          const data = await this.request({ url, method: 'GET', params, ...options });
          this.set(url + JSON.stringify(params || {}), data);
          return data;
        } catch (error) {
          const key = url + JSON.stringify(params || {});
          const cached = this.get(key);
          if (cached) return cached;
          throw error;
        }
      }
    };
    
    this.getWithStrategy = async (strategy, url, params, options) => {
      const fn = this.cacheStrategy[strategy];
      if (!fn) throw new Error(`未知缓存策略: ${strategy}`);
      return fn(url, params, options);
    };
  }
}


// 先缓存,后台更新(适合实时性要求不高的场景)
const config = await http.getWithStrategy('stale-while-revalidate', '/config');

// 先网络,失败用缓存(适合离线优先的应用)
const data = await http.getWithStrategy('network-first', '/users', { page: 1 });

五、请求重试

网络请求可能因暂时性问题失败,自动重试机制可以提高请求成功率。

5.1 重试策略

code
第1次请求 ─失败─> 延迟1秒 ─> 第2次请求 ─失败─> 延迟2秒 ─> 第3次请求 ─失败─> 抛出错误
    │                                                    │
   成功                                                 成功
    │                                                    │
    ▼                                                    ▼
 返回结果                                            返回结果

5.2 基础重试实现

javascript
/**
 * 带重试的请求函数
 * @param {Function} requestFn - 请求函数
 * @param {Object} options - 重试选项
 * @returns {Promise}
 */
async function requestWithRetry(requestFn, options = {}) {
  const {
    retries = 3,           // 最大重试次数
    delay = 1000,          // 基础延迟时间
    backoff = 'linear',    // 退避策略:linear | exponential
    maxDelay = 30000,      // 最大延迟时间
    retryable = () => true // 哪些错误需要重试
  } = options;

  let lastError;

  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await requestFn();
    } catch (error) {
      lastError = error;
      // 是否需要重试
      if (attempt >= retries || !retryable(error)) break;

      // 计算延迟时间
      let delayTime;
      if (backoff === 'exponential') {
        delayTime = Math.min(delay * Math.pow(2, attempt), maxDelay);
      } else {
        delayTime = delay * (attempt + 1);
      }
      // 添加随机抖动,避免雪崩
      delayTime += Math.random() * 100;

      console.log(`请求失败,${delayTime}ms 后重试(第 ${attempt + 1} 次)`);
      await new Promise(resolve => setTimeout(resolve, delayTime));
    }
  }

  throw lastError;
}

5.3 集成到 HttpClient

javascript
class HttpClient {
  constructor(config = {}) {
    this.baseURL = config.baseURL || '';
    this.timeout = config.timeout || 10000;
    this.headers = config.headers || {};
    
    // 重试配置
    this.retryConfig = {
      enabled: true,
      retries: 3,
      delay: 1000,
      backoff: 'exponential',
      maxDelay: 30000
    };
  }

  // 带重试和超时的请求
  async request(url, options = {}) {
    const retry = options.retry ?? this.retryConfig;
    
    if (!retry.enabled || retry.retries === 0) {
      return this._fetch(url, options);
    }
    
    return requestWithRetry(() => this._fetch(url, options), {
      retries: retry.retries,
      delay: retry.delay,
      backoff: retry.backoff,
      maxDelay: retry.maxDelay
    });
  }

  // 带超时的基础请求
  async _fetch(url, options) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      const response = await fetch(url, { ...options, signal: controller.signal });
      if (!response.ok) {
        throw new HttpError(`HTTP ${response.status}`, response.status);
      }
      return await response.json();
    } catch (error) {
      clearTimeout(timeoutId);
      throw error;
    }
  }
}

5.4 使用示例

javascript
// 创建实例
const http = new HttpClient({
  baseURL: '/api',
  retry: {
    enabled: true,
    retries: 3,
    delay: 1000,
    backoff: 'exponential',
  }
});

// 自动重试(使用默认配置)
const retried = await http.get('/flaky-api', null, {});

// 自定义重试次数
const custom = await http.get('/important', null, {
  retry: { retries: 5, delay: 2000 }
});

// 禁用重试
const noRetry = await http.get('/critical', null, {
  retry: { enabled: false }
});

5.5 高级重试场景

javascript
/**
 * 熔断器模式
 * 当错误率达到阈值时,快速失败,避免无效请求
 */
class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000;
    this.failures = 0;
    this.state = 'CLOSED'; // CLOSED | OPEN | HALF_OPEN
    this.nextAttempt = Date.now();
  }

  // 是否允许发起请求
  allowRequest() {
    if (this.state === 'CLOSED') return true;
    if (this.state === 'OPEN') {
      // 达到重置时间,进入半开状态
      if (Date.now() >= this.nextAttempt) {
        this.state = 'HALF_OPEN';
        return true;
      }
      return false;  // 快速失败
    }
    // HALF_OPEN 状态
    return true;
  }

  // 执行请求(带熔断保护)
  async execute(fn) {
    if (!this.allowRequest()) {
      throw new Error('熔断器打开,请求被拒绝');
    }
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  // 成功:重置计数
  onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  // 失败:累积并判断是否打开熔断器
  onFailure() {
    this.failures++;
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
    }
  }
}

// 使用
const breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 60000 });
try {
  const data = await breaker.execute(() => http.get('/api/data'));
  console.log('请求成功:', data);
} catch (error) {
  console.error('请求失败:', error.message);
}

六、完整实现

6.1 生产级 HttpClient

javascript
/**
 * 自定义错误类
 */
class HttpError extends Error {
  constructor(message, status, data = {}) {
    super(message);
    this.name = 'HttpError';
    this.status = status;
    this.data = data;
    this.timestamp = Date.now();
  }
}

/**
 * 生产级 HTTP 客户端
 * 集成:拦截器、缓存、重试、超时、错误处理
 */
class HttpClient {
  constructor(config = {}) {
    this.baseURL = config.baseURL || '';
    this.timeout = config.timeout || 10000;
    this.headers = config.headers || {};
    this.retryConfig = config.retry || { enabled: false };
    this.interceptors = { request: [], response: [] };
    this.cache = new Map();
  }

  // 注册拦截器
  useRequestInterceptor(handler) {
    this.interceptors.request.push(handler);
    return this;
  }
  useResponseInterceptor(handler) {
    this.interceptors.response.push(handler);
    return this;
  }

  // 核心请求方法
  async request({ url, method = 'GET', data, params, headers = {}, options = {} }) {
    let config = {
      url: this.baseURL + url,
      method,
      data,
      params,
      headers: { ...this.headers, ...headers }
    };

    // 请求拦截器
    for (const handler of this.interceptors.request) {
      config = await handler(config) || config;
    }

    // 构建 URL
    let fullUrl = config.url;
    if (config.params) {
      const query = new URLSearchParams(config.params).toString();
      fullUrl += (fullUrl.includes('?') ? '&' : '?') + query;
    }

    // 发送请求(带重试)
    const fetchFn = () => fetch(fullUrl, {
      method: config.method,
      headers: config.headers,
      body: config.data ? JSON.stringify(config.data) : undefined
    }).then(async (response) => {
      if (!response.ok) {
        throw new HttpError(`HTTP ${response.status}`, response.status);
      }
      return response.json();
    });

    let result = this.retryConfig.enabled
      ? await requestWithRetry(fetchFn, this.retryConfig)
      : await fetchFn();

    // 响应拦截器
    for (const handler of this.interceptors.response) {
      result = await handler(result) || result;
    }
    return result;
  }

  get(url, params, options) {
    return this.request({ url, method: 'GET', params, options });
  }
  post(url, data, options) {
    return this.request({ url, method: 'POST', data, options });
  }
  delete(url, options) {
    return this.request({ url, method: 'DELETE', options });
  }
}

// 导出实例
export default HttpClient;

6.2 使用示例

javascript
// 创建实例
const http = new HttpClient({
  baseURL: '/api',
  timeout: 10000,
  headers: {
    'X-App-Version': '1.0.0',
  },
  retry: {
    enabled: true,
    retries: 3,
    delay: 1000,
    backoff: 'exponential',
    maxDelay: 30000
  }
});

// 添加拦截器
http.useRequestInterceptor((config) => {
  const token = localStorage.getItem('token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

http.useResponseInterceptor((response) => {
  // 统一处理业务码
  if (response && response.code !== 0) {
    throw new Error(response.message || '业务错误');
  }
  return response;
});

// 使用
const users = await http.get('/users', { page: 1 });

// 自定义重试
const data = await http.get('/data', null, { retries: 5, retryDelay: 2000 });

// 清除缓存
http.clearCache('/users');

七、API 参考

7.1 构造函数参数

参数类型默认值说明
baseURLstring''请求基础路径
timeoutnumber10000请求超时时间(毫秒)
headersobject{}默认请求头
retry.enabledbooleantrue是否启用重试
retry.retriesnumber3最大重试次数
retry.delaynumber1000重试延迟(毫秒)
retry.backoffstring'exponential'退避策略
cache.enabledbooleanfalse是否启用缓存
cache.ttlnumber300000缓存过期时间(毫秒)
cache.maxSizenumber100最大缓存数量

7.2 实例方法

方法参数返回值说明
request(options)ObjectPromise发送请求
get(url, params, options)string, Object, ObjectPromiseGET 请求
post(url, data, options)string, Object, ObjectPromisePOST 请求
put(url, data, options)string, Object, ObjectPromisePUT 请求
patch(url, data, options)string, Object, ObjectPromisePATCH 请求
delete(url, options)string, ObjectPromiseDELETE 请求
useRequestInterceptor(onFulfilled, onRejected)Function, FunctionHttpClient添加请求拦截器
useResponseInterceptor(onFulfilled, onRejected)Function, FunctionHttpClient添加响应拦截器
clearCache(pattern)stringvoid清除缓存

7.3 请求选项

参数类型默认值说明
urlstring-请求路径(必填)
methodstring'GET'请求方法
dataObject-请求体数据
paramsObject-URL 查询参数
headersObject-自定义请求头
timeoutnumber-本次请求超时时间
retriesnumber-本次请求重试次数
retryDelaynumber-本次请求重试延迟
cacheboolean-是否启用缓存
cacheTTLnumber-本次请求缓存时间

八、最佳实践

8.1 实例管理

javascript
// ✅ 推荐:单例模式
// api/http.js
const http = new HttpClient({ baseURL: '/api' });
export default http;

// 使用
import http from '@/api/http';

// ❌ 不推荐:每次都创建新实例
const http = new HttpClient({ baseURL: '/api' });
const data = await http.get('/users');

8.2 错误处理

javascript
// ✅ 推荐:统一错误处理 + 局部处理
http.useResponseInterceptor(
  (res) => res,
  (error) => {
    // 全局错误处理
    if (error.status === 401) {
      // 跳转登录
    }
    return Promise.reject(error);
  }
);

// 局部处理
try {
  const data = await http.get('/users');
} catch (error) {
  // 局部特殊处理
  console.error('获取用户失败:', error);
}

// ✅ 推荐:错误类型判断
if (error instanceof HttpError) {
  // HTTP 错误
} else if (error.name === 'TypeError') {
  // 网络错误
}

8.3 缓存使用

javascript
// ✅ 推荐:合理使用缓存
// 静态数据、配置数据使用长缓存
const config = await http.get('/config', null, { cacheTTL: 30 * 60 * 1000 });

// 动态数据禁用缓存或使用短缓存
const notifications = await http.get('/notifications', null, { 
  cache: false 
});

// 数据更新后清除相关缓存
await http.post('/users', userData);
http.clearCache('/users'); // 清除用户相关缓存

8.4 取消请求

javascript
// ✅ 推荐:避免重复请求
let pendingRequest = null;

async function searchUsers(query) {
  // 取消之前的请求
  if (pendingRequest) {
    pendingRequest.abort();
  }

  const controller = new AbortController();
  pendingRequest = controller;

  try {
    const result = await http.get('/users', { q: query }, {
      signal: controller.signal,
    });
    pendingRequest = null;
    return result;
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('请求已取消');
    } else {
      throw error;
    }
  }
}

九、常见问题

Q1: 如何处理文件上传?

javascript
class HttpClient {
  /**
   * 上传文件
   */
  async upload(url, file, options = {}) {
    const formData = new FormData();
    formData.append('file', file);
    
    // 添加其他字段
    if (options.data) {
      Object.entries(options.data).forEach(([key, value]) => {
        formData.append(key, value);
      });
    }
    
    // 使用 XHR 实现上传进度
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.upload.onprogress = (event) => {
        if (event.lengthComputable) {
          const percent = Math.round((event.loaded / event.total) * 100);
          options.onProgress?.(percent);
        }
      };
      xhr.onload = () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(JSON.parse(xhr.response));
        } else {
          reject(new Error(`HTTP ${xhr.status}`));
        }
      };
      xhr.onerror = () => reject(new Error('网络错误'));
      xhr.open('POST', this.baseURL + url);
      xhr.send(formData);
    });
  }
}


// 使用
const file = document.querySelector('#file').files[0];
await http.upload('/upload', file, {
  data: { userId: 123 },
  onProgress: (percent) => console.log(`上传进度: ${percent}%`),
});

Q2: 如何实现请求进度监听?

javascript
/**
 * 带进度的请求
 */
async requestWithProgress(url, data, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const percent = (event.loaded / event.total) * 100;
        onProgress?.(percent);
      }
    };

    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(JSON.parse(xhr.response));
      } else {
        reject(new Error(`HTTP ${xhr.status}`));
      }
    };

    xhr.onerror = () => reject(new Error('网络错误'));
    
    xhr.open('POST', this.baseURL + url);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.send(JSON.stringify(data));
  });
}

Q3: 如何处理并发请求?

javascript
// 并发请求
const [users, posts, comments] = await Promise.all([
  http.get('/users'),
  http.get('/posts'),
  http.get('/comments'),
]);

// 限制并发数量
async function limitConcurrency(tasks, limit = 3) {
  const results = new Array(tasks.length);
  const executing = [];
  let index = 0;

  async function worker() {
    while (index < tasks.length) {
      const taskIndex = index++;
      try {
        results[taskIndex] = await tasks[taskIndex]();
      } catch (error) {
        results[taskIndex] = { error };
      }
    }
  }

  // 启动 limit 个并发 worker
  const workers = Array.from({ length: Math.min(limit, tasks.length) }, worker);
  await Promise.all(workers);
  return results;
}

// 使用
const urls = ['/api/1', '/api/2', '/api/3', '/api/4', '/api/5'];
const results = await limitConcurrency(
  urls.map(url => () => http.get(url)),
  2 // 最多同时 2 个请求
);

Q4: 如何实现请求防抖?

javascript
/**
 * 防抖请求
 */
function debounceRequest(fn, delay = 300) {
  let timer = null;
  
  return function(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    
    return new Promise((resolve, reject) => {
      timer = setTimeout(() => {
        fn.apply(this, args)
          .then(resolve)
          .catch(reject);
      }, delay);
    });
  };
}

// 使用
const searchUsers = debounceRequest(async (query) => {
  return http.get('/users', { q: query });
}, 500);

// 输入时自动搜索(防抖)
input.addEventListener('input', (e) => {
  searchUsers(e.target.value);
});

💡 提示:请求封装是前端工程化的基础,合理的封装可以大幅提升开发效率和代码质量。建议根据项目实际需求选择合适的功能模块。


💡 提示:封装请求工具可以统一管理接口,提高代码可维护性。