🛠️手写企业级请求库,支持缓存、重试、并发请求,以及SSE、进度处理...

1,776 阅读8分钟

📡 现况

前端的请求库,大家基本都用的是 Axios 📦

而他是基于 XHR 封装的,目前 XHR 已经停更了 ⏹️

相较于 fetch,缺失了一些功能 ❌

如:

  • 🌊 可读流
  • 🛑 中断请求
  • 🔗 自定义referrer

由于 fetchPromise,所以只有两种状态,即 成功 ✅ | 失败 ❌

所以 fetch 不能获取请求进度(不过我通过另一种方式实现了),而 XHR 基于事件,所以可以获取请求进度 📊

此外,fetch 还支持请求的优先级等 🎯

cc16a788439e471cf6e583172d89afe.png

🚫 缺失的功能

这些请求库,大多没有提供如下功能 😱

  • 💾 缓存请求
  • 🔁 重试请求
  • 🚦 并发请求
  • 📡 SSE 流式数据处理

不过还是有一些库支持的,但是这些库很喜欢和框架绑定在一起(Vue、React)。 这种做法没有任何优点

而且对于我而言,这些库差点定制化 🛠️

最重要的是,我喜欢造轮子,而不是写业务代码 😁


🚀 实现功能

📋 第一,列出要实现的功能

这点相当重要,因为后面要改,可比先想好再写麻烦多了 ⚡

✨ 特性

  • 🔄 请求中断 - 随时取消进行中的请求
  • 💾 请求缓存 - 可选自动缓存请求,提高应用性能,减小服务端压力和潜在的多次错误调用
  • 🔁 请求重试 - 自动重试失败的请求,增强应用稳定性
  • 🚦 并发控制 - 轻松管理并发请求,保持结果顺序
  • 🧩 模板生成 - 通过 CLI 工具快速生成模板代码
  • 📊 SSE流处理 - 完美支持流式数据,特别适用于AI接口,自动字符串转 JSON,自动处理不完整的JSON(因为消息是一点点发的,不保证完整性)
  • 进度追踪 - 实时掌握请求进度,提升用户体验
  • 📦 轻量级 - 零外部依赖,体积小,加载快
  • 🔧 高度可配置 - 灵活的拦截器和配置选项

🎯 定义接口

🏗️ 基础接口

滤清思路后,就要定义接口了。为什么一定要写个接口约束呢?🤔

这是因为方便修改

举个例子,你用 XHR 封装了一套 API

这时,fetch 突然发布了,那你不成了 “49年入国军” 了吗

这时你要改的话,那你就得非常的小心翼翼,一点点的对照之前的函数实现

为了避免以后发布比 fetch 更先进的 API 让我在写一遍,我提供了一个接口和一个抽象类

接口定义基础的请求方法,抽象类实现 缓存请求的方法

接口如下,就是 get | post ...

/** 请求基础接口 */
export interface BaseHttpReq {
  get: <T, HttpResponse = Resp<T>>(url: string, config?: BaseReqMethodConfig) => Promise<HttpResponse>
  head: <T, HttpResponse = Resp<T>>(url: string, config?: BaseReqMethodConfig) => Promise<HttpResponse>

  delete: <T, HttpResponse = Resp<T>>(url: string, data?: ReqBody, config?: BaseReqMethodConfig) => Promise<HttpResponse>
  options: <T, HttpResponse = Resp<T>>(url: string, data?: ReqBody, config?: BaseReqMethodConfig) => Promise<HttpResponse>

  post: <T, HttpResponse = Resp<T>>(url: string, data?: ReqBody, config?: BaseReqMethodConfig) => Promise<HttpResponse>
  put: <T, HttpResponse = Resp<T>>(url: string, data?: ReqBody, config?: BaseReqMethodConfig) => Promise<HttpResponse>
  patch: <T, HttpResponse = Resp<T>>(url: string, data?: ReqBody, config?: BaseReqMethodConfig) => Promise<HttpResponse>
}

💾 请求缓存抽象类

