/** * 传 `null` 表示"库不做解析":`data` 是 fetch 原生的 `Response`,body 尚未被读取。 * 这个逃生口不能是 `undefined` —— 不写 `returnType` 就是 `'json'` 的默认值语义, * `undefined` 与"没写"在运行时是同一个东西。 */ type OnoFetchReturnType = 'text' | 'json' | 'blob' | 'arrayBuffer' | 'formData' | null; /** * 请求体。联合成员就是 `normalizeBody` 运行时真认识的形状,一一对应: * 数组与 `Record` 走 JSON、`URLSearchParams` 走 urlencoded、`Blob` / `string` 原样、 * `FormData` 交给运行时补 boundary。 */ type OnoFetchParams = Record | unknown[] | FormData | URLSearchParams | string | Blob | undefined; /** * 内部类型(不在公共面):实例上有语法糖的那五个高频动词。 * 它不描述"能发什么" —— 那由下面的 `OnoFetchMethod` 管,任意 token 都能发。 * HEAD / OPTIONS / `PROPFIND` 都走调用式,引擎对它们没有名单式的依赖。 */ type MethodType = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; /** * 调用式 `method` 可写的值:五个命名动词 + 任意自定义动词(WebDAV 的 `PROPFIND`、 * 私有协议的 `MGET` 等)。`| (string & {})` 是保留字面量补全的写法, * 单写 `string` 会让编辑器不再提示那五个。 * 大小写不敏感:合法方法 token 统一大写后再交给引擎,见 `normalizeMethod`。 */ type OnoFetchMethod = MethodType | (string & {}); /** * 查询参数,序列化后拼到 URL 上;与 method 无关,任意动词都生效。 * 值域越界(对象、函数……)在运行时是**抛错**而不是静默变成 `filter=[object Object]`: * 窄类型只挡得住 TS 项目,挡不住 JS 与 `as any`。 * 需要完全不同的口径(`ids[]=`、逗号拼接、按 key 排序签名),走 `paramsSerializer`。 */ type OnoFetchQuery = Record>; /** * 自定义查询串:拿到整个 `params` 对象,返回**不含 `?`** 的完整 query 串 * (`a=1&b=2`),是 `?` 开头还是 `&` 续接由库根据 url 里有没有 `?` 决定。 * 返回空串等于不发 query。serializer 自身抛错原样直通(与"连接失败抛底层错误"同口径)。 */ type OnoFetchParamsSerializer = (params: OnoFetchQuery) => string; /** * 替换掉底层 `fetch` 的实现。**只有实例级,没有请求级** —— 一个实例打哪条 transport * 走是配置,不是每次请求临场改的主意;留请求级那一格就等于留第二个真相。 * * 两条硬性要求,都是"库替你假设好了"的部分: * - **必须尊重 `init.signal`**。`timeout` 与外部 `signal` 都靠它断流,忽略它的实现 * 会让这两格管不到自己(promise 照样结算,请求还在后台跑)。 * - **必须返回真的 `Response`**(带 `headers` / `body` / `status`)。缺字段时库在读 * 响应的那一行抛原生错误,原样直通,不替它包装。 * * 它自己抛的错同样原样直通(与 `paramsSerializer` / `jsonReviver` 同口径),所以它会 * 出现在 `onError` 的第一桶里。 */ type OnoFetchFetchImpl = (input: RequestInfo | URL, init?: RequestInit) => Promise; /** * 请求钩子拿到的上下文。`url` 是 baseURL 拼接与 query 序列化**都算完之后**的最终 URL, * 钩子只读它(按路径决定带哪个 token),改不动它 —— * `api.buildURL()` 与真实请求"逐字符相等"那条契约靠的就是钩子不碰 URL 与 `params`。 */ interface OnoFetchRequestContext { method: OnoFetchMethod; url: string; headers?: HeadersInit; body?: OnoFetchParams; } /** * 发出前最后一刻改请求:现取 token、挂 trace-id、把业务体包成签名信封都写在这一格。 * 返回的对象**只有 `headers` / `body` 两格有话语权**,写 `url` / `params` 是编译错误; * 返回 `undefined` 等于不改。 * * 钩子产出的 headers 落的是**请求级**那一格,所以同名键仍按既有规则胜过实例级 * `config.headers`,而两者都没写时 `content-type` 才由 body 推断补上。 * 多个钩子按数组顺序串成链,前一个的产出是后一个的输入。 */ type OnoFetchRequestHook = (context: OnoFetchRequestContext) => { headers?: HeadersInit; body?: OnoFetchParams; } | Promise<{ headers?: HeadersInit; body?: OnoFetchParams; }>; /** * 拿到库组装好的整个外壳,返回新的外壳 —— 拆业务信封(`{ code, data, message }` 只把 * `.data` 交出去)与统一上报都挂在响应侧,因为 `status` 在外壳上、`code` 在 `data` 里, * 只给一半就判不了。 * * **必须返回值**:`res => { report(res) }` 这种只观察却忘了 `return res` 的写法抛 * `OnoFetchError`,而不是静默把 `data` 变成 `undefined`。要改 `data` 就 * `return { ...res, data: res.data.xxx }`。 * 钩子自身抛的错**原样直通**(与 `paramsSerializer` / `jsonReviver` 同口径), * 所以"业务 `code` 非 0 当失败"就写成 `throw` —— 那会让本应 resolve 的结果变成 reject, * 这是调用方在一处自己定的规则,库依旧不判 HTTP 状态(没有 `validateStatus` 那个开关)。 */ type OnoFetchResponseHook = (response: OnoFetchReturnData) => OnoFetchReturnData | Promise; /** * reject 通路上的集中出口:失败上报、埋点、把 `TypeError: fetch failed` 换成本项目的错误类。 * * 入参是**没有被包装过的 rejection 原值**,三族都可能在里面:底层直接抛的错误(没注入 * `fetchImpl` 时就是 `TypeError: fetch failed`,注入了就是那个实现抛的任何东西)、 * 库自己判定的 `OnoFetchError`(`ABORTED` / `TIMEOUT` / `TOO_LARGE` / `INVALID_QUERY`)、 * 以及调用方回调(`paramsSerializer` / `jsonReviver`)抛的那个类。 * **前两族在类型上不可分辨**(都是 `Error`,`instanceof OnoFetchError` 只分得出第二族, * 而错误对象不带 config),所以这一格只该用来上报,**不该用来判"要不要重试"** —— * "断网"和"你 header 写错了"在它手里长得一样。 * * 返回 `undefined` 等于不改(与 `onRequest`、请求级各字段同一口径),其余任何值都成为 * 新的 rejection 值,并作为后一个钩子的入参。所以 `onError: [report]` 这种只观察的写法 * 直接写就行,不必 `return error` —— 与响应钩子"忘了 return 必须响"是相反的口径, * 因为观察失败才是这一格的主用途,而观察成功不是。 * * 不给你上下文(`{ method, url }` 之类):本库的错误对象与实例都不带 config, * 钩子给了就等于在另一处破掉它,而且失败时根本没有 `status` / `headers` 可给。 * 钩子自己抛的错原样直通,会盖掉它正在上报的那个错 —— 上报逻辑请自己 `try/catch`。 */ type OnoFetchErrorHook = (error: unknown) => unknown; /** * 语法糖(`get` / `post` / ...)的 options。不含 `method` 与 `body`: * 前者由方法名固定、后者由 `post` / `put` / `patch` 的位置参数固定, * 这里能写就等于给用户留一个"编译通过、运行时被静默改回"的坑。 * 需要自己决定 method 或改用请求体,走调用式 `api(url, options)`。 */ interface OnoFetchOptions extends Omit { returnType?: OnoFetchReturnType; timeout?: number; headers?: HeadersInit; params?: OnoFetchQuery; paramsSerializer?: OnoFetchParamsSerializer; /** * 下载进度。每读到一个数据块回调一次,与 `content-length` 是否存在无关。 * - `progress`:0–1 的比值;拿不到总量(chunked 无 `content-length`, * 或响应带 `content-encoding` —— 那时它量的是压缩后的字节,与已解压的 * `loaded` 不可比)时为 `undefined`,**调用方要显式处理**。 * - `loaded`:已读的**解码后**字节数。 * - `total`:`content-length` 声明的总量,未知时 `undefined`(与 `progress` 同步)。 */ onProgress?: (progress: number | undefined, loaded: number, total: number | undefined) => void; /** 透传给 `JSON.parse` 的 reviver,只在 `returnType: 'json'` 时生效。 */ jsonReviver?: (key: string, value: unknown) => unknown; /** * 响应体体积上限(字节),**默认 0 = 不限**。超限时断流并 reject * `OnoFetchError(code: 'TOO_LARGE')`。量的是解码后的字节,所以 gzip 响应下 * 它会大于 `content-length`。`returnType: null` 的透传模式下不生效 * (库一个字节都不读,也就无从计量)。 */ maxResponseSize?: number; } /** 调用式的 options:没有方法名也没有 body 位,`method` / `body` 只能在这里表达。 */ interface OnoFetchCallOptions extends OnoFetchOptions { method?: OnoFetchMethod; body?: OnoFetchParams; } type OnoFetchGetFunction = (url: string, options?: OnoFetchOptions) => Promise>; type OnoFetchPostFunction = (url: string, params: OnoFetchParams, options?: OnoFetchOptions) => Promise>; /** 实例本身被当作函数调用时的那条通路:`api('/users', { method: 'POST', body })`。 */ type OnoFetchRequestFunction = (url: string, options?: OnoFetchCallOptions) => Promise>; /** * `buildURL` 的 options。只收影响 URL 的两个字段:`method` / `body` / `headers` * 都不参与拼接,留在这里就是"写了不生效"的坑。 */ interface OnoFetchBuildURLOptions { params?: OnoFetchQuery; paramsSerializer?: OnoFetchParamsSerializer; } /** * 只拼 URL、不发请求:baseURL 拼接 + query 序列化,与真实请求走同一次 * `joinURL` / `withQuery`,所以逐字符等于最终发出去的那个 URL。 * 返回值含完整 query,别把长效凭证写在 `params` 里再上报它。 */ type OnoFetchBuildURLFunction = (url: string, options?: OnoFetchBuildURLOptions) => string; interface OnoFetchConfig { baseURL?: string; timeout?: number; headers?: HeadersInit; returnType?: OnoFetchReturnType; paramsSerializer?: OnoFetchParamsSerializer; jsonReviver?: (key: string, value: unknown) => unknown; maxResponseSize?: number; /** * 换掉底层 `fetch` 的实现(mock、按域名分流的 transport、把请求交给宿主App 的桥)。 * **只有实例级**,写在请求级 options 里是编译错误。见 `OnoFetchFetchImpl` 那两条硬性要求。 */ fetchImpl?: OnoFetchFetchImpl; /** * 请求钩子数组,按顺序串行执行。**只有实例级,没有请求级** —— * 单次请求要特殊处理,调用点本来就能自己算 headers,那不叫新能力。 * 数组在创建实例时拷贝一份定死,之后再往原数组里 push 不影响这个实例 * (要一份带额外钩子的配置,派生一个新实例)。 */ onRequest?: OnoFetchRequestHook[]; /** 响应钩子数组,按顺序串行执行,前一个返回的外壳是后一个的输入。数组同上,创建期定死。 */ onResponse?: OnoFetchResponseHook[]; /** * 错误钩子数组,按顺序串行执行,前一个产出的值(没产出就是原值)是后一个的入参。 * 数组同上,创建期定死。 * * 跑的范围:所有 reject 通路,**包括一个字节都没发出去的**(`INVALID_QUERY`、 * `paramsSerializer` 抛错、已中止的 `signal`)。三种情况不跑: * 非 2xx(那是 resolve,库不判状态)、`onRequest` / `onResponse` 自己抛的错 * (否则钩子互相喂错,报错来源也没法分辨),以及响应钩子那条"忘了 return"的 * `UNKNOWN` —— 它是库自己判定的失败,跟 `INVALID_QUERY` 同族,所以照跑。 */ onError?: OnoFetchErrorHook[]; } /** * 实例 = 调用式 + 五个高频动词糖 + `buildURL` + `extend`。糖的意义是日常手感(`api.post(url, body)` 的 * 位置 body、`api.get(url, options)` 不接受 method / body),不承担"库支持哪些动词" * 的表达 —— `method` 已经放开成任意 token,`HEAD` / `OPTIONS` / `PROPFIND` 一律 * 走 `api(url, { method: 'HEAD' })`,多挂一个成员只会让人以为它俩有区别。 */ interface OnoFetchInstance extends OnoFetchRequestFunction { get: OnoFetchGetFunction; post: OnoFetchPostFunction; put: OnoFetchPostFunction; patch: OnoFetchPostFunction; delete: OnoFetchGetFunction; buildURL: OnoFetchBuildURLFunction; /** * 以这个实例当前的配置为底,叠一份新配置,返回一个**新的完整实例**; * 原实例一个字节都没变(所以它同时也是"给一组请求换 baseURL / 加一层钩子"的写法)。 * * 合并口径:**数组整体覆盖、不追加**(`onRequest` 给了就是换一批,想接续旧的请自己 * 把两批拼成一个数组再传进来);其余字段按"写了就替换、没写沿用",而 `undefined` * 才算没写 —— `returnType: null`、`timeout: 0` 都是写了,与请求级那几个字段同一格口径。 * `headers` 也是整体替换,不按键合并。 */ extend: (config?: OnoFetchConfig) => OnoFetchInstance; } interface OnoFetchReturnData { url: string; headers: Record; status: number; statusText: string; data: T; } /** * 只有这几类失败会被包成 `OnoFetchError`,其余一律原样抛出底层错误 —— 连接失败、 * 解析期抛错、`fetchImpl` 抛的错、以及调用方回调(`paramsSerializer` / `jsonReviver`) * 自己抛的错都在其列,包装会让 `e instanceof TypeError` 之类的老写法失效。 * - `ABORTED` / `TIMEOUT`:走同一个 `AbortController` 通路,靠 `timedOut` 区分。 * - `TOO_LARGE`:响应体超过 `maxResponseSize`,已断流。 * - `INVALID_QUERY`:`params` 里有序列化不了的成员,例如值是个对象。 * - `UNKNOWN`:`OnoFetchError` 的默认值,今天是"`onResponse` 钩子没返回外壳"这一格在用。 * * 钩子(`onRequest` / `onResponse` / `onError`)自身抛的错也在这条线以外 —— 原样直通,不包装。 */ type OnoFetchErrorCode = 'ABORTED' | 'TIMEOUT' | 'TOO_LARGE' | 'INVALID_QUERY' | 'UNKNOWN'; declare const createOnoFetch: (config?: OnoFetchConfig) => OnoFetchInstance; declare class OnoFetchError extends Error { readonly code: OnoFetchErrorCode; constructor(message: string, code?: OnoFetchErrorCode); } export { type OnoFetchBuildURLOptions, type OnoFetchCallOptions, type OnoFetchConfig, OnoFetchError, type OnoFetchErrorCode, type OnoFetchErrorHook, type OnoFetchInstance, type OnoFetchMethod, type OnoFetchOptions, type OnoFetchParams, type OnoFetchParamsSerializer, type OnoFetchQuery, type OnoFetchRequestContext, type OnoFetchRequestHook, type OnoFetchResponseHook, type OnoFetchReturnData, type OnoFetchReturnType, createOnoFetch };