如何实现一个请求库?【面试场景题】

如何实现一个请求库?这个问题在面试中是一个常见的场景题,可以从第三方库的一些不足进行分析,然后答出如何实现这个库的步骤流程即可。

前端有诸多成熟的请求库,比如 axios、vue-request、swr 等,那么为什么在一些大中型项目中,放着成熟的请求库不用,仍然固执的选择重新造轮子?

  1. 定制化需求:成熟的请求库通常是通用型工具,难以完全满足特定业务场景的需求例如:
    • 统一处理错误码和异常
    • 自定义的请求拦截与响应拦截逻辑
    • 特定的请求缓存策略
    • 请求日志上报或埋点
  2. 统一接口规范
    • 对接后端统一的返回格式
    • 支持全局 Token 认证
    • 实现统一的超时控制、重试机制等
  3. 性能优化
    • 请求并发控制
    • 接口聚合(合并多个请求)
    • 预加载、懒加载策略

这些请求库到底有什么难以接受的缺陷,就连对它进行二次封装都难以解决问题?

  1. 扩展性受限
    • 虽然支持拦截器、插件等机制,但其内部结构可能不够灵活,无法很好地支持复杂的业务逻辑扩展
    • 某些定制化需求(如特定的缓存策略、重试机制)难以通过简单的封装实现
  2. 封装成本高
    • 二次封装往往只能"包裹"一层逻辑,而无法彻底控制底层行为(如重试策略、缓存机制等),导致封装后仍需频繁处理边界情况
    • 多层封装可能导致调用链路变长,调试困难,性能损耗增加
  3. 灵活性不足
    • 请求库的默认行为可能与项目需求冲突,例如默认的错误处理机制或超时控制策略,调整这些行为可能需要深入修改库的源码,而非简单的配置
  4. 维护复杂度高
    • 随着项目迭代,对请求库的定制化需求可能不断增加,依赖外部库的版本更新和兼容性问题会进一步增加维护成本

如果让你来实现一个完整的请求库,你会如何规划?

实现一个完整的请求库(HTTP 请求库)需要从功能完整性、易用性、性能和扩展性等多个方面进行规划

  1. 功能需求规划
    • 支持常见的 HTTP 方法:GET, POST, PUT, DELETE, PATCH 等
    • 支持设置请求头(Headers)
    • 支持发送请求体(Body),如 JSON、表单数据等格式
    • 支持查询参数(Query Parameters)
    • 支持响应拦截器与请求拦截器
    • 支持超时配置
    • 支持重试机制
  2. 模块化设计
    • RequestConfig:统一的请求配置接口,包含 URL、方法、headers、body 等
    • ResponseHandler:处理响应数据并返回标准化的响应对象
    • InterceptorManager:管理请求/响应拦截器,用于在请求前后执行逻辑(如添加 token、日志等)
  3. 异常处理
    • 请求超时
    • 网络错误
    • 响应状态码非 2xx(可自定义判定)
  4. 性能优化
    • 使用缓存策略
    • 支持并发控制(如 Promise.all + 控制并发数)
  5. 可扩展性设计
    • 插件系统:允许第三方开发者扩展功能(如自动重试插件、日志插件等)
  6. 测试
  7. 文档与示例
    • 提供详细的 API 文档
    • 提供常见使用场景的示例代码
    • 提供 TypeScript 类型定义文件

具体实现

基于 fetch 实现,实现了以下功能:

  1. 统一错误处理
    • 通过响应拦截器统一处理 HTTP 状态码
    • 支持自定义错误处理逻辑
  2. 请求/响应拦截器
    • 可以在请求发送前修改请求配置
    • 可以在响应返回后统一处理响应数据
    • 支持多个拦截器链式调用
  3. 请求缓存
    • 实现了基于内存的缓存系统
    • 支持设置缓存过期时间
    • 可以针对不同请求设置不同的缓存策略
  4. Token 认证
    • 支持全局设置认证 token
    • 自动将 token 添加到请求头
  5. 超时控制和重试机制
    • 可配置请求超时时间
    • 支持请求失败自动重试
    • 可设置最大重试次数
  6. 并发控制
    • 实现了请求并发数量限制
    • 使用队列管理超出并发限制的请求
  7. 请求聚合
    • 支持批量发送多个请求
    • 统一处理批量请求的结果
  8. 便捷方法
    • 提供了 get、post、put、delete 等常用方法
    • 支持自定义请求配置