那么缓存抽象类要怎么缓存呢?

  1. 定义一个 Map,url 作为键,响应作为值
  2. Map 还需要存一个时间,如果过期了,就删除这个缓存
  3. 用户每次请求时,去缓存里看看,用深度递归的方式,比较值。如果请求体、url一致,则直接返回
  4. 每隔两秒,看看缓存有没有过期的,有则删除,释放内存
/** 带缓存控制的请求基类 */
export abstract class AbsCacheReq implements BaseHttpReq {
  abstract http: BaseHttpReq
  /** 缓存过期时间,默认 1 秒 */
  protected _cacheTimeout = 1000
  /** 未命中缓存 */
  protected static NO_MATCH_TAG = Symbol('No Match')
  /** 缓存已超时 */
  protected static CACHE_TIMEOUT_TAG = Symbol('Cache Timeout')

  protected cacheMap = new Map<string, Cache>()

  // ...
}

类型定义完毕,接下来只要实现请求的接口,然后继承那个抽象类即可。

以后有再多的请求 API,也仅需实现基础接口即可,这个后端同学应该比较熟。

⚙️ 实现请求核心函数

🔧 配置处理

构造器负责收集默认配置,request 函数负责请求

request 的配置会覆盖默认配置

export class BaseReq implements BaseHttpReq {
  constructor(private config?: BaseReqConstructorConfig) { }

  async request<T, HttpResponse = Resp<T>>(config: BaseReqConfig): Promise<HttpResponse> {
    /** 核心请求逻辑 */
  }

  // ... 其他方法,基于上面的 request 调用即可,get | post ...
}

/** 构造器默认配置 */
export interface BaseReqConstructorConfig {
  /** 基路径 */
  baseUrl?: string
  headers?: ReqHeaders
  /** 请求超时时间,默认 10 秒 */
  timeout?: number
  /** 重试请求次数 */
  retry?: number
  /** 请求拦截 */
  reqInterceptor?: (config: BaseReqMethodConfig) => any
  /** 响应拦截 */
  respInterceptor?: <T = any>(resp: Resp<T>) => any
  /** 错误拦截 */
  respErrInterceptor?: <T = any>(err: T) => any
}

export type FetchOptions = Omit<RequestInit, 'method'> & {
  method?: HttpMethod // 'GET' | 'POST' ...
}

/** 请求参数 */
export interface BaseReqConfig extends Omit<FetchOptions, 'body'> {
  /** 返回类型,默认 json。如果设置为 stream,会返回一个 ReadableStream */
  respType?: FetchType
  url: string
  /** 基路径,传入后比实例化时的 baseUrl 优先级高 */
  baseUrl?: string
  /** 请求超时时间,默认 10 秒 */
  timeout?: number
  /** 是否终止请求,你也可以自己传递 signal 控制 */
  abort?: () => boolean
  query?: Record<string, any>
  body?: ReqBody
  /** 重试请求次数 */
  retry?: number
}

🛠️ 实现配置功能

🔁 请求重试

非常简单,用 while 循环一直检查,直到失败次数达到上限抛出异常即可

/**
 * 失败后自动重试异步任务。
 * @param task 要执行的异步任务函数,该函数应返回一个 Promise。
 * @param maxAttempts 最大尝试次数(包括首次尝试)。默认为 3。
 * @returns 返回任务成功的结果 Promise。如果所有尝试都失败,则 reject 一个 RetryError。
 */
export async function retryTask<T>(
  task: () => Promise<T>,
  maxAttempts = 3,
  opts: RetryTaskOpts = {},
): Promise<T> {
  const { delayMs = 0 } = opts
  let attempts = 0
  let lastError: Error | undefined
  maxAttempts = Math.max(maxAttempts, 1)

  while (attempts < maxAttempts) {
    attempts++
    try {
      const res = await task()
      return res
    }
    catch (error) {
      lastError = error instanceof Error
        ? error
        : new Error(String(error))

      if (attempts >= maxAttempts) {
        /** 所有尝试已用尽,抛出最终错误 */
        throw new RetryError(
          `Task failed after ${attempts} attempts. Last error: ${lastError.message}`,
          attempts,
          lastError,
        )
      }
      /** 如果还有重试机会,并且设置了延迟 */
      if (delayMs > 0) {
        await wait(delayMs)
      }
      /** 可以在这里添加日志,记录重试尝试 */
      console.log(`Attempt ${attempts} failed for task. Retrying...`)
    }
  }

  /**
   * 理论上不应该执行到这里,因为循环内要么成功返回,要么在最后一次尝试失败后抛出错误
   * 但为了类型安全和逻辑完整性,如果意外到达这里,也抛出一个错误
   */
  throw new RetryError(
    `Task failed unexpectedly after ${attempts} attempts. Should not happen.`,
    attempts,
    lastError,
  )
}
🛑 终止请求

