@tiyee/requests
v1.1.2
Published
借鉴 Python requests 调用方式的轻量 TypeScript fetch 封装
Readme
@tiyee/requests
借鉴 Python requests 的简洁调用方式,支持独立位置参数和配置对象,使用 JavaScript 的 async/await、原生 Response 和 AbortSignal。提供 Axios 风格的请求与响应拦截器,无运行时依赖,包含 ESM、CommonJS 和 TypeScript 类型声明。
运行环境:现代浏览器或 Node.js 18.17+。同时使用 timeout 和 signal 时需要原生 AbortSignal.any。
安装
pnpm add @tiyee/requests
# 或
npm install @tiyee/requests浏览器中可以使用相对 URL;Node.js 中需要完整 URL,或通过实例的 prefix 配置完整域名。以下 /api/... 示例适用于浏览器,也可在 Node.js 中替换成完整 URL。
import {create} from '@tiyee/requests'
// Node.js / ESM
const api = create({prefix: 'https://example.com/api'})
const response = await api.get('/users')
console.log(await response.json())CommonJS 使用默认导出的 default 属性,也支持具名导出:
const {default: requests} = require('@tiyee/requests')
requests.get('https://example.com/api/users')
.then(response => response.json())
.then(users => console.log(users))
.catch(error => console.error(error))快速开始
import requests from '@tiyee/requests'
const api = requests.create({
prefix: '/api',
timeout: 5000,
throwHttpErrors: true,
})
api.interceptors.request.use(config => {
config.headers.set('Authorization', 'Bearer token')
return config
})
// 独立参数顺序:url, params, data, json, headers, options
const response = await api.get('/users', {page: 1, tag: ['js', 'ts']})
console.log(response.status, response.ok)
console.log(response.headers.get('content-type'))
const users = await response.json()
await api.post('/users', {source: 'web'}, undefined,
{name: 'Alice'}, {'X-Request-ID': '123'})
// 配置对象用法也支持
await api.patch('/users/1', {json: {name: 'Bob'}})
await api.delete('/users/1')支持 get、post、put、patch、delete、head、options,也可以使用 requests.request('POST', url, options)。
独立参数
所有请求方法也接受独立位置参数,顺序一致:
requests.get(url, params, data, json, headers, options)
requests.post(url, params, data, json, headers, options)
requests.put(url, params, data, json, headers, options)
requests.patch(url, params, data, json, headers, options)
requests.delete(url, params, data, json, headers, options)
requests.head(url, params, data, json, headers, options)
requests.options(url, params, data, json, headers, options)
requests.request(method, url, params, data, json, headers, options)| 参数 | 类型 / 用途 | 省略时的行为 |
| --- | --- | --- |
| url | 请求地址,必填 | 与实例的 prefix、suffix 直接拼接 |
| params | 对象、URLSearchParams 或查询字符串 | 沿用配置中的查询参数 |
| data | 原生 BodyInit、对象或数组 | 沿用配置中的请求体 |
| json | JSON 可序列化值,支持 null、false、0 等值 | 沿用配置中的 JSON 请求体 |
| headers | HeadersInit:对象、Headers 或键值对数组 | 沿用配置中的请求头 |
| options | 最后的额外配置对象,如 timeout、signal、responseType | 使用实例配置和默认值 |
// 只传查询参数
const response = await requests.get('/api/users', {page: 1, tag: ['js', 'ts']})
// 查询参数 + JSON + 请求头;不使用的位置传 undefined
await requests.post('/api/users', {source: 'web'}, undefined,
{name: 'Alice'}, {Authorization: 'Bearer token'})
// data + 最后的额外配置对象
const result = await requests.post('/api/login', undefined,
new URLSearchParams({username: 'alice', password: 'secret'}),
undefined, undefined, {timeout: 5000, responseType: 'json'})位置参数会覆盖最后 options 对象中的对应字段,undefined 表示沿用配置。data 和 json 仍然互斥,GET/HEAD 仍然不接受请求体。
为兼容旧用法,只传一个对象且包含 params、headers、timeout 等配置字段时,会作为配置对象处理。查询参数与配置字段同名时,请显式补上第三个参数 undefined,例如 requests.get(url, {timeout: 123}, undefined)。
复用配置
import {create, extend} from '@tiyee/requests'
const api = create({
prefix: 'https://example.com/api',
headers: {Authorization: 'Bearer token'},
params: {lang: 'zh'},
timeout: 5000,
throwHttpErrors: true,
})
const response = await api.get('/users', {
headers: {'X-Request-ID': '123'},
params: {page: 1},
})
// 请求 https://example.com/api/users?lang=zh&page=1
// 同时发送 Authorization 和 X-Request-ID
const legacy = extend({prefix: '/api'}) // extend 是 create 的别名requests.create()、requests.extend() 也可使用。请求级配置覆盖实例配置;请求头按大小写不敏感的名称合并,查询参数按名称合并。请求级覆盖不会影响之后的请求。传入 params: {lang: null} 可移除实例中的 lang。
prefix、URL、suffix 按顺序直接拼接,不会自动补 /,完整 URL 也不会自动跳过 prefix。需要使用另一域名时,可在该请求的配置对象中设置 prefix: ''。
合并优先级为:实例配置 → 请求的配置对象 → 非 undefined 的独立位置参数。Headers 和 params 按键合并,其余配置直接覆盖。查询参数的数组使用重复键,null、undefined 和空数组可删除对应实例参数;URL 字符串上已有的查询参数会保留,新参数追加在其后。
请求体
// JSON:自动设置 Content-Type,保留调用方指定的请求头
await requests.post('/api/users', {json: {name: 'Alice'}})
// URL 编码表单:由 fetch 根据 URLSearchParams 设置 Content-Type
await requests.post('/api/login', {
data: new URLSearchParams({username: 'alice', password: 'secret'}),
})
// 文件:由 fetch 生成 multipart boundary
const form = new FormData()
form.append('file', new Blob(['hello'], {type: 'text/plain'}), 'hello.txt')
await requests.post('/api/upload', {data: form})
// 兼容原有 data + requestType 用法
await requests.post('/api/users', {data: {name: 'Alice'}}) // 默认按 JSON 编码
await requests.post('/api/login', {
data: {username: 'alice', password: 'secret'},
requestType: 'form',
})json 和 data 同时提供会抛出 TypeError。GET/HEAD 不接受请求体,请使用 params。字符串、Blob、二进制数据及其他原生 BodyInit 可通过 data 直接传入。
data 支持 ReadableStream,发送流式请求体时会自动设置 duplex: 'half';浏览器端仍需运行环境支持流式上传。json 和默认按 JSON 编码的 data 使用相同的序列化校验,循环引用、BigInt 或顶层 toJSON() 返回 undefined 都会抛出 TypeError。
响应与类型
默认返回原生 Response,使用 response.status、response.ok、await response.text()、await response.json()。响应体遵循 fetch 的一次性读取规则,重复读取前请先 clone()。
const text = await requests.get('/api/message', {responseType: 'text'}) // string
const blob = await requests.get('/api/file', {responseType: 'blob'}) // Blob
const data = await requests.get('/api/users', {responseType: 'json'}) // unknown
const textApi = requests.create({responseType: 'text'})
const message = await textApi.get('/api/message') // string
const raw = await textApi.get('/api/message', {responseType: 'response'}) // ResponseresponseType 支持 response(默认)、json、text、blob、arrayBuffer 和 formData。返回类型随配置推导;JSON 返回 unknown,请按接口约定校验或收窄类型。无效 JSON 保留原生解析错误。
显式传入 responseType 时返回对应类型;若配置变量中的 responseType 可省略,返回类型会包含实例默认类型。例如 RequestOptions<'text'> 传给默认实例时返回 string | Response。create()、extend() 和直接构造 RequestClient 同样会考虑配置缺省时的原生 Response 回退。
使用可省略 responseType 的配置变量创建实例时,建议使用 create() 保留精确推导;直接构造也可以显式写为 new RequestClient<'text' | 'response'>(options)。
| responseType | 返回值 |
| --- | --- |
| response | 原生 Response,可自行读取或处理流 |
| json | unknown,需要校验或收窄 |
| text | string |
| blob | Blob |
| arrayBuffer | ArrayBuffer |
| formData | FormData |
自动解析结果直接返回,没有 Axios 的 {data, status, headers} 包装。需要同时读取状态、响应头和响应体时,请保留默认的 responseType: 'response'。
错误、取消与超时
import requests, {HTTPError, TimeoutError, raiseForStatus} from '@tiyee/requests'
try {
const response = await requests.get('/api/users', {
throwHttpErrors: true,
timeout: 5000,
})
console.log(await response.json())
} catch (error) {
if (error instanceof HTTPError) {
console.log(error.response.status, await error.response.text())
} else if (error instanceof TimeoutError) {
console.log(error.timeout) // 毫秒
} else {
throw error
}
}
// 默认不会因 HTTP 状态码抛错,也可以手动检查
const response = await requests.get('/api/users')
raiseForStatus(response) // 4xx/5xx 抛出 HTTPError
const controller = new AbortController()
const pending = requests.get('/api/users', {signal: controller.signal, timeout: 5000})
controller.abort()
await pending.catch(error => {
if (error.name !== 'AbortError') throw error
})timeout 单位是毫秒,默认 0 表示不超时。默认返回原生响应时,超时计时到响应头返回为止;使用自动解析的 responseType 时,计时包含响应体读取。用户取消保留原生错误或自定义 signal.reason,不会被误报成 TimeoutError。网络错误也保留 fetch 原始异常。
配置选项
| 选项 | 默认值 | 说明 |
| --- | --- | --- |
| params | 空查询 | 对象、查询字符串或 URLSearchParams;数组编码为重复键,null/undefined 忽略 |
| headers | 空请求头 | 原生 HeadersInit,包括对象、Headers 和键值对数组 |
| data | 无请求体 | BodyInit、对象或数组;对象/数组的编码由 requestType 决定 |
| json | 无 JSON 请求体 | JSON 可序列化值;自动设置 Content-Type,与 data 互斥 |
| requestType | json | data 中对象/数组的编码方式;form 表示 URL 编码表单 |
| responseType | response | 返回原生 Response,或按指定类型自动解析 |
| timeout | 0 | 毫秒;0 禁用超时,允许范围为 0~2147483647 |
| signal | 无 | 外部 AbortSignal,可与 timeout 同时使用 |
| throwHttpErrors | false | 为 true 时,4xx/5xx 抛出带原生 response 的 HTTPError |
| prefix | 空字符串 | 在 URL 前直接拼接 |
| suffix | 空字符串 | 在 URL 后、查询参数前直接拼接 |
| credentials | same-origin | fetch 的凭据策略,也支持 omit / include |
| mode | 由 fetch 决定 | fetch 的跨域策略;mod 是兼容旧名,mode 优先 |
| redirect | follow | fetch 的 follow / manual / error |
| method | 由调用方法确定 | get/post 等方法和 request 的首参决定方法;拦截器可修改 config.method |
浏览器 Cookie 和连接复用交给 fetch;实例用于复用配置,不实现独立 Cookie jar。浏览器的跨域和重定向可见性遵循 fetch 规则。
拦截器
提供与 Axios 拦截器 相似的 use(onFulfilled, onRejected)、eject(id) 和 clear()。默认导出的 requests 和每个 create/extend 实例各自维护拦截器,不会相互继承。
import requests, {HTTPError} from '@tiyee/requests'
const api = requests.create({prefix: '/api', throwHttpErrors: true})
// 请求:拿到合并后的配置,headers 为原生 Headers
const requestId = api.interceptors.request.use(async config => {
config.headers.set('Authorization', 'Bearer token')
return config
}, error => Promise.reject(error))
// 响应:拿到最终请求结果,或在失败分支统一处理错误
const responseId = api.interceptors.response.use(result => {
if (result instanceof Response) console.log(result.status)
return result
}, error => {
if (error instanceof HTTPError && error.response.status === 401) {
console.log('登录已失效')
}
return Promise.reject(error)
})
await api.get('/users', {page: 1})
api.interceptors.request.eject(requestId)
api.interceptors.response.eject(responseId)
api.interceptors.request.clear()
api.interceptors.response.clear()请求拦截器按注册顺序逆序执行,响应拦截器按注册顺序执行。支持异步处理,返回值传给下一个拦截器;必须返回配置或结果才能继续保留它们。抛错或返回 rejected Promise 后,会进入后续拦截器的失败分支;同一次 use 的 onRejected 不捕获其配对 onFulfilled 的错误。失败分支返回正常值可恢复 Promise 链。
请求拦截器可修改 url、method、params、data、json、headers 和其他配置,在发送请求之前执行。响应拦截器在状态检查和自动解析之后执行:默认拿到原生 Response,配置 responseType: 'json' 时拿到解析后的 JSON。类型为 unknown,使用前请收窄。网络、超时、取消、配置校验、解析以及请求拦截器错误都会进入响应失败分支。
HTTP 状态错误默认进入响应成功分支;设置 throwHttpErrors: true 后,4xx/5xx 才进入失败分支。拦截器如果改变返回值类型或 responseType,调用方需要同步调整类型约定;方法的 TypeScript 返回类型仍按调用时的 responseType 推导。
拦截器链在请求启动时确定,之后注册或移除仅影响新请求。超时从发送请求开始计算,包含自动解析,不包含请求或响应拦截器执行时间。
仅注册失败处理时,第一个参数可以传 null。例如为 JSON 请求提供符合接口类型的降级结果:
import {create, HTTPError} from '@tiyee/requests'
const api = create({prefix: '/api', responseType: 'json', throwHttpErrors: true})
api.interceptors.response.use(null, error => {
if (error instanceof HTTPError && error.response.status === 503) {
return {items: [], unavailable: true}
}
return Promise.reject(error)
})
const result = await api.get('/users') // unknown,仍需按接口约定收窄默认返回原生 Response 时,若拦截器需要读取内容但仍要交给调用方读取,请使用 await result.clone().json(),并返回原来的 result。调用方在请求返回后自行读取响应体所产生的错误,不会再进入已执行完毕的响应拦截器;需要统一处理解析错误时,请使用自动解析的 responseType。
与旧版的差异
- 保留默认原生 Response、毫秒超时、
extend、prefix、suffix、data的默认 JSON 编码和所有responseType。 - 新增
json、create、通用request、patch/head/options、throwHttpErrors和具名错误类型。 - 所有方法支持
params/data/json/headers独立位置参数,配置对象用法继续保留。 - 原
hooks已移除,请使用interceptors.response.use;请求准备逻辑使用interceptors.request.use。响应拦截器处理自动解析后的结果,迁移时请按 responseType 调整读取方式。 - 查询参数数组改为标准重复键(
tag=a&tag=b),不再使用tag[]=a&tag[]=b;复杂查询请自行序列化后传字符串或 URLSearchParams。 - 实例与请求的请求头、查询参数现在会合并;空查询不会添加多余的
?,参数会插入 URL hash 之前。 - TypeScript 的自动 JSON 解析结果从
any收紧为unknown;原生 fetch 运行环境要求明确为 Node.js 18.17+ 或现代浏览器。
开发验证
pnpm --filter @tiyee/requests test包括 ESM/CommonJS 构建、本地 HTTP 集成测试、返回类型推导检查,以及使用包导出入口的 Node16 模块模式 ESM/CommonJS 消费端编译检查。
