稀土掘金技术社区 10月16日 09:50
自定义请求库,支持中断、缓存、重试、并发控制
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文介绍了一个自定义的请求库,它支持中断、缓存、重试、并发控制以及 SSE 自动解析等功能。该库弥补了现有请求库的不足,并提供了更高的灵活性和可配置性,适用于各种前端开发场景。

🔧 该库支持请求中断功能,可以随时取消进行中的请求,提高开发效率和用户体验。

💾 它提供了请求缓存功能,可以自动缓存请求结果,提高应用性能,减小服务端压力。

🔁 请求重试功能可以自动重试失败的请求,增强应用的稳定性。

🚦 并发控制功能可以轻松管理并发请求,保持结果顺序,提高请求效率。

🌊 SSE 流处理功能完美支持流式数据,特别适用于 AI 接口,自动字符串转 JSON,自动处理不完整的 JSON。

原创 寅时码 2025-10-16 08:30 重庆

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

点击关注公众号,“技术干货” 及时达!

📡 现况前端的请求库,大家基本都用的是 「Axios」 📦

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

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

如:

🌊 可读流

🛑 中断请求

🔗 自定义 referrer

由于 fetch 是 Promise,所以只有两种状态,即 「成功 ✅ | 失败 ❌」

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

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

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

💾 缓存请求

🔁 重试请求

🚦 并发请求

📡 SSE 流式数据处理

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

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

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

「NPM」:@jl-org/http - npm

「Github」: GitHub - beixiyo/jl-http: 支持中断、缓存、重试、并发控制,内置 SSE 自动解析的库

🚀 实现功能📋 第一,列出要实现的功能这点相当重要,因为后面要改,可比先想好再写麻烦多了 ⚡

✨ 特性🔄 「请求中断」 - 随时取消进行中的请求

💾 「请求缓存」 - 可选自动缓存请求,提高应用性能,减小服务端压力和潜在的多次错误调用

🔁 「请求重试」 - 自动重试失败的请求,增强应用稳定性

🚦 「并发控制」 - 轻松管理并发请求,保持结果顺序