这是 fetch 自带的功能,只需要传递一个 AbortController 对象即可

在你想中断请求时调用 AbortController.abort 方法就能实现

const controller = new AbortController()
fetch('/test', { signal: controller.signal })
controller.abort()
🚦 请求并发

核心思想就是每次请求完成后

递归调用检查是否完成所有任务

没有完成则开启新任务,完成则 resolve

/**
 * 并发执行异步任务数组,并保持结果顺序。
 * 当一个任务完成后,会自动从队列中取下一个任务执行,直到所有任务完成。
 * @param tasks 要执行的异步任务函数数组。每个函数应返回一个 Promise。
 * @param maxConcurrency 最大并发数。默认为 4。
 * @returns 返回一个 Promise,该 Promise resolve 为一个结果对象数组,
 *          每个结果对象表示对应任务的完成状态(成功或失败)。
 *          结果数组的顺序与输入 tasks 数组的顺序一致。
 */
export function concurrentTask<T>(
  tasks: (() => Promise<T>)[],
  maxConcurrency = 4,
): Promise<TaskResult<T>[]> {
  const numTasks = tasks.length
  if (numTasks === 0)
    return Promise.resolve([])

  const results: TaskResult<T>[] = new Array(numTasks)
  /** 当前正在运行的任务数 */
  let running = 0
  /** 已完成的任务数 */
  let completed = 0
  /** 下一个要执行的任务的索引 */
  let index = 0

  return new Promise((resolve) => {
    function runNextTask() {
      while (running < maxConcurrency && index < numTasks) {
        const taskIndex = index++ // 捕获当前任务的索引
        running++

        tasks[taskIndex]()
          .then((value) => {
            results[taskIndex] = { status: 'fulfilled', value }
          })
          .catch((reason) => {
            results[taskIndex] = {
              status: 'rejected',
              reason: reason instanceof Error
                ? reason
                : new Error(String(reason)),
            }
          })
          .finally(() => {
            running--
            completed++
            if (completed === numTasks) {
              resolve(results)
            }
            else {
              runNextTask() // 一个任务完成,尝试补充新的任务
            }
          })
      }
    }

    runNextTask()
  })
}

export type TaskResult<T> =
  | { status: 'fulfilled', value: T }
  | { status: 'rejected', reason: Error }

🌊 实现 SSE 自动解析

完美支持SSE流式数据,特别适用于AI接口:

用法
/** 实时处理流式数据 */
const { promise, cancel } = await iotHttp.fetchSSE('/ai/chat', {
  method: 'POST',
  body: {
    messages: [{ role: 'user', content: '你好' }]
  },
  /** 是否解析数据,删除 data: 前缀(默认为 true) */
  needParseData: true,
  /** 是否解析 JSON(默认为 true) */
  needParseJSON: true,
  /** 每次接收到新数据时触发 */
  onMessage: ({ currentContent, allContent, currentJson, allJson }) => {
    console.log('当前片段:', currentContent)
    console.log('累积内容:', allContent)

    /** 如果启用了 needParseJSON */
    console.log('当前 JSON:', currentJson)
    console.log('累积 JSON:', allJson)
  },
  /** 跟踪进度 */
  onProgress: (progress) => {
    console.log(`进度: ${progress * 100}%`)
  },
  /** 错误处理 */
  onError: (error) => {
    console.error(error)
  },
})

const data = await promise
console.log('最终数据:', data)
📖 SSE 规范详解