RequestConfig 类

请求基础配置

js 复制代码
// 请求配置类型定义
class RequestConfig {
  constructor(config = {}) {
    this.baseURL = config.baseURL || ''
    this.timeout = config.timeout || 5000
    this.headers = config.headers || {}
    this.maxRetries = config.maxRetries || 3 // 最大重试次数
    this.maxConcurrent = config.maxConcurrent || 5 // 最大并发数
    this.cacheTime = config.cacheTime || 0 // 缓存时间,0表示不缓存
  }
}

RequestInterceptor 类

js 复制代码
// 请求拦截器
class RequestInterceptor {
  constructor() {
    this.interceptors = []
  }

  use(onFulfilled, onRejected) {
    this.interceptors.push({
      onFulfilled,
      onRejected
    })
  }

  execute(config) {
    // 使用 reduce 方法将每个拦截器串联起来,形成一个处理链,返回的是 config
    return this.interceptors.reduce((promise, interceptor) => {
      return promise.then(
        // 每个拦截器可以通过 onFulfilled 对配置进行处理并返回新的 config
        (config) => interceptor.onFulfilled(config),
        (error) => interceptor.onRejected(error)
      )
    }, Promise.resolve(config))
  }
}

ResponseHandler 类

js 复制代码
// 响应拦截器
class ResponseInterceptor {
  constructor() {
    this.interceptors = []
  }

  use(onFulfilled, onRejected) {
    this.interceptors.push({
      onFulfilled,
      onRejected
    })
  }

  execute(response) {
    return this.interceptors.reduce((promise, interceptor) => {
      return promise.then(
        (response) => interceptor.onFulfilled(response),
        (error) => interceptor.onRejected(error)
      )
    }, Promise.resolve(response))
  }
}

CacheManager 类

js 复制代码
// 请求缓存管理
class CacheManager {
  constructor() {
    this.cache = new Map()
  }

  set(key, value, expireTime) {
    if (expireTime <= 0) return
    const item = {
      value,
      expire: Date.now() + expireTime
    }
    this.cache.set(key, item)
  }

  get(key) {
    const item = this.cache.get(key)
    if (!item) return null
    if (Date.now() > item.expire) {
      this.cache.delete(key)
      return null
    }
    return item.value
  }

  clear() {
    this.cache.clear()
  }
}

ConcurrencyController 类

js 复制代码
// 并发控制器
class ConcurrencyController {
  constructor(maxConcurrent) {
    this.maxConcurrent = maxConcurrent
    this.currentConcurrent = 0
    this.queue = []
  }

  async add(fn) {
    if (this.currentConcurrent >= this.maxConcurrent) {
      await new Promise((resolve) => this.queue.push(resolve))
    }
    this.currentConcurrent++
    try {
      return await fn()
    } finally {
      this.currentConcurrent--
      if (this.queue.length > 0) {
        const next = this.queue.shift()
        next()
      }
    }
  }
}

HttpClient 类(核心)

js 复制代码
// 主请求类
class HttpClient {
  constructor(config = new RequestConfig()) {
    this.config = config
    this.requestInterceptor = new RequestInterceptor()
    this.responseInterceptor = new ResponseInterceptor()
    this.cacheManager = new CacheManager()
    this.concurrencyController = new ConcurrencyController(config.maxConcurrent)
    this.token = null

    // 添加默认响应拦截器处理错误码
    this.responseInterceptor.use(
      (response) => {
        // ok 属性是 Fetch API 的 Response 对象的一个只读属性。它是一个布尔值,用来表示响应是否成功
        if (!response.ok) {
          throw new Error(`HTTP Error: ${response.status}`)
        }
        return response.json()
      },
      (error) => {
        throw error
      }
    )
  }

  // 设置认证token
  setToken(token) {
    this.token = token
  }

  // 生成缓存key
  generateCacheKey(url, options) {
    return `${options.method || 'GET'}-${url}-${JSON.stringify(
      options.body || ''
    )}`
  }