🧩 「模板生成」 - 通过 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>
    }

    💾 请求缓存抽象类那么缓存抽象类要怎么缓存呢?

    定义一个 Map,url 作为键,响应作为值

    Map 还需要存一个时间,如果过期了,就删除这个缓存

    用户每次请求时,去缓存里看看,用深度递归的方式,比较值。如果请求体、url 一致,则直接返回

    每隔两秒,看看缓存有没有过期的,有则删除,释放内存

      /** 带缓存控制的请求基类 */
      export abstract class AbsCacheReq implements BaseHttpReq {
        abstract httpBaseHttpReq
        /** 缓存过期时间,默认 1 秒 */
        protected _cacheTimeout = 1000
        /** 未命中缓存 */
        protected static NO_MATCH_TAG = Symbol('No Match')
        /** 缓存已超时 */
        protected static CACHE_TIMEOUT_TAG = Symbol('Cache Timeout')

        protected cacheMap = new Map<stringCache>()

        // ...
      }

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

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

      ⚙️ 实现请求核心函数🔧 配置处理构造器负责收集默认配置,request 函数负责请求

      request 的配置会覆盖默认配置

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

          async request<T, HttpResponse = Resp<T>>(configBaseReqConfig): 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
          urlstring
          /** 基路径,传入后比实例化时的 baseUrl 优先级高 */
          baseUrl?: string
          /** 请求超时时间,默认 10 秒 */
          timeout?: number
          /** 是否终止请求,你也可以自己传递 signal 控制 */
          abort?: () => boolean
          query?: Record<stringany>
          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,
            optsRetryTaskOpts = {},
          ): Promise<T> {
            const { delayMs = 0 } = opts
            let attempts = 0
            let lastErrorError | 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 resultsTaskResult<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'reasonError }

              🌊 实现 SSE 自动解析完美支持 SSE 流式数据,特别适用于 AI 接口:

              用法

                /** 实时处理流式数据 */
                const { promise, cancel } = await iotHttp.fetchSSE('/ai/chat', {
                  method'POST',
                  body: {
                    messages: [{ role'user'content'你好' }]
                  },
                  /** 是否解析数据,删除 data: 前缀(默认为 true) */
                  needParseDatatrue,
                  /** 是否解析 JSON(默认为 true) */
                  needParseJSONtrue,
                  /** 每次接收到新数据时触发 */
                  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 数据流存在以下不可靠问题:

                  「📦 数据分片传输」:一个完整的 JSON 可能被分成多个数据块传输

                  「🔀 消息边界模糊」:数据可能在任意位置被切断

                  「❌ 不完整的消息」:单次接收的数据可能不是完整的 SSE 消息

                  「🎭 格式不一致」:不同服务可能有不同的数据格式

                  例如,一个完整的消息:

                    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 方法
                          bodyJSON.stringify({ query'hello' }), // 📍 可发送请求体
                          headers: {
                            'Authorization''Bearer token'// 📍 可设置任意请求头
                            'Content-Type''application/json'
                          }
                        })

                        2️⃣ 核心解析逻辑 - 有限状态机

                          async fetchSSE(urlstring, config?: SSEOptions): Promise<FetchSSEReturn> {
                            // 🔧 配置处理和拦截器设置
                            const formatConfig = this.normalizeSSEOpts(url, config)

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

                            // 📚 创建 SSE 解析器(核心状态机)
                            const sseParser = new SSEStreamProcessor({
                              needParseDatatrue,    // 是否解析 SSE 格式
                              needParseJSONtrue,    // 是否解析 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(0100))

                                    // 🎯 尝试解析剩余数据
                                    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」

                                  ✅ 完整类型

                                  ⚠️ 基础类型

                                  ⚠️ 类型不全

                                  🏆 核心优势总结「🔧 零配置智能解析」:自动处理 SSE 格式、JSON 解析、不完整数据

                                  「🚀 全能请求支持」:突破原生 EventSource 的 GET 限制

                                  「🛡️ 错误容错机制」:网络异常、数据格式错误不会中断整个流程

                                  「📊 实时进度追踪」:知道数据传输进度,提升用户体验

                                  「🎯 TypeScript 原生支持」:完整的类型提示,开发效率倍增

                                  「🔄 灵活的拦截器」:可以在请求 / 响应的任何阶段进行自定义处理

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

                                  实现进度处理后端必须写入 content-length 响应头

                                  前端必须监听 onProgress 回调

                                  通过复制响应体,然后读取流数据,计算进度,从而实现进度处理

                                    let contentLengthnumber
                                    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📝 定义配置文件

                                    🔄 读取配置文件,生成对应的代码

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

                                    但是读取文件只能用 「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 也有

                                      🔨 搭建 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 {
                                              inputresolve(process.cwd(), input || ''),
                                              outputresolve(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, { recursivetrue })
                                                }
                                              }

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

                                              ❓ 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<stringany>) {
                                                  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 插件 用来把 JSON 或 JS(包含各种复杂情况,如单双引号、有无声明语句)转为 type 或 interface

                                                GitHub-beixiyo/vsc-data-to-ts

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

                                                VSCode 插件市场搜 「Data To Typescript」

                                                🧪 测试🔁 重试测试默认重试三次 🎯

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

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

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

                                                「NPM」:@jl-org/http - npm

                                                「Github」GitHub - beixiyo/jl-http: 支持中断、缓存、重试、并发控制,内置 SSE 自动解析的库

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

                                                  # 构建核心包
                                                  pnpm build

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

                                                  🎯 总结这个 HTTP 库的核心优势:

                                                  🚀 「性能优越」:零依赖,体积小,加载快

                                                  🛠️ 「功能全面」:缓存、重试、并发、SSE 一应俱全

                                                  🎯 「智能解析」:自动处理复杂的 SSE 数据流

                                                  🔧 「高度可配置」:丰富的拦截器和配置选项

                                                  📱 「现代化」:基于 fetch,支持所有 HTTP 方法

                                                  🎭 「类型安全」:完整的 TypeScript 支持

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

                                                  ""~

                                                  阅读原文

                                                  跳转微信打开

                                                  Fish AI Reader

                                                  Fish AI Reader

                                                  AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

                                                  FishAI

                                                  FishAI

                                                  鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

                                                  联系邮箱 441953276@qq.com

                                                  相关标签

                                                  自定义请求库 中断 缓存 重试 并发控制 SSE 前端开发
                                                  相关文章