在深入代码实现之前,我们先了解一下 Server-Sent Events (SSE) 的标准规范:

🔧 SSE 协议格式

SSE 是一种单向通信协议,服务器可以主动向客户端推送数据。其数据格式遵循以下规范:

data: 这是数据内容
event: 事件名称(可选)
id: 消息ID(可选)
retry: 重连间隔(可选)

data: 另一条消息

每个字段都以换行符结尾,完整的消息块以两个换行符\n\n)分隔。

⚠️ SSE 数据传输的不可靠性

由于网络传输的特性,SSE 数据流存在以下不可靠问题:

  1. 📦 数据分片传输:一个完整的 JSON 可能被分成多个数据块传输
  2. 🔀 消息边界模糊:数据可能在任意位置被切断
  3. ❌ 不完整的消息:单次接收的数据可能不是完整的 SSE 消息
  4. 🎭 格式不一致:不同服务可能有不同的数据格式

例如,一个完整的消息:

data: {"name": "张三", "age": 25}

可能会被分成这样接收:

// 第一次接收
"data: {\"name\": \"张"

// 第二次接收
"三\", \"age\": 25}\n\n"
🛠️ 代码实现解析
1️⃣ 使用 Fetch API 获取 SSE 数据流

相比浏览器原生的 EventSource,使用 fetch 有以下优势:

// ❌ 原生 EventSource 的限制
const eventSource = new EventSource('/api/sse') // 仅支持 GET
eventSource.onmessage = (event) => {
  console.log(event.data) // 只能接收 data 字段
}

// ✅ 使用 fetch 的优势
const response = await fetch('/api/sse', {
  method: 'POST', // 📍 支持任何 HTTP 方法
  body: JSON.stringify({ query: 'hello' }), // 📍 可发送请求体
  headers: {
    'Authorization': 'Bearer token', // 📍 可设置任意请求头
    'Content-Type': 'application/json'
  }
})
2️⃣ 核心解析逻辑 - 有限状态机
async fetchSSE(url: string, config?: SSEOptions): Promise<FetchSSEReturn> {
  // 🔧 配置处理和拦截器设置
  const formatConfig = this.normalizeSSEOpts(url, config)

  // 📡 发起 fetch 请求
  const response = await fetch(withQueryUrl, data)

  // 📚 创建 SSE 解析器(核心状态机)
  const sseParser = new SSEStreamProcessor({
    needParseData: true,    // 是否解析 SSE 格式
    needParseJSON: true,    // 是否解析 JSON
    separator: '\n\n',      // 消息分隔符
    dataPrefix: 'data:',    // 数据前缀
    doneSignal: '[DONE]',   // 结束信号
    onMessage: (data) => {
      // 实时处理解析后的数据
      console.log('解析结果:', data)
    }
  })

  // 🌊 读取流数据
  const reader = response.body!.getReader()
  const decoder = new TextDecoder()

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    // 🔄 将二进制数据解码为字符串
    const chunk = decoder.decode(value)

    // 🧠 核心:将数据块交给状态机处理
    sseParser.processChunk(chunk)
  }
}
3️⃣ SSEStreamProcessor - 智能解析引擎

这是整个 SSE 处理的核心,采用有限状态机设计:

export class SSEStreamProcessor {
  private buffer: string = '' // 📦 数据缓冲区
  private allJsonObjects: any[] = [] // 🗃️ 累积的 JSON 对象
  private allRawPayloadsString: string = '' // 📝 累积的原始字符串
  private isEnd: boolean = false // 🏁 流结束标志

