本文深入探讨了前端 HTTP 请求库的自研之路,作者首先分析了当前主流请求库(如 Axios 基于 XHR)相对于 Fetch API 的局限性,以及现有库在缓存、重试、并发、SSE 处理等方面功能不足或与框架绑定的痛点。在此基础上,作者详细阐述了其自研的 `@jl-org/http` 库所实现的核心功能,包括基于 AbortController 的请求中断、基于 Map 的请求缓存策略、利用 while 循环实现的请求重试机制、基于任务队列的并发控制方案,以及一项创新性的 SSE 流式数据智能解析方案(利用 Fetch API 和有限状态机解决数据分片、消息边界模糊和不完整消息等传输不可靠性问题)。此外,文章还介绍了如何通过监听 Content-Length 头实现请求进度追踪,并提供了一个 CLI 工具来自动生成 API 接口代码,提升开发效率。文章通过代码片段展示了关键实现细节,并提供了重试和缓存的测试截图,最后总结了该库在性能、功能、智能解析、可配置性和类型安全方面的核心优势。
📡 现况
前端的请求库,大家基本都用的是 「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 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?: stringheaders?: 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?: FetchTypeurl: string/** 基路径,传入后比实例化时的 baseUrl 优先级高 */baseUrl?: string/** 请求超时时间,默认 10 秒 */timeout?: number/** 是否终止请求,你也可以自己传递 signal 控制 */abort?: () => booleanquery?: 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 } = optslet attempts = 0let lastError: Error | undefinedmaxAttempts = 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.lengthif (numTasks === 0)return Promise.resolve([])const results: TaskResult<T>[] = new Array(numTasks)/** 当前正在运行的任务数 */let running = 0/** 已完成的任务数 */let completed = 0/** 下一个要执行的任务的索引 */let index = 0return 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 promiseconsole.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') // 仅支持 GETeventSource.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, // 是否解析 JSONseparator: '\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 += chunkif (this.config.needParseData) {// 🔍 SSE 格式解析模式const result = this.parseBufferSSE()// 🧹 更新缓冲区,移除已处理的完整消息this.buffer = result.remainingBuffer// 📊 收集解析结果parsedObjects = result.parsedObjectsstreamEndedThisChunk = result.streamEnded}else {// 📄 纯文本模式:直接处理数据块currentRawPayload = chunk}// 📢 触发回调,通知外部处理结果this.config.onMessage({currentContent: currentRawPayload, // 当前块的内容allContent: this.allRawPayloadsString, // 所有内容currentJson: parsedObjects, // 当前解析的 JSONallJson: 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 = falsebreak}// ✂️ 提取完整的消息块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」 |
✅ 完整类型 |
⚠️ 基础类型 |
⚠️ 类型不全 |
🏆 核心优势总结
-
「🔧 零配置智能解析」:自动处理 SSE 格式、JSON 解析、不完整数据
-
「🚀 全能请求支持」:突破原生 EventSource 的 GET 限制
-
「🛡️ 错误容错机制」:网络异常、数据格式错误不会中断整个流程
-
「📊 实时进度追踪」:知道数据传输进度,提升用户体验
-
「🎯 TypeScript 原生支持」:完整的类型提示,开发效率倍增
-
「🔄 灵活的拦截器」:可以在请求 / 响应的任何阶段进行自定义处理
这套 SSE 处理方案完美解决了传统方案的痛点,为现代 Web 应用提供了强大而可靠的实时数据处理能力! 🎉
实现进度处理
-
后端必须写入
content-length响应头 -
前端必须监听
onProgress回调 -
通过复制响应体,然后读取流数据,计算进度,从而实现进度处理
let contentLength: numberif (onProgress&& (contentLength = Number(response.headers.get('content-length'))) > 0) {const res = response.clone()const reader = res.body!.getReader()let loaded = 0while (true) {const { done, value } = await reader.read()if (done) {break}loaded += value.lengthconst 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 文件,第一行的注释是告诉他要执行命令
下面的代码是打印你传递的参数
import { resolve } from 'node:path'console.log(getSrc())function getSrc() {const [_, __, input, output] = process.argvreturn {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*['"](.*?)['"]/greturn 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 })}}
最终要实现的效果如下,左边的配置会转成右边的代码

❓ 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))continueconst value = args[k]const type = normalizeType(value)ts += `\n\t\t${k}: ${type}`}ts += '\n\t}'return ts}function normalizeType(value: string) {// @ts-ignoreconst type = typeMap[value]if (type)return typeif (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 支持
相信这套方案能为大家的项目带来更好的开发体验和用户体验!💪
