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

06-01 1226阅读

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

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

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

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

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

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

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

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

具体实现

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

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

RequestConfig 类

请求基础配置

// 请求配置类型定义
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 类

// 请求拦截器
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 类

// 响应拦截器
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 类

// 请求缓存管理
class CacheManager {
  constructor() {
    this.cache = new Map()
  }
  set(key, value, expireTime) {
    if (expireTime 
      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 类

// 并发控制器
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 类(核心)

// 主请求类
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.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()

使用案例

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()
如何实现一个请求库?【面试场景题】
(图片来源网络,侵删)
如何实现一个请求库?【面试场景题】
(图片来源网络,侵删)
如何实现一个请求库?【面试场景题】
(图片来源网络,侵删)
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码