  processChunk(chunk: string): ProcessChunkResult {
    // 🚫 流已结束,不再处理新数据
    if (this.isEnd) {
      console.warn('流已结束')
      return this.getCurrentStateAsResult('', [])
    }

    // 📥 将新数据添加到缓冲区
    this.buffer += chunk

    if (this.config.needParseData) {
      // 🔍 SSE 格式解析模式
      const result = this.parseBufferSSE()
      // 🧹 更新缓冲区,移除已处理的完整消息
      this.buffer = result.remainingBuffer
      // 📊 收集解析结果
      parsedObjects = result.parsedObjects
      streamEndedThisChunk = result.streamEnded
    }
    else {
      // 📄 纯文本模式:直接处理数据块
      currentRawPayload = chunk
    }

    // 📢 触发回调,通知外部处理结果
    this.config.onMessage({
      currentContent: currentRawPayload, // 当前块的内容
      allContent: this.allRawPayloadsString, // 所有内容
      currentJson: parsedObjects, // 当前解析的 JSON
      allJson: this.allJsonObjects // 所有 JSON 对象
    })

    return this.getCurrentStateAsResult(currentRawPayload, parsedObjects)
  }
}
4️⃣ 解决数据不可靠性的关键技术

🔧 缓冲区机制

private parseBufferSSE(): InternalParseResult {
  let remainingBuffer = this.buffer

  // 📋 使用分隔符切割完整消息
  SSEStreamProcessor.parseSSEMessages({
    content: remainingBuffer,
    separator: '\n\n',    // 标准 SSE 分隔符
    onMessage: ({ content, remainingBuffer: newBuffer }) => {
      // ✅ 只处理完整的消息
      if (content) {
        // 🎯 解析 JSON(如果需要)
        const parsed = JSON.parse(content)
        parsedObjects.push(parsed)
      }
      // 🔄 更新剩余缓冲区
      remainingBuffer = newBuffer
    }
  })

  return { parsedObjects, remainingBuffer }
}

🎭 消息格式处理

static parseSSEMessages(config: ParseSSEContentParam) {
  // 🔄 循环处理缓冲区直到没有完整消息
  while (continueParsing) {
    const separatorIndex = currentBuffer.indexOf(separator)

    // 🚫 找不到分隔符,说明消息不完整,停止处理
    if (separatorIndex === -1) {
      continueParsing = false
      break
    }

    // ✂️ 提取完整的消息块
    const messageBlock = currentBuffer.slice(0, separatorIndex)

    // 📝 解析消息块中的各行数据
    const lines = messageBlock.split('\n')
    for (const line of lines) {
      if (line.startsWith('data:')) {
        // 🎯 提取数据内容
        const payload = line.slice(5).trim()
        currentPayload += payload
      }
      else if (line.startsWith('event:')) {
        // 🏷️ 提取事件名
        currentEventName = line.slice(6).trim()
      }
    }

    // 📤 触发消息回调
    onMessage?.({ content: currentPayload, event: currentEventName })

    // ➡️ 移动到下一个消息
    currentBuffer = currentBuffer.slice(separatorIndex + separator.length)
  }
}

🛡️ 错误容错机制

// 🔄 处理剩余缓冲区数据
handleRemainingBuffer(): ProcessChunkResult | null {
  if (this.buffer.trim() === '') return null

  // ⚠️ 警告:有未处理的数据
  console.warn('处理剩余缓冲区内容:', this.buffer.slice(0, 100))

  // 🎯 尝试解析剩余数据
  try {
    const parsed = JSON.parse(this.buffer)
    // ✅ 成功解析,添加到结果中
    this.allJsonObjects.push(parsed)
  } catch (error) {
    // ❌ 解析失败,记录错误但不中断流程
    console.error('剩余数据解析失败:', error)
  }

  return this.getCurrentStateAsResult(this.buffer, [])
}
5️⃣ 与市面上 SSE 库的对比
特性对比🔥 本库🌐 原生 EventSource📚 其他库
HTTP 方法✅ 支持所有方法❌ 仅 GET⚠️ 部分支持
请求体✅ 支持任意格式❌ 不支持⚠️ 有限支持
自定义 Headers✅ 完全支持❌ 不支持✅ 支持
拦截器✅ 请求/响应拦截❌ 不支持❌ 不支持
自动 JSON 解析✅ 智能解析❌ 手动解析⚠️ 基础解析
不完整数据处理✅ 缓冲区机制❌ 可能丢失⚠️ 简单处理
进度追踪✅ 实时进度❌ 不支持❌ 不支持
请求取消✅ 随时取消✅ 支持⚠️ 有限支持
错误重试✅ 自动重试❌ 手动重连⚠️ 基础重试
TypeScript✅ 完整类型⚠️ 基础类型⚠️ 类型不全
🏆 核心优势总结
  1. 🔧 零配置智能解析:自动处理 SSE 格式、JSON 解析、不完整数据
  2. 🚀 全能请求支持:突破原生 EventSource 的 GET 限制
  3. 🛡️ 错误容错机制:网络异常、数据格式错误不会中断整个流程
  4. 📊 实时进度追踪:知道数据传输进度,提升用户体验
  5. 🎯 TypeScript 原生支持:完整的类型提示,开发效率倍增
  6. 🔄 灵活的拦截器:可以在请求/响应的任何阶段进行自定义处理

