dart-request
v0.0.1
Published
DART.IM组件库 - 基于原生 fetch 的通用请求库
Maintainers
Readme
dart-request
基于原生 fetch 的通用请求库。axios 风格实例、拦截器、超时与取消、重试、重复请求去重、下载进度;运行时除 dart-common 外零第三方依赖,浏览器与 Node 18+ 同构。
特性
- 原生实现:只用
fetch/AbortController/Headers/FormData/Blob/ReadableStream - axios 风格实例:
createRequest()得到独立配置、独立拦截器、独立去重表 - 可控失败:统一
RequestError与稳定错误码,HTTP / 网络 / 超时 / 取消 / 解析可区分 - 纯 HTTP 层:不判断任何业务码,业务约定交给响应拦截器
安装
npm i dart-request dart-common
dart-common是 peerDependency,必须一起安装。
快速开始
import request, { createRequest } from 'dart-request'
const api = createRequest({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: { Authorization: 'Bearer token' }
})
const { data, status } = await api.get('/users', { params: { page: 1, size: 20 } })
const created = await api.post('/users', { name: 'Alice' })
// 默认实例
const health = await request.get('/health')API
createRequest(defaults?)
| 成员 | 说明 |
| ------------------------------------- | --------------------------------------------------- |
| request(config) / request(url) | 发起请求,config 可为字符串地址 + 配置对象 |
| get / delete / head / options | 无请求体的快捷方法 |
| post / put / patch | 带请求体的快捷方法,签名为 (url, data, config) |
| defaults | 实例默认配置,可读写 |
| interceptors.request / response | 拦截器管理器 |
| create(childDefaults) | 基于当前配置创建子实例,不继承拦截器 |
| fetch(input, init) | 原生 fetch 逃生舱,不套用 baseURL、拦截器与默认配置 |
顶层导出
request(默认实例)、createRequest、get、post、put、patch、del、head、options、RequestError、ErrorCode、createError、isRequestError、isCanceled。
delete是保留字,因此顶层导出名为del;实例上仍是instance.delete()。
拦截器
| 方法 | 说明 |
| -------------------------------- | ------------------- |
| use(onFulfilled?, onRejected?) | 注册拦截器,返回 id |
| remove(id) / eject(id) | 按 id 移除 |
| clear() | 清空 |
| size | 当前有效拦截器数量 |
- 请求拦截器逆序执行(后注册的先跑),响应拦截器正序执行(先注册的先跑)
- 回调返回
undefined表示「原样放行」;请求拦截器返回非普通对象会以ERR_INVALID_CONFIG拒绝 - 响应拦截器可以
return response.data直接做业务解包
const api = createRequest({ baseURL: '/api' })
api.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${getToken()}`
return config
})
api.interceptors.response.use((response) => {
const body = response.data
if (body && body.code === 'success') return body.data
return response
})配置项
| 字段 | 默认值 | 说明 |
| -------------------- | ------------------------------------------------- | -------------------------------------------------------------------- |
| url | '' | 请求地址,相对地址与 baseURL 拼接 |
| method | 'GET' | 内部统一转大写 |
| baseURL | '' | 基础地址 |
| headers | { Accept: 'application/json, text/plain, */*' } | 值为 null 表示删除该头 |
| params | null | 查询参数:对象 / URLSearchParams / 字符串 |
| paramsSerializer | null | 自定义序列化函数或选项对象,false 表示禁用 |
| data | undefined | 请求体 |
| timeout | 0 | 毫秒,0 或 Infinity 表示不超时 |
| withCredentials | false | 为 true 时映射成 credentials: 'include' |
| credentials | undefined | 直接透传 fetch,优先级高于 withCredentials |
| responseType | 'auto' | auto / json / text / blob / arrayBuffer / stream / raw |
| validateStatus | 2xx | 成功判定,可自定义 |
| transformRequest | [] | 请求体转换,内置 JSON 序列化最后执行 |
| transformResponse | [] | 响应数据转换,在解析之后执行 |
| onDownloadProgress | null | 下载进度回调 |
| onUploadProgress | null | 原生 fetch 不支持,传了只会提示一次 |
| signal | null | 取消信号 |
| retry | false | 重试配置,true 表示使用默认参数 |
| dedupe | false | 去重配置,true 表示对默认方法按 reject 策略去重 |
| fetchOptions | null | 透传任意 fetch 选项 |
| duplex | undefined | 流式请求体在 Node 下自动补 'half' |
| meta | undefined | 业务自定义元数据,拦截器可读取,不传给 fetch |
mode / cache / redirect / referrer / referrerPolicy / integrity / keepalive 原样透传 fetch。
查询参数序列化
defaultParamsSerializer({ page: 1, tags: ['a', 'b'] }) // 'page=1&tags=a&tags=b'| 选项 | 默认值 | 说明 |
| ------------- | ---------- | ------------------------------------------------------------- |
| arrayFormat | 'repeat' | repeat → a=1&a=2;brackets → a[]=1;comma → a=1,2 |
| encode | true | encodeURIComponent,并保留 : $ , [ ] |
| skipNull | true | 跳过 null / undefined,为 false 时输出 key= |
规则补充:Date 转 ISO 字符串;嵌套对象展开为 a[b]=c;空数组不输出;params 为字符串时去掉前导 ? 后直接追加;查询串始终插在 hash 之前。
响应解析
| responseType | 行为 |
| -------------- | ----------------------------------------------------------------- |
| auto | 按 content-type 判定;无类型时读取文本,{ / [ 开头尝试 JSON |
| json | 严格 JSON.parse,失败抛 ERR_PARSE |
| text | 文本,按 charset 解码 |
| blob | Blob,环境不支持时抛 ERR_UNSUPPORTED |
| arrayBuffer | ArrayBuffer |
| stream | ReadableStream,不消费响应体,不支持进度与响应转换 |
| raw | 原始 Response,不消费响应体 |
空响应(204 / 205 / 304、content-length: 0)一律返回 null。
返回值
const response = await api.get('/users')
response.data // 解析后的数据
response.status // 200
response.statusText
response.headers // Headers 实例
response.config // 最终生效的配置
response.request // { url, method, headers }
response.response // 原始 Response(读过进度时为缓冲区重建的实例)
response.retryCount // 重试次数,首次为 0错误处理
import { ErrorCode, isCanceled } from 'dart-request'
try {
await api.get('/orders')
} catch (error) {
if (isCanceled(error)) return
if (error.code === ErrorCode.HTTP_STATUS) console.log(error.status, error.response.data)
throw error
}| 错误码 | 含义 |
| --------------------- | ----------------------------- |
| ERR_INVALID_CONFIG | 配置非法 |
| ERR_TIMEOUT | 超时 |
| ERR_CANCELED | 被取消 |
| ERR_NETWORK | 网络层失败(DNS、断网、CORS) |
| ERR_HTTP_STATUS | 状态码未通过 validateStatus |
| ERR_PARSE | 响应解析失败 |
| ERR_DUPLICATE | 命中重复请求去重 |
| ERR_RETRY_EXHAUSTED | 重试次数耗尽 |
| ERR_UNSUPPORTED | 当前环境缺少必要的原生能力 |
RequestError 字段:code、status、response、config、request、cause、retryCount。超时不算取消,便于区分「用户放弃」与「服务端太慢」。
重试
await api.get('/report', { retry: { retries: 3, retryDelay: 300 } })| 选项 | 默认值 | 说明 |
| ------------------- | ----------------------------- | ------------------------------------ |
| retries | 0 | 最大重试次数,0~10 |
| retryDelay | 300 | 首次重试基础延迟 |
| maxDelay | 5000 | 单次延迟上限 |
| backoff | 2 | 指数退避倍数 |
| jitter | 0.2 | 抖动比例,0 关闭 |
| methods | GET/HEAD/OPTIONS/PUT/DELETE | 参与重试的方法 |
| statusCodes | 408/429/500/502/503/504 | 参与重试的状态码 |
| retryOn | null | 自定义判定,返回 true 覆盖默认规则 |
| respectRetryAfter | true | 优先使用响应头 Retry-After |
只重试网络错误、超时与命中 statusCodes 的响应;用户已取消或请求体是 ReadableStream 时不重试。
重复请求去重
const api = createRequest({ dedupe: { strategy: 'share' } })
const [a, b] = await Promise.all([api.get('/dict'), api.get('/dict')]) // 只发一次| 选项 | 默认值 | 说明 |
| ---------- | ------------------ | ----------------------------------------------- |
| strategy | 'reject' | 重复时抛 ERR_DUPLICATE / share 复用同一请求 |
| key | null | 自定义去重键,返回空字符串表示跳过去重 |
| methods | GET/HEAD/OPTIONS | 参与去重的方法 |
去重键 = method + 最终 URL + responseType + 请求体摘要,不包含 header,跨认证上下文请自定义 key。share 策略下所有调用方共享同一个结果对象,且只使用首个请求的 signal / timeout。
取消与超时
const controller = new AbortController()
api.get('/slow', { signal: controller.signal, timeout: 5000 })
controller.abort()超时优先使用 AbortSignal.timeout + AbortSignal.any,缺失时回退 AbortController。超时会覆盖响应体读取阶段,定时器在请求结束(含失败)时清理。
下载进度
const res = await api.get('/export', {
responseType: 'blob',
onDownloadProgress: ({ loaded, total, percent }) => {
console.log(percent === null ? `${loaded}B` : `${percent}%`)
}
})无 Content-Length 时 total 与 percent 为 null(lengthComputable: false)。读取进度后响应体会被缓冲区重建,content-encoding / content-length 会被移除。
浏览器与 Node 兼容性
| 能力 | 说明 |
| ----------------- | -------------------------------------- |
| fetch | Node 18+ / 现代浏览器 |
| AbortSignal.any | 无原生实现时自动回退手写合并 |
| ReadableStream | 缺失时下载进度不可用,请求本身仍可发 |
| Blob | 缺失时 blob 解析抛 ERR_UNSUPPORTED |
| Response | 缺失时不重建响应,直接用缓冲区解析 |
已知限制
- 原生
fetch无法获取上传进度,onUploadProgress只提示一次,不做 XHR 降级 stream/raw不支持onDownloadProgress与transformResponse- 不做 GET 缓存、并发队列与 SSE
- 去重键不含 header,
share策略的取消与超时只认首个请求 baseURL只折叠边界斜杠,不做..路径归一化Content-Length受压缩影响,percent仅作展示参考- 浏览器禁止设置
Content-Length/Host等头,设置会被忽略 FormData不要手动设置Content-Type,否则 boundary 会丢失
开发
npm run dev # tsup watch
npm run build # tsup + tsc 生成 .d.ts
npm run lint
npm run format本地联调前需先构建同级依赖(该包当前没有 dist,import 'dart-common' 会解析失败):
cd ../npm-dart-common && npm run buildLicense
GPL-3.0-or-later
