@i.un/api-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +8 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/README.zh-CN.md +275 -0
- package/dist/client.d.mts +41 -0
- package/dist/client.d.ts +41 -0
- package/dist/client.js +184 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +160 -0
- package/dist/client.mjs.map +1 -0
- package/package.json +51 -0
- package/src/client.ts +263 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 zhyswan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
English | [简体中文](./README.zh-CN.md)
|
|
2
|
+
|
|
3
|
+
# Api Client
|
|
4
|
+
|
|
5
|
+
A lightweight HTTP client built on top of `ofetch`, with:
|
|
6
|
+
|
|
7
|
+
- Automatic `Authorization` token injection
|
|
8
|
+
- Optional token refresh with single-flight behavior
|
|
9
|
+
- Unified success / error handling with configurable protocol
|
|
10
|
+
- Full TypeScript support
|
|
11
|
+
|
|
12
|
+
Works in browsers, Node, browser extensions, and more.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @i.un/api-client
|
|
20
|
+
# or
|
|
21
|
+
pnpm add @i.un/api-client
|
|
22
|
+
# or
|
|
23
|
+
yarn add @i.un/api-client
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> Package name: `@i.un/api-client`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { createApiClient, type TokenStorage } from "@i.un/api-client";
|
|
34
|
+
|
|
35
|
+
// 1. Provide a TokenStorage (where to read/store the token)
|
|
36
|
+
const tokenStorage: TokenStorage = {
|
|
37
|
+
async getAccessToken() {
|
|
38
|
+
return localStorage.getItem("access_token") || "";
|
|
39
|
+
},
|
|
40
|
+
async setAccessToken(token: string) {
|
|
41
|
+
localStorage.setItem("access_token", token);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// 2. Create client
|
|
46
|
+
const client = createApiClient({
|
|
47
|
+
baseURL: "https://api.example.com",
|
|
48
|
+
tokenStorage,
|
|
49
|
+
// Token refresh endpoint (default ApiResult protocol: { code, data: { access_token }, message })
|
|
50
|
+
refreshToken: "/auth/refresh",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 3. Use it
|
|
54
|
+
const { get, post, put, patch, del, request } = client;
|
|
55
|
+
|
|
56
|
+
// Example: GET
|
|
57
|
+
const user = await get<{ name: string }>("/user/profile");
|
|
58
|
+
|
|
59
|
+
// Example: POST
|
|
60
|
+
const updated = await post<{ ok: boolean }>("/user/profile", { name: "foo" });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Default Response Protocol
|
|
66
|
+
|
|
67
|
+
This library ships with a default response shape:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export interface ApiResult<T> {
|
|
71
|
+
code: number;
|
|
72
|
+
data: T;
|
|
73
|
+
message: string;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Default behavior:
|
|
78
|
+
|
|
79
|
+
- Your backend is expected to return `ApiResult<T>`.
|
|
80
|
+
- When calling `request/get/post/...`:
|
|
81
|
+
- If `code === 0`:
|
|
82
|
+
- By default, returns `data` (i.e. `T`).
|
|
83
|
+
- If `returnFullResponse: true` is passed, returns the full `ApiResult<T>`.
|
|
84
|
+
- If `code !== 0`:
|
|
85
|
+
- Throws an `ApiError` (custom error type, see below).
|
|
86
|
+
|
|
87
|
+
You can override this protocol via configuration (see "Advanced: custom unwrapping and error mapping").
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Error Type
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
export interface ApiError extends Error {
|
|
95
|
+
code: number; // business error code
|
|
96
|
+
data?: unknown; // backend data field
|
|
97
|
+
status?: number; // HTTP status code (for network-level errors)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const isApiError = (error: unknown): error is ApiError => {
|
|
101
|
+
return error instanceof Error && "code" in error;
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Usage example:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
try {
|
|
109
|
+
const data = await get("/some/api");
|
|
110
|
+
} catch (e) {
|
|
111
|
+
if (isApiError(e)) {
|
|
112
|
+
console.log("business error", e.code, e.message, e.data);
|
|
113
|
+
} else {
|
|
114
|
+
console.error("unexpected error", e);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Options: `CreateApiClientOptions`
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
export interface CreateApiClientOptions {
|
|
125
|
+
baseURL: string;
|
|
126
|
+
tokenStorage: TokenStorage;
|
|
127
|
+
|
|
128
|
+
// Optional automatic token refresh
|
|
129
|
+
refreshToken?: (() => Promise<string>) | string | false;
|
|
130
|
+
|
|
131
|
+
// How to detect an auth error (defaults to code === 401)
|
|
132
|
+
isAuthError?: (code: number) => boolean;
|
|
133
|
+
|
|
134
|
+
// Custom success unwrapping logic
|
|
135
|
+
unwrapResponse?<T>(result: unknown, returnFullResponse: boolean): T;
|
|
136
|
+
|
|
137
|
+
// Custom error mapping from response to Error
|
|
138
|
+
createErrorFromResult?(res: unknown): Error;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### `TokenStorage`
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
export interface TokenStorage {
|
|
146
|
+
getAccessToken: () => Promise<string> | string;
|
|
147
|
+
setAccessToken: (token: string) => Promise<void> | void;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
You decide where the token lives, e.g.:
|
|
152
|
+
|
|
153
|
+
- Browser: `localStorage` / `sessionStorage` / cookies
|
|
154
|
+
- Node: in-memory, Redis, database, etc.
|
|
155
|
+
- Browser extension: `chrome.storage`, cookies, etc.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Automatic Token Refresh
|
|
160
|
+
|
|
161
|
+
### Option 1: String endpoint (recommended)
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const client = createApiClient({
|
|
165
|
+
baseURL,
|
|
166
|
+
tokenStorage,
|
|
167
|
+
refreshToken: "/auth/refresh",
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Behavior:
|
|
172
|
+
|
|
173
|
+
- When a business response satisfies `isAuthError(code) === true` (401 by default):
|
|
174
|
+
- Calls `ofetch<ApiResult<{ access_token: string }>>(refreshToken, { baseURL, method: "POST" })`.
|
|
175
|
+
- If `code !== 0`, uses `createErrorFromResult` to throw an error.
|
|
176
|
+
- If `code === 0`, takes `data.access_token` as the new token and calls `tokenStorage.setAccessToken`.
|
|
177
|
+
- Then automatically retries the original request once with the new token.
|
|
178
|
+
- Single-flight:
|
|
179
|
+
- Uses an internal `refreshingPromise` to ensure only one refresh request is in-flight at a time.
|
|
180
|
+
- Concurrent 401s will wait for that promise and reuse the result.
|
|
181
|
+
|
|
182
|
+
### Option 2: Custom function
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const client = createApiClient({
|
|
186
|
+
baseURL,
|
|
187
|
+
tokenStorage,
|
|
188
|
+
refreshToken: async () => {
|
|
189
|
+
const res = await ofetch<{ accessToken: string }>("/auth/refresh", {
|
|
190
|
+
baseURL,
|
|
191
|
+
method: "POST",
|
|
192
|
+
});
|
|
193
|
+
return res.accessToken;
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
> It is recommended **not** to call the same auto-refreshing `request` inside `refreshToken`, to avoid possible recursion if the refresh endpoint itself returns an auth error.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Request Methods
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const { rawRequest, request, get, post, put, patch, del } = createApiClient(...);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- `rawRequest`: The underlying `$fetch` instance (from `ofetch.create`), returns raw results, no auto refresh or unwrapping.
|
|
209
|
+
- `request<T>(url, options?)`: Core method with token injection, refresh, unwrapping, and error throwing.
|
|
210
|
+
- `get<T>(url, params?, options?)`: `GET` with `params` mapped to `query`.
|
|
211
|
+
- `post/put/patch<T>(url, body?, options?)`: `body` defaults to `{}`.
|
|
212
|
+
- `del<T>(url, params?, options?)`: `DELETE` with `params` mapped to `query`.
|
|
213
|
+
|
|
214
|
+
Examples:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
// GET /users?id=1
|
|
218
|
+
const user = await get<User>("/users", { id: 1 });
|
|
219
|
+
|
|
220
|
+
// POST /users { name }
|
|
221
|
+
const created = await post<User>("/users", { name: "foo" });
|
|
222
|
+
|
|
223
|
+
// Direct request (more flexible)
|
|
224
|
+
const data = await request<User>("/users/1", {
|
|
225
|
+
method: "GET",
|
|
226
|
+
returnFullResponse: false, // true returns ApiResult<User>
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Advanced: Custom unwrapping and error mapping
|
|
233
|
+
|
|
234
|
+
If your backend does not follow the `{ code, data, message }` protocol, you can override it:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
const client = createApiClient({
|
|
238
|
+
baseURL,
|
|
239
|
+
tokenStorage,
|
|
240
|
+
refreshToken: false,
|
|
241
|
+
|
|
242
|
+
unwrapResponse<T>(result, returnFullResponse) {
|
|
243
|
+
// Example: backend returns { success, result, errorMsg }
|
|
244
|
+
if (result && typeof result === "object" && "success" in result) {
|
|
245
|
+
const body = result as any;
|
|
246
|
+
if (body.success) {
|
|
247
|
+
return returnFullResponse ? (body as T) : (body.result as T);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return result as T;
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
createErrorFromResult(res): Error {
|
|
254
|
+
const body = res as any;
|
|
255
|
+
const err = new Error(body.errorMsg || "Request failed");
|
|
256
|
+
// You can attach custom fields here, e.g. (err as any).code = body.code;
|
|
257
|
+
return err;
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Internally, the library:
|
|
263
|
+
|
|
264
|
+
- Calls `unwrapResponse` for success cases.
|
|
265
|
+
- Calls `createErrorFromResult` for business failures.
|
|
266
|
+
- Manages token injection, refresh, retry, etc.
|
|
267
|
+
|
|
268
|
+
The protocol details are entirely up to you.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Environment & Notes
|
|
273
|
+
|
|
274
|
+
- Built on `ofetch`, works in browsers, Node 18+, Nuxt, etc.
|
|
275
|
+
- Requires `fetch` / `Headers`:
|
|
276
|
+
- Browsers: built-in.
|
|
277
|
+
- Node 18+: built-in `fetch`.
|
|
278
|
+
- Older Node: you need to polyfill e.g. with `undici` or `cross-fetch`.
|
|
279
|
+
- The core client does **not** depend on `window` / `localStorage` / `chrome`, etc. Environment-specific logic should live in your `TokenStorage` (and higher-level wrappers if needed).
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Api Client
|
|
2
|
+
|
|
3
|
+
一个基于 `ofetch` 的轻量级 HTTP 客户端,内置:
|
|
4
|
+
|
|
5
|
+
- 自动携带 `Authorization` token
|
|
6
|
+
- 自动刷新 token(可选,串行防抖)
|
|
7
|
+
- 统一成功/失败返回结构(可配置协议)
|
|
8
|
+
- 完整 TypeScript 类型支持
|
|
9
|
+
|
|
10
|
+
适用于浏览器、Node、浏览器扩展等多种环境。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @i.un/api-client
|
|
18
|
+
# or
|
|
19
|
+
pnpm add @i.un/api-client
|
|
20
|
+
# or
|
|
21
|
+
yarn add @i.un/api-client
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> 包名:`@i.un/api-client`。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 快速上手
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createApiClient, type TokenStorage } from "@i.un/api-client";
|
|
32
|
+
|
|
33
|
+
// 1. 提供一个 TokenStorage(决定 token 从哪里来、存到哪里去)
|
|
34
|
+
const tokenStorage: TokenStorage = {
|
|
35
|
+
async getAccessToken() {
|
|
36
|
+
return localStorage.getItem("access_token") || "";
|
|
37
|
+
},
|
|
38
|
+
async setAccessToken(token: string) {
|
|
39
|
+
localStorage.setItem("access_token", token);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// 2. 创建 client
|
|
44
|
+
const client = createApiClient({
|
|
45
|
+
baseURL: "https://api.example.com",
|
|
46
|
+
tokenStorage,
|
|
47
|
+
// 刷新 token 的接口(默认 ApiResult 协议:{ code, data: { access_token }, message })
|
|
48
|
+
refreshToken: "/auth/refresh",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 3. 使用
|
|
52
|
+
const { get, post, put, patch, del, request } = client;
|
|
53
|
+
|
|
54
|
+
// 示例:GET
|
|
55
|
+
const user = await get<{ name: string }>("/user/profile");
|
|
56
|
+
|
|
57
|
+
// 示例:POST
|
|
58
|
+
const updated = await post<{ ok: boolean }>("/user/profile", { name: "foo" });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 默认响应协议
|
|
64
|
+
|
|
65
|
+
库内置了一个默认响应协议类型:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
export interface ApiResult<T> {
|
|
69
|
+
code: number;
|
|
70
|
+
data: T;
|
|
71
|
+
message: string;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
默认行为:
|
|
76
|
+
|
|
77
|
+
- 约定后端统一返回 `ApiResult<T>`。
|
|
78
|
+
- 调用 `request/get/post/...`:
|
|
79
|
+
- 当 `code === 0`:
|
|
80
|
+
- 默认返回 `data`(即 `T`)。
|
|
81
|
+
- 如果传了 `returnFullResponse: true`,则返回完整的 `ApiResult<T>`。
|
|
82
|
+
- 当 `code !== 0`:
|
|
83
|
+
- 抛出 `ApiError`(自定义错误类型,见下文)。
|
|
84
|
+
|
|
85
|
+
你可以通过配置项覆盖这套协议(见「高级:自定义解包和错误映射」)。
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 错误类型
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
export interface ApiError extends Error {
|
|
93
|
+
code: number; // 业务错误码
|
|
94
|
+
data?: unknown; // 后端 data 字段
|
|
95
|
+
status?: number; // HTTP 状态码(仅网络层错误时可能存在)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const isApiError = (error: unknown): error is ApiError => {
|
|
99
|
+
return error instanceof Error && "code" in error;
|
|
100
|
+
};
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
调用方使用示例:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
try {
|
|
107
|
+
const data = await get("/some/api");
|
|
108
|
+
} catch (e) {
|
|
109
|
+
if (isApiError(e)) {
|
|
110
|
+
console.log("business error", e.code, e.message, e.data);
|
|
111
|
+
} else {
|
|
112
|
+
console.error("unexpected error", e);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 配置项:`CreateApiClientOptions`
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
export interface CreateApiClientOptions {
|
|
123
|
+
baseURL: string;
|
|
124
|
+
tokenStorage: TokenStorage;
|
|
125
|
+
|
|
126
|
+
// 自动刷新 token(可选)
|
|
127
|
+
refreshToken?: (() => Promise<string>) | string | false;
|
|
128
|
+
|
|
129
|
+
// 识别“需要刷新 / 认证失败”的业务错误(可选,默认 code === 401)
|
|
130
|
+
isAuthError?: (code: number) => boolean;
|
|
131
|
+
|
|
132
|
+
// 自定义成功响应解包逻辑(可选)
|
|
133
|
+
unwrapResponse?<T>(result: unknown, returnFullResponse: boolean): T;
|
|
134
|
+
|
|
135
|
+
// 自定义失败响应映射为 Error 的逻辑(可选)
|
|
136
|
+
createErrorFromResult?(res: unknown): Error;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `TokenStorage`
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
export interface TokenStorage {
|
|
144
|
+
getAccessToken: () => Promise<string> | string;
|
|
145
|
+
setAccessToken: (token: string) => Promise<void> | void;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
你可以自由决定 token 存到哪里,比如:
|
|
150
|
+
|
|
151
|
+
- 浏览器:`localStorage` / `sessionStorage` / `cookie`
|
|
152
|
+
- Node:内存变量、Redis、数据库等
|
|
153
|
+
- 浏览器扩展:`chrome.storage`、`cookies` 等
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 自动刷新 Token
|
|
158
|
+
|
|
159
|
+
### 方式一:字符串 endpoint(推荐)
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const client = createApiClient({
|
|
163
|
+
baseURL,
|
|
164
|
+
tokenStorage,
|
|
165
|
+
refreshToken: "/auth/refresh",
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
内部行为:
|
|
170
|
+
|
|
171
|
+
- 业务请求返回 `isAuthError(code) === true`(默认 401)时:
|
|
172
|
+
- 调用 `ofetch<ApiResult<{ access_token: string }>>(refreshToken, { baseURL, method: "POST" })`。
|
|
173
|
+
- 如果 `code !== 0`,使用 `createErrorFromResult` 抛错。
|
|
174
|
+
- 如果 `code === 0`,从 `data.access_token` 取出新 token,并调用 `tokenStorage.setAccessToken` 存储。
|
|
175
|
+
- 然后自动使用新 token 重试原请求一次。
|
|
176
|
+
- 串行防抖:
|
|
177
|
+
- 使用内部的 `refreshingPromise` 确保同一时刻只有一个刷新请求在进行;
|
|
178
|
+
- 其它并发 401 请求会等待这次刷新完成,复用刷新结果。
|
|
179
|
+
|
|
180
|
+
### 方式二:自定义函数
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const client = createApiClient({
|
|
184
|
+
baseURL,
|
|
185
|
+
tokenStorage,
|
|
186
|
+
refreshToken: async () => {
|
|
187
|
+
const res = await ofetch<{ accessToken: string }>("/auth/refresh", {
|
|
188
|
+
baseURL,
|
|
189
|
+
method: "POST",
|
|
190
|
+
});
|
|
191
|
+
return res.accessToken;
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
> 建议在 `refreshToken` 内部不要再调用同一个带自动刷新的 `request`,以避免在刷新接口也返回认证错误时形成递归。
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 请求方法
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const { rawRequest, request, get, post, put, patch, del } = createApiClient(...);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- `rawRequest`: 底层的 `$fetch` 实例(来自 `ofetch.create`),按原样返回结果,不做自动刷新和解包。
|
|
207
|
+
- `request<T>(url, options?)`: 核心方法,自动携带 token / 自动刷新 / 解包 / 抛 `Error`。
|
|
208
|
+
- `get<T>(url, params?, options?)`: `GET` 请求,`params` 会被放到 `query`。
|
|
209
|
+
- `post/put/patch<T>(url, body?, options?)`: `body` 默认 `{}`。
|
|
210
|
+
- `del<T>(url, params?, options?)`: `DELETE` 请求,`params` 放到 `query`。
|
|
211
|
+
|
|
212
|
+
示例:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// GET /users?id=1
|
|
216
|
+
const user = await get<User>("/users", { id: 1 });
|
|
217
|
+
|
|
218
|
+
// POST /users { name }
|
|
219
|
+
const created = await post<User>("/users", { name: "foo" });
|
|
220
|
+
|
|
221
|
+
// 直接用 request(更通用)
|
|
222
|
+
const data = await request<User>("/users/1", {
|
|
223
|
+
method: "GET",
|
|
224
|
+
returnFullResponse: false, // true 时返回 ApiResult<User>
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 高级:自定义解包和错误映射
|
|
231
|
+
|
|
232
|
+
如果你的后端协议不是 `{ code, data, message }`,可以通过配置覆盖:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const client = createApiClient({
|
|
236
|
+
baseURL,
|
|
237
|
+
tokenStorage,
|
|
238
|
+
refreshToken: false,
|
|
239
|
+
|
|
240
|
+
unwrapResponse<T>(result, returnFullResponse) {
|
|
241
|
+
// 举例:后端协议是 { success, result, errorMsg }
|
|
242
|
+
if (result && typeof result === "object" && "success" in result) {
|
|
243
|
+
const body = result as any;
|
|
244
|
+
if (body.success) {
|
|
245
|
+
return returnFullResponse ? (body as T) : (body.result as T);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return result as T;
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
createErrorFromResult(res): Error {
|
|
252
|
+
const body = res as any;
|
|
253
|
+
const err = new Error(body.errorMsg || "Request failed");
|
|
254
|
+
// 可以按需挂一些自定义字段,比如 err.code / err.raw
|
|
255
|
+
return err;
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 环境支持与注意事项
|
|
263
|
+
|
|
264
|
+
- 依赖 `ofetch`,可在浏览器、Node 18+、Nuxt 等环境使用。
|
|
265
|
+
- 需要环境有 `fetch` / `Headers`:
|
|
266
|
+
- 浏览器:原生支持。
|
|
267
|
+
- Node 18+:内置 `fetch`。
|
|
268
|
+
- 更低版本 Node:需要自行 polyfill(例如 `undici` 或 `cross-fetch`)。
|
|
269
|
+
- `client.ts` 本身不依赖 `window`/`localStorage`/`chrome` 等对象,这些应在你实现 `TokenStorage` 时根据环境自行选择。
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { $Fetch, FetchOptions } from 'ofetch';
|
|
2
|
+
|
|
3
|
+
interface ApiResult<T> {
|
|
4
|
+
code: number;
|
|
5
|
+
data: T;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
interface ApiError extends Error {
|
|
9
|
+
code: number;
|
|
10
|
+
data?: unknown;
|
|
11
|
+
status?: number;
|
|
12
|
+
}
|
|
13
|
+
declare const isApiError: (error: unknown) => error is ApiError;
|
|
14
|
+
interface TokenStorage {
|
|
15
|
+
getAccessToken: () => Promise<string> | string;
|
|
16
|
+
setAccessToken: (token: string) => Promise<void> | void;
|
|
17
|
+
}
|
|
18
|
+
interface CreateApiClientOptions {
|
|
19
|
+
baseURL: string;
|
|
20
|
+
tokenStorage: TokenStorage;
|
|
21
|
+
refreshToken?: (() => Promise<string>) | string | false;
|
|
22
|
+
isAuthError?: (code: number) => boolean;
|
|
23
|
+
unwrapResponse?<T>(result: unknown, returnFullResponse: boolean): T;
|
|
24
|
+
createErrorFromResult?(res: unknown): Error;
|
|
25
|
+
}
|
|
26
|
+
type RequestOptions = FetchOptions<"json"> & {
|
|
27
|
+
returnFullResponse?: boolean;
|
|
28
|
+
};
|
|
29
|
+
declare function createApiClient(options: CreateApiClientOptions): {
|
|
30
|
+
rawRequest: $Fetch;
|
|
31
|
+
request: <T = unknown>(url: string, options?: RequestOptions & {
|
|
32
|
+
_retry?: boolean;
|
|
33
|
+
}) => Promise<T>;
|
|
34
|
+
get: <T = unknown>(url: string, params?: FetchOptions["query"], options?: RequestOptions) => Promise<T>;
|
|
35
|
+
post: <T = unknown>(url: string, body?: FetchOptions["body"], options?: RequestOptions) => Promise<T>;
|
|
36
|
+
put: <T = unknown>(url: string, body?: FetchOptions["body"], options?: RequestOptions) => Promise<T>;
|
|
37
|
+
patch: <T = unknown>(url: string, body?: FetchOptions["body"], options?: RequestOptions) => Promise<T>;
|
|
38
|
+
del: <T = unknown>(url: string, params?: FetchOptions["query"], options?: RequestOptions) => Promise<T>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { type ApiError, type ApiResult, type CreateApiClientOptions, type TokenStorage, createApiClient, isApiError };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { $Fetch, FetchOptions } from 'ofetch';
|
|
2
|
+
|
|
3
|
+
interface ApiResult<T> {
|
|
4
|
+
code: number;
|
|
5
|
+
data: T;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
interface ApiError extends Error {
|
|
9
|
+
code: number;
|
|
10
|
+
data?: unknown;
|
|
11
|
+
status?: number;
|
|
12
|
+
}
|
|
13
|
+
declare const isApiError: (error: unknown) => error is ApiError;
|
|
14
|
+
interface TokenStorage {
|
|
15
|
+
getAccessToken: () => Promise<string> | string;
|
|
16
|
+
setAccessToken: (token: string) => Promise<void> | void;
|
|
17
|
+
}
|
|
18
|
+
interface CreateApiClientOptions {
|
|
19
|
+
baseURL: string;
|
|
20
|
+
tokenStorage: TokenStorage;
|
|
21
|
+
refreshToken?: (() => Promise<string>) | string | false;
|
|
22
|
+
isAuthError?: (code: number) => boolean;
|
|
23
|
+
unwrapResponse?<T>(result: unknown, returnFullResponse: boolean): T;
|
|
24
|
+
createErrorFromResult?(res: unknown): Error;
|
|
25
|
+
}
|
|
26
|
+
type RequestOptions = FetchOptions<"json"> & {
|
|
27
|
+
returnFullResponse?: boolean;
|
|
28
|
+
};
|
|
29
|
+
declare function createApiClient(options: CreateApiClientOptions): {
|
|
30
|
+
rawRequest: $Fetch;
|
|
31
|
+
request: <T = unknown>(url: string, options?: RequestOptions & {
|
|
32
|
+
_retry?: boolean;
|
|
33
|
+
}) => Promise<T>;
|
|
34
|
+
get: <T = unknown>(url: string, params?: FetchOptions["query"], options?: RequestOptions) => Promise<T>;
|
|
35
|
+
post: <T = unknown>(url: string, body?: FetchOptions["body"], options?: RequestOptions) => Promise<T>;
|
|
36
|
+
put: <T = unknown>(url: string, body?: FetchOptions["body"], options?: RequestOptions) => Promise<T>;
|
|
37
|
+
patch: <T = unknown>(url: string, body?: FetchOptions["body"], options?: RequestOptions) => Promise<T>;
|
|
38
|
+
del: <T = unknown>(url: string, params?: FetchOptions["query"], options?: RequestOptions) => Promise<T>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { type ApiError, type ApiResult, type CreateApiClientOptions, type TokenStorage, createApiClient, isApiError };
|