这套 SSE 处理方案完美解决了传统方案的痛点,为现代 Web 应用提供了强大而可靠的实时数据处理能力! 🎉


实现进度处理

  1. 后端必须写入 content-length 响应头
  2. 前端必须监听 onProgress 回调
  3. 通过复制响应体,然后读取流数据,计算进度,从而实现进度处理
let contentLength: number
if (
  onProgress
  && (contentLength = Number(response.headers.get('content-length'))) > 0
) {
  const res = response.clone()
  const reader = res.body!.getReader()
  let loaded = 0
  while (true) {
    const { done, value } = await reader.read()
    if (done) {
      break
    }

    loaded += value.length
    const progress = Number((loaded / contentLength).toFixed(2))
    onProgress?.(progress)
  }
}
else if (onProgress) {
  onProgress(-1)
}

🧩 实现自动生成代码 CLI

  1. 📝 定义配置文件
  2. 🔄 读取配置文件,生成对应的代码

就这两步,是不是很简单 😊

但是读取文件只能用 CJS 📦。 因为 ESM 不支持绝对路径导入模块,所以你想用动态 import 是不行的 ❌。

但是我就想用 ESM 写配置文件怎么办呢?🤔

那就只能转译一下代码,把 esm 转成 cjs 🔄

先写个辅助函数,给配置文件加上类型提示

export function defineConfig(config: Config) {
  return config
}

export type Config = {
  /** 顶部导入的路径 */
  importPath: string
  /** 类名 */
  className: string
  /** 可以发送请求的对象 */
  requestFnName: string
  /** 类里的函数 */
  fns: Fn[]
}

export type Fn = {
  /** 函数的名字 */
  name: string
  /** 添加异步关键字 */
  isAsync?: boolean
  /** 请求地址 */
  url: string
  /**
   * 生成 TS 类型的代码
   * 你可以像写 TS 一样写,也可以写字面量,字面量会被自动转换类型
   */
  args?: Record<string, any>
  /** 请求的方法,如 get | post | ... */
  method: Lowercase<HttpMethod>
}

于是这样就有了类型提示,就算你用 js 也有

image.png

🔨 搭建 CLI 脚手架

首先在 package.json 里的 bin,写上执行的文件路径和执行命令名字

"bin": {
  "jl-http": "./cli/index.cjs"
},

创建 ./cli/index.cjs 文件,第一行的注释是告诉他要执行命令

下面的代码是打印你传递的参数

#!/usr/bin/env node
import { resolve } from 'node:path'

console.log(getSrc())

function getSrc() {
  const [_, __, input, output] = process.argv
  return {
    input: resolve(process.cwd(), input || ''),
    output: resolve(process.cwd(), output || ''),
  }
}

然后 npm link

接下来你就能用自定义的命令了,比如我上面的命令

jl-http ./src/config.ts ./src/output.ts

执行这行命令会输出你传递的路径

🔍 识别配置文件

我希望我能用 ESM,但是代码显然是无法使用的

于是我写个简单的代码转译一下,然后把转译的文件,放入 node_modules 里的临时目录 到时候我读取那个临时文件即可,读完再删掉

import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'