  // 执行请求
  async request(url, options = {}) {
    const cacheKey = this.generateCacheKey(url, options)
    const cachedResponse = this.cacheManager.get(cacheKey)
    if (cachedResponse) return cachedResponse

    // 重试计数
    let retries = 0
    const executeRequest = async () => {
      try {
        // 合并配置
        const finalOptions = {
          ...options,
          headers: {
            ...this.config.headers,
            ...options.headers,
            ...(this.token ? { Authorization: `Bearer ${this.token}` } : {})
          }
        }

        // 应用请求拦截器
        const processedConfig = await this.requestInterceptor.execute(
          finalOptions
        )
        // console.log('processedConfig', processedConfig);

        // 执行请求
        const response = await this.concurrencyController.add(() =>
          // race 接收一个 Promise 数组,一旦其中一个 Promise 被解决(resolve)或拒绝(reject),就立即以该 Promise 的结果作为自身结果来完成或拒绝。
          // 使用 Promise.race() 来实现超时处理,如果 fetch 在超时时间内完成,则返回结果;否则抛出异常
          Promise.race([
            fetch(this.config.baseURL + url, processedConfig),
            new Promise((_, reject) =>
              setTimeout(
                () => reject(new Error('Request timeout')),
                this.config.timeout
              )
            )
          ])
        )

        // 应用响应拦截器
        const processedResponse = await this.responseInterceptor.execute(
          response
        )

        // 缓存响应
        this.cacheManager.set(
          cacheKey,
          processedResponse,
          this.config.cacheTime
        )

        return processedResponse
      } catch (error) {
        if (retries < this.config.maxRetries) {
          retries++
          return executeRequest()
        }
        throw error
      }
    }

    return executeRequest()
  }

  // 批量请求
  async batch(requests) {
    return Promise.all(
      requests.map((req) => this.request(req.url, req.options))
    )
  }

  // 便捷方法
  get(url, options = {}) {
    return this.request(url, { ...options, method: 'GET' })
  }

  post(url, data, options = {}) {
    return this.request(url, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data),
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      }
    })
  }

  put(url, data, options = {}) {
    return this.request(url, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data),
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      }
    })
  }

  delete(url, options = {}) {
    return this.request(url, { ...options, method: 'DELETE' })
  }
}

// 导出实例
export default new HttpClient()

使用案例

js 复制代码
import http from './fetch.js'

// 配置全局token
http.setToken('your-auth-token')

// 添加请求拦截器
http.requestInterceptor.use(
  (config) => {
    config.headers['Custom-Header'] = 'value'
    // 在发送请求之前做些什么
    console.log('Request interceptor:', config)
    return config
  },
  (error) => {
    // 对请求错误做些什么
    console.error('Request error:', error)
    return Promise.reject(error)
  }
)

// 添加响应拦截器
http.responseInterceptor.use(
  (response) => {
    // 对响应数据做点什么
    console.log('Response interceptor:', response)
    return response
  },
  (error) => {
    // 对响应错误做点什么
    console.error('Response error:', error)
    return Promise.reject(error)
  }
)

// 示例请求
async function testApi() {
  try {
    // 单个请求
    const data = await http.get('https://jsonplaceholder.typicode.com/posts')
    console.log('Single request result:', data)

    // 批量请求
    const batchResults = await http.batch([
      { url: 'https://jsonplaceholder.typicode.com/posts/1' },
      { url: 'https://jsonplaceholder.typicode.com/posts/2' }
    ])
    console.log('Batch request results:', batchResults)

    // POST请求示例
    const postData = await http.post(
      'https://jsonplaceholder.typicode.com/posts',
      {
        name: 'test',
        value: 123
      }
    )
    console.log('POST request result:', postData)
  } catch (error) {
    console.error('API test error:', error)
  }
}

// 运行测试
testApi()
相关推荐
GISer_Jing2 小时前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪3 小时前
CSS复习
前端·css
咖啡の猫5 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲7 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5817 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路8 小时前
GeoTools 读取影像元数据
前端
ssshooter8 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry9 小时前
Jetpack Compose 中的状态
前端
dae bal10 小时前
关于RSA和AES加密
前端·vue.js
柳杉10 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化