export function esmTocjs(path: string) {
  const content = readFileSync(path, 'utf-8')
  const reg = /import\s*\{\s*(.*?)\s*\}\s*from\s*['"](.*?)['"]/g

  return content
    .replace(reg, (_match: string, fn: string, path: string) => {
      return `const { ${fn} } = require('${path}')`
    })
    .replace(/export default/g, 'module.exports =')
}

export function writeTempFile(cjsCode: string, tempPath: string, tempFile: string) {
  createDir(tempPath)
  writeFileSync(resolve(process.cwd(), `${tempPath}/${tempFile}`), cjsCode, 'utf-8')
}

function createDir(dir: string) {
  if (!existsSync(dir)) {
    mkdirSync(dir, { recursive: true })
  }
}

最终要实现的效果如下,左边的配置会转成右边的代码

image.png

❓ QA

Q:你这配置文件比你代码还多,你是不是有病?(疑?)🤔 A:写接口最麻烦的事就是定义类型,所以 args 参数直接复制文档即可 📋 我这里的类型如果识别不到,就会用类型转换,所以你直接复制就行了(悟!)✨

Q:为什么要用类呢?(疑?)🤔 A:如果你接口写多了,那你导入的时候,你要import { ... 好多好多 },你记得住吗?🤯 写静态类的话,你直接 类名. 就有代码提示了(悟!)💡

接下来的内容就很简单了,就是配置转字符串,也叫编译。 也就类型转换有点难度,我把这部分贴一下,参数就是配置文件里的 args

最后的转换不能用 typeof,因为他识别的全是 object

/** 获取类型 */
export const getType = (data: any) => (Object.prototype.toString.call(data) as string).slice(8, -1).toLowerCase()

const typeMap = {
  string: 'string',
  number: 'number',
  boolean: 'boolean',
  true: 'true',
  false: 'false',
  array: 'any[]',
  object: 'object',
  any: 'any',
  null: 'null',
  undefined: 'undefined',
  function: 'Function',
  Function: 'Function',
  bigInt: 'bigInt',
}

export function genType(args?: Record<string, any>) {
  if (!args)
    return ''

  let ts = '{'
  for (const k in args) {
    if (!Object.hasOwnProperty.call(args, k))
      continue

    const value = args[k]
    const type = normalizeType(value)
    ts += `\n\t\t${k}: ${type}`
  }

  ts += '\n\t}'
  return ts
}

function normalizeType(value: string) {
  // @ts-ignore
  const type = typeMap[value]
  if (type)
    return type

  if (typeof value === 'string') {
    const match = value.match(/.+?\[\]/g)
    if (match?.[0]) {
      return match[0]
    }
  }

  const finaltype = getType(value)
  return finaltype === 'array'
    ? 'any[]'
    : finaltype
}

另外,我还写了个 VSCode 插件 用来把 JSONJS(包含各种复杂情况,如单双引号、有无声明语句)转为 typeinterface

github.com/beixiyo/vsc…

VSCode已经有类似插件,为什么要写呢?(疑?) 因为他们仅仅支持 JSON 转 TS,而且无法配置,比如要不要导出、要不要分号、选择 interface 还是 type 而我写的,全都支持(悟!

VSCode 插件市场搜 Data To Typescript

image.png

🧪 测试

🔁 重试测试

默认重试三次 🎯

image.png image.png

💾 缓存测试

get | post 各发两次,后面直接返回缓存了 ⚡

image.png

至此,大功告成!🎉 代码我已经发布在 npm,大家直接去下载就能用了 📦

代码内提供了完整的文档注释,以及百分百覆盖率的测试代码 ✅

自动化测试

此外,我还用 vitest 写了全面的测试,包括集成测试、Web 页面测试 ...

# 构建核心包
pnpm build

# 运行所有测试
pnpm test
# 运行 Web 页面测试
pnpm test:page

🎯 总结

这个HTTP库的核心优势:

  • 🚀 性能优越:零依赖,体积小,加载快
  • 🛠️ 功能全面:缓存、重试、并发、SSE一应俱全
  • 🎯 智能解析:自动处理复杂的SSE数据流
  • 🔧 高度可配置:丰富的拦截器和配置选项
  • 📱 现代化:基于fetch,支持所有HTTP方法
  • 🎭 类型安全:完整的TypeScript支持

相信这套方案能为大家的项目带来更好的开发体验和用户体验!💪