@minson1994/ms-utils 1.0.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/LICENSE +21 -0
- package/README.md +696 -0
- package/dist/api/ApiConfig.d.ts +206 -0
- package/dist/api/ApiConfig.d.ts.map +1 -0
- package/dist/api/ApiError.d.ts +30 -0
- package/dist/api/ApiError.d.ts.map +1 -0
- package/dist/api/ApiRequest.d.ts +87 -0
- package/dist/api/ApiRequest.d.ts.map +1 -0
- package/dist/api/ApiResponse.d.ts +15 -0
- package/dist/api/ApiResponse.d.ts.map +1 -0
- package/dist/api/adapter/AjaxHttpAdapter.d.ts +22 -0
- package/dist/api/adapter/AjaxHttpAdapter.d.ts.map +1 -0
- package/dist/api/adapter/ApiAdapter.d.ts +56 -0
- package/dist/api/adapter/ApiAdapter.d.ts.map +1 -0
- package/dist/api/adapter/AxiosHttpAdapter.d.ts +43 -0
- package/dist/api/adapter/AxiosHttpAdapter.d.ts.map +1 -0
- package/dist/api/adapter/FetchHttpAdapter.d.ts +50 -0
- package/dist/api/adapter/FetchHttpAdapter.d.ts.map +1 -0
- package/dist/api/adapter/WxHttpAdapter.d.ts +33 -0
- package/dist/api/adapter/WxHttpAdapter.d.ts.map +1 -0
- package/dist/api/adapter/XhrHttpAdapter.d.ts +46 -0
- package/dist/api/adapter/XhrHttpAdapter.d.ts.map +1 -0
- package/dist/api/index.d.ts +22 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/interceptor/ApiCacheInterceptor.d.ts +29 -0
- package/dist/api/interceptor/ApiCacheInterceptor.d.ts.map +1 -0
- package/dist/api/interceptor/ApiInterceptor.d.ts +18 -0
- package/dist/api/interceptor/ApiInterceptor.d.ts.map +1 -0
- package/dist/api/interceptor/ApiStatusInterceptor.d.ts +49 -0
- package/dist/api/interceptor/ApiStatusInterceptor.d.ts.map +1 -0
- package/dist/api/interceptor/ApiUIInterceptor.d.ts +37 -0
- package/dist/api/interceptor/ApiUIInterceptor.d.ts.map +1 -0
- package/dist/axios.cjs +151 -0
- package/dist/axios.d.ts +6 -0
- package/dist/axios.d.ts.map +1 -0
- package/dist/axios.js +58 -0
- package/dist/browser.cjs +102 -0
- package/dist/browser.d.ts +5 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +75 -0
- package/dist/chunk-PV2X54HI.js +75 -0
- package/dist/index.cjs +1880 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1753 -0
- package/dist/utils/AlgorithmUtils.d.ts +98 -0
- package/dist/utils/AlgorithmUtils.d.ts.map +1 -0
- package/dist/utils/Concurrency.d.ts +28 -0
- package/dist/utils/Concurrency.d.ts.map +1 -0
- package/dist/utils/CookieUtils.d.ts +38 -0
- package/dist/utils/CookieUtils.d.ts.map +1 -0
- package/dist/utils/Hook.d.ts +50 -0
- package/dist/utils/Hook.d.ts.map +1 -0
- package/dist/utils/TaskQueue.d.ts +159 -0
- package/dist/utils/TaskQueue.d.ts.map +1 -0
- package/dist/utils/TimeUtils.d.ts +82 -0
- package/dist/utils/TimeUtils.d.ts.map +1 -0
- package/dist/utils/ValidateUtils.d.ts +139 -0
- package/dist/utils/ValidateUtils.d.ts.map +1 -0
- package/dist/wx.cjs +169 -0
- package/dist/wx.d.ts +6 -0
- package/dist/wx.d.ts.map +1 -0
- package/dist/wx.js +76 -0
- package/package.json +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
# @minson/ms-utils
|
|
2
|
+
|
|
3
|
+
一个实用型 TypeScript 工具库,封装 HTTP 请求、时间处理、加密/随机、数据验证、任务队列、并发去重、Promise Hook、Cookie 等常用能力。
|
|
4
|
+
|
|
5
|
+
- TypeScript 编写,自带 `.d.ts` 类型提示
|
|
6
|
+
- 同时支持 ESM / CommonJS
|
|
7
|
+
- 核心能力不绑定浏览器、Node、微信小程序、uniapp
|
|
8
|
+
- 浏览器、微信小程序、uniapp、axios 能力通过子路径按需导入
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @minson/ms-utils
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
如果要使用 axios 适配器,请在项目里自行安装:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install axios qs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 导入方式
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import {
|
|
26
|
+
$Use,
|
|
27
|
+
ApiConfig,
|
|
28
|
+
ApiRequest,
|
|
29
|
+
ApiStatusInterceptor,
|
|
30
|
+
Concurrency,
|
|
31
|
+
AlgorithmUtils,
|
|
32
|
+
Hook,
|
|
33
|
+
TaskQueue,
|
|
34
|
+
TimeUtils,
|
|
35
|
+
ValidateUtils,
|
|
36
|
+
} from "@minson/ms-utils";
|
|
37
|
+
|
|
38
|
+
import { AxiosHttpAdapter } from "@minson/ms-utils/axios";
|
|
39
|
+
import { CookieUtils } from "@minson/ms-utils/browser";
|
|
40
|
+
import { WxHttpAdapter } from "@minson/ms-utils/wx";
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
CommonJS:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const { TimeUtils, AlgorithmUtils } = require("@minson/ms-utils");
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 功能总览
|
|
50
|
+
|
|
51
|
+
| 能力 | 导入 | 说明 |
|
|
52
|
+
| ------------ | -------------------------- | ------------------------------------------------- |
|
|
53
|
+
| HTTP 请求 | `@minson/ms-utils` | 请求配置、拦截器、适配器编排 |
|
|
54
|
+
| axios 适配器 | `@minson/ms-utils/axios` | 用 axios 作为底层请求实现 |
|
|
55
|
+
| Cookie | `@minson/ms-utils/browser` | 浏览器 `document.cookie` 工具 |
|
|
56
|
+
| 小程序/uni 请求 | `@minson/ms-utils/wx` | 微信小程序 `wx.request` / uniapp `uni.request` 适配器 |
|
|
57
|
+
| 时间 | `TimeUtils` | 格式化、加减、差值、多久前、延迟 |
|
|
58
|
+
| 加密/随机 | `AlgorithmUtils` | Base64、MD5、AES、SHA256、HMAC、RSA、UUID、随机数 |
|
|
59
|
+
| 数据验证 | `ValidateUtils` | 空值、类型、邮箱、手机号、身份证、银行卡等 |
|
|
60
|
+
| 任务队列 | `TaskQueue` | 异步任务并发控制、超时、暂停恢复 |
|
|
61
|
+
| 并发去重 | `Concurrency` | 同 key 并发只执行一次 |
|
|
62
|
+
| Hook | `Hook` | 外部回调唤醒 Promise |
|
|
63
|
+
|
|
64
|
+
## HTTP 请求
|
|
65
|
+
|
|
66
|
+
### 基础请求
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import axios from "axios";
|
|
70
|
+
import qs from "qs";
|
|
71
|
+
import { $Use, ApiRequest } from "@minson/ms-utils";
|
|
72
|
+
import { AxiosHttpAdapter } from "@minson/ms-utils/axios";
|
|
73
|
+
|
|
74
|
+
const request = new ApiRequest({
|
|
75
|
+
url: ["https://api.example.com"],
|
|
76
|
+
adapter: new AxiosHttpAdapter(axios, qs),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await $Use({
|
|
80
|
+
url: "/user/info",
|
|
81
|
+
method: "GET",
|
|
82
|
+
data: { id: 1 },
|
|
83
|
+
})
|
|
84
|
+
.useJsonBody()
|
|
85
|
+
.useBackFailResult()
|
|
86
|
+
.request(request);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 绑定请求后执行
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
const result = await request
|
|
93
|
+
.use({
|
|
94
|
+
url: "/",
|
|
95
|
+
method: "GET",
|
|
96
|
+
})
|
|
97
|
+
.useJsonBody()
|
|
98
|
+
.useBackFailResult()
|
|
99
|
+
.emit();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 链式配置
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
const config = ApiConfig.use({ url: "/upload" })
|
|
106
|
+
.useAuth()
|
|
107
|
+
.useTimeout(10)
|
|
108
|
+
.useHeaders({ token: "xxx" })
|
|
109
|
+
.useWithCredentials()
|
|
110
|
+
.useUpload()
|
|
111
|
+
.useJsonBody()
|
|
112
|
+
.useSign()
|
|
113
|
+
.useLoading()
|
|
114
|
+
.useShowErrorMessage()
|
|
115
|
+
.useCheckHttpStatus()
|
|
116
|
+
.useBackFailResult()
|
|
117
|
+
.useBackOnlyData()
|
|
118
|
+
.useCache();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 状态拦截器
|
|
122
|
+
|
|
123
|
+
`ApiStatusInterceptor` 默认读取:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
{
|
|
127
|
+
code: 200,
|
|
128
|
+
message: 'ok',
|
|
129
|
+
data: {}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
可以按你的后端字段改:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const statusInterceptor = new ApiStatusInterceptor({
|
|
137
|
+
status: "code",
|
|
138
|
+
message: "message",
|
|
139
|
+
data: "data",
|
|
140
|
+
success: 200,
|
|
141
|
+
onUnauthorized(message) {
|
|
142
|
+
console.log("登录失效:", message);
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### HTTP API
|
|
148
|
+
|
|
149
|
+
| API | 说明 |
|
|
150
|
+
| ----------------------- | --------------------------------------- |
|
|
151
|
+
| `ApiRequest` | 请求编排器,串起 adapter 和 interceptor |
|
|
152
|
+
| `ApiConfig` | 单次请求配置,支持链式 `useXxx` |
|
|
153
|
+
| `$Use(options)` | `ApiConfig.use(options)` 的快捷入口 |
|
|
154
|
+
| `ApiGlobalConfig` | 全局默认配置 |
|
|
155
|
+
| `ApiResponse` | 统一响应对象 |
|
|
156
|
+
| `ApiAdapter` | 抽象适配器基类 |
|
|
157
|
+
| `ApiInterceptor` | 抽象拦截器基类 |
|
|
158
|
+
| `ApiStatusInterceptor` | HTTP/业务状态码处理 |
|
|
159
|
+
| `ApiCacheInterceptor` | 缓存拦截器 |
|
|
160
|
+
| `ApiUIInterceptor` | loading / error message UI 钩子 |
|
|
161
|
+
|
|
162
|
+
## TimeUtils 时间工具
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
TimeUtils.now();
|
|
166
|
+
TimeUtils.timestamp();
|
|
167
|
+
TimeUtils.timestamp(new Date(), "second");
|
|
168
|
+
TimeUtils.toDate("2026-06-08");
|
|
169
|
+
TimeUtils.isValid("2026-06-08");
|
|
170
|
+
TimeUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
|
|
171
|
+
TimeUtils.add(new Date(), 1, "day");
|
|
172
|
+
TimeUtils.subtract(new Date(), 1, "hour");
|
|
173
|
+
TimeUtils.diff("2026-06-08", "2026-06-10", "day");
|
|
174
|
+
TimeUtils.fromNow(Date.now() - 60 * 1000); // 1 分钟前
|
|
175
|
+
TimeUtils.startOfDay(new Date());
|
|
176
|
+
TimeUtils.endOfDay(new Date());
|
|
177
|
+
await TimeUtils.wait(1000);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| 方法 | 说明 |
|
|
181
|
+
| ------------------------------ | -------------------------------- |
|
|
182
|
+
| `now()` | 当前时间 `Date` |
|
|
183
|
+
| `timestamp(date?, unit?)` | 毫秒/秒时间戳 |
|
|
184
|
+
| `toDate(value)` | 转成 `Date` |
|
|
185
|
+
| `isValid(value)` | 是否是有效时间 |
|
|
186
|
+
| `format(date?, pattern?)` | 格式化时间 |
|
|
187
|
+
| `add(date, amount, unit)` | 增加时间 |
|
|
188
|
+
| `subtract(date, amount, unit)` | 减少时间 |
|
|
189
|
+
| `diff(start, end, unit?)` | 计算差值 |
|
|
190
|
+
| `fromNow(date, base?)` | 输出“刚刚 / x 分钟前 / x 小时后” |
|
|
191
|
+
| `startOfDay(date?)` | 当天开始时间 |
|
|
192
|
+
| `endOfDay(date?)` | 当天结束时间 |
|
|
193
|
+
| `wait(ms)` | 延迟等待 |
|
|
194
|
+
|
|
195
|
+
## AlgorithmUtils 加密和随机工具
|
|
196
|
+
|
|
197
|
+
> Base64、MD5、AES、SHA256、HMAC、UUID、随机数基于 `crypto-js` 和运行时随机源;RSA 使用 WebCrypto。老环境没有 `crypto.subtle` 时,RSA 不可用。
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
AlgorithmUtils.base64Encode("hello");
|
|
201
|
+
AlgorithmUtils.base64Decode("aGVsbG8=");
|
|
202
|
+
|
|
203
|
+
AlgorithmUtils.md5("hello");
|
|
204
|
+
AlgorithmUtils.md5("hello", 16, true);
|
|
205
|
+
AlgorithmUtils.md5_16("hello");
|
|
206
|
+
AlgorithmUtils.md5_32("hello");
|
|
207
|
+
|
|
208
|
+
const encrypted = AlgorithmUtils.aesEncrypt("content", "password");
|
|
209
|
+
const decrypted = AlgorithmUtils.aesDecrypt(encrypted, "password");
|
|
210
|
+
|
|
211
|
+
AlgorithmUtils.sha256("hello");
|
|
212
|
+
AlgorithmUtils.hmac("hello", "secret", "SHA256");
|
|
213
|
+
AlgorithmUtils.hmacSha256("hello", "secret");
|
|
214
|
+
|
|
215
|
+
AlgorithmUtils.uuid();
|
|
216
|
+
AlgorithmUtils.randomInt(1, 100);
|
|
217
|
+
AlgorithmUtils.randomFloat();
|
|
218
|
+
AlgorithmUtils.randomString(16);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### AES 指定 iv
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
const ciphertext = AlgorithmUtils.aesEncrypt("content", {
|
|
225
|
+
key: "1234567890123456",
|
|
226
|
+
iv: "1234567890123456",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
AlgorithmUtils.aesDecrypt(ciphertext, {
|
|
230
|
+
key: "1234567890123456",
|
|
231
|
+
iv: "1234567890123456",
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### RSA
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
const keys = await AlgorithmUtils.rsaGeneratePemKeyPair();
|
|
239
|
+
const ciphertext = await AlgorithmUtils.rsaEncrypt("secret", keys.publicKey);
|
|
240
|
+
const plaintext = await AlgorithmUtils.rsaDecrypt(ciphertext, keys.privateKey);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
| 方法 | 说明 |
|
|
244
|
+
| -------------------------------------------------------------------- | -------------------------- |
|
|
245
|
+
| `base64Encode(text)` / `base64Decode(text)` | Base64 编解码 |
|
|
246
|
+
| `md5(text, length?, upper?)` | MD5,支持 16/32 位和大小写 |
|
|
247
|
+
| `md5_16(text, upper?)` / `md5_32(text, upper?)` | 快捷 MD5 |
|
|
248
|
+
| `aesEncrypt(text, options)` / `aesDecrypt(text, options)` | AES 加解密 |
|
|
249
|
+
| `sha256(text, upper?)` | SHA256 |
|
|
250
|
+
| `hmac(text, key, algorithm?, upper?)` | HMAC,支持 SHA256/SHA1/MD5 |
|
|
251
|
+
| `hmacSha256(text, key, upper?)` | HMAC-SHA256 |
|
|
252
|
+
| `uuid()` | UUID v4 |
|
|
253
|
+
| `randomInt(min, max)` | 指定范围整数 |
|
|
254
|
+
| `randomFloat()` | 0 到 1 的随机小数 |
|
|
255
|
+
| `randomString(length?, chars?)` | 随机字符串 |
|
|
256
|
+
| `rsaGenerateKeyPair()` / `rsaGeneratePemKeyPair()` | 生成 RSA 密钥对 |
|
|
257
|
+
| `rsaEncrypt(text, publicKey)` / `rsaDecrypt(ciphertext, privateKey)` | RSA-OAEP 加解密 |
|
|
258
|
+
|
|
259
|
+
## ValidateUtils 数据验证工具
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
ValidateUtils.isEmpty("");
|
|
263
|
+
ValidateUtils.isNotEmpty("abc");
|
|
264
|
+
ValidateUtils.isString("abc");
|
|
265
|
+
ValidateUtils.isNumber(123);
|
|
266
|
+
ValidateUtils.isInteger(1);
|
|
267
|
+
ValidateUtils.isBoolean(false);
|
|
268
|
+
ValidateUtils.isArray([]);
|
|
269
|
+
ValidateUtils.isPlainObject({});
|
|
270
|
+
|
|
271
|
+
ValidateUtils.lengthBetween("abc", 1, 10);
|
|
272
|
+
ValidateUtils.numberBetween(5, 1, 10);
|
|
273
|
+
ValidateUtils.match("abc", /^[a-z]+$/);
|
|
274
|
+
|
|
275
|
+
ValidateUtils.isEmail("a@b.com");
|
|
276
|
+
ValidateUtils.isMobileCN("13800138000");
|
|
277
|
+
ValidateUtils.isUrl("https://example.com");
|
|
278
|
+
ValidateUtils.isIPv4("127.0.0.1");
|
|
279
|
+
ValidateUtils.isIPv6("::1");
|
|
280
|
+
ValidateUtils.isJSON('{"a":1}');
|
|
281
|
+
ValidateUtils.isPostalCodeCN("528000");
|
|
282
|
+
ValidateUtils.isPlateNumberCN("粤A12345");
|
|
283
|
+
ValidateUtils.isBankCard("4111111111111111");
|
|
284
|
+
ValidateUtils.isIdCardCN("11010519491231002X");
|
|
285
|
+
ValidateUtils.validateIdCardCN("11010519491231002X");
|
|
286
|
+
ValidateUtils.passwordStrength("Abcdef12!@");
|
|
287
|
+
ValidateUtils.isStrongPassword("Abcdef12!@");
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
| 方法 | 说明 |
|
|
291
|
+
| --------------------------------------------------- | ------------------------------ |
|
|
292
|
+
| `isNil` / `isEmpty` / `isNotEmpty` | 空值判断 |
|
|
293
|
+
| `isString` / `isNumber` / `isInteger` / `isBoolean` | 基础类型判断 |
|
|
294
|
+
| `isArray` / `isPlainObject` | 数组和普通对象判断 |
|
|
295
|
+
| `lengthBetween` / `numberBetween` / `match` | 通用规则判断 |
|
|
296
|
+
| `isEmail` / `isMobileCN` / `isUrl` | 常用格式判断 |
|
|
297
|
+
| `isIPv4` / `isIPv6` / `isJSON` | 网络和 JSON 判断 |
|
|
298
|
+
| `isPostalCodeCN` / `isPlateNumberCN` | 国内邮编、车牌判断 |
|
|
299
|
+
| `isBankCard` | 银行卡 Luhn 校验 |
|
|
300
|
+
| `isIdCardCN` / `validateIdCardCN` | 身份证校验,支持生日和性别返回 |
|
|
301
|
+
| `passwordStrength` / `isStrongPassword` | 密码强度判断 |
|
|
302
|
+
|
|
303
|
+
## TaskQueue 任务队列
|
|
304
|
+
|
|
305
|
+
用于限制异步任务并发,支持超时、任务间隔、暂停、恢复、停止和等待空闲。
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
const queue = new TaskQueue({
|
|
309
|
+
concurrency: 1,
|
|
310
|
+
taskDelayMs: 100,
|
|
311
|
+
timeout: 0,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
queue.add(async () => {
|
|
315
|
+
await TimeUtils.wait(1000);
|
|
316
|
+
return "done";
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const results = await queue.addMany([() => 1, async () => 2]);
|
|
320
|
+
|
|
321
|
+
queue.pause();
|
|
322
|
+
queue.resume();
|
|
323
|
+
await queue.waitForIdle();
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
| 方法/属性 | 说明 |
|
|
327
|
+
| ----------------------------- | -------------------- |
|
|
328
|
+
| `add(task, timeout?)` | 添加单个任务 |
|
|
329
|
+
| `addMany(tasks)` | 批量添加任务 |
|
|
330
|
+
| `pause()` / `resume()` | 暂停/恢复队列 |
|
|
331
|
+
| `clear(reason?)` | 清空等待队列 |
|
|
332
|
+
| `stop(reason?)` | 停止队列并拒绝新任务 |
|
|
333
|
+
| `reset()` | 重置队列状态 |
|
|
334
|
+
| `waitForIdle()` | 等待队列空闲 |
|
|
335
|
+
| `setConcurrency(n)` | 修改并发数 |
|
|
336
|
+
| `setTimeout(ms)` | 修改默认超时 |
|
|
337
|
+
| `setTaskDelay(ms)` | 修改任务间隔 |
|
|
338
|
+
| `size` / `activeCount` | 等待数 / 运行数 |
|
|
339
|
+
| `TaskQueue.delay(ms, value?)` | 静态延迟工具 |
|
|
340
|
+
|
|
341
|
+
## Concurrency 并发去重
|
|
342
|
+
|
|
343
|
+
同名同参数的并发调用只执行一次,后续调用复用同一个 Promise。
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
const loadUserOnce = Concurrency.createConcurrentCall(
|
|
347
|
+
async (id) => fetchUser(id),
|
|
348
|
+
"load-user",
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
await Promise.all([loadUserOnce(1), loadUserOnce(1)]);
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
| 方法 | 说明 |
|
|
355
|
+
| ----------------------------------------- | ---------------------- |
|
|
356
|
+
| `createConcurrentCall(fn, name, getKey?)` | 创建并发去重函数 |
|
|
357
|
+
| `pendingRequests` | 当前正在执行的请求 Map |
|
|
358
|
+
|
|
359
|
+
## Hook 回调工具
|
|
360
|
+
|
|
361
|
+
适合“先发起动作,后由外部回调唤醒”的场景,比如扫码、第三方 SDK 回调、`postMessage`。
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
const hook = new Hook();
|
|
365
|
+
const promise = Hook.on(hook);
|
|
366
|
+
|
|
367
|
+
setTimeout(() => {
|
|
368
|
+
hook.call("ok");
|
|
369
|
+
}, 1000);
|
|
370
|
+
|
|
371
|
+
const result = await promise;
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
自动生成编号:
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
const result = await Hook.no(async (no) => {
|
|
378
|
+
window.postMessage({ no });
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
| 方法/属性 | 说明 |
|
|
383
|
+
| ------------------------------------------- | ------------------------- |
|
|
384
|
+
| `new Hook(no?, timeout?)` | 创建 Hook,timeout 单位秒 |
|
|
385
|
+
| `Hook.on(hook)` | 注册等待 |
|
|
386
|
+
| `Hook.call(hook, data)` / `hook.call(data)` | 唤醒等待 |
|
|
387
|
+
| `Hook.clear(hook)` | 清理 Hook |
|
|
388
|
+
| `Hook.no(fn, timeout?)` | 自动生成编号并等待回调 |
|
|
389
|
+
| `hook.no` / `hook.timeout` | 编号 / 超时时间 |
|
|
390
|
+
|
|
391
|
+
## CookieUtils 浏览器 Cookie 工具
|
|
392
|
+
|
|
393
|
+
`CookieUtils` 依赖浏览器 `document.cookie`,请从 browser 子路径导入,避免 Node 环境误加载浏览器对象。
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
import { CookieUtils } from "@minson/ms-utils/browser";
|
|
397
|
+
|
|
398
|
+
CookieUtils.write("token", "xxx");
|
|
399
|
+
CookieUtils.read("token");
|
|
400
|
+
CookieUtils.has("token");
|
|
401
|
+
CookieUtils.delete("token");
|
|
402
|
+
CookieUtils.writeJSON("user", { id: 1 });
|
|
403
|
+
CookieUtils.readJSON("user");
|
|
404
|
+
CookieUtils.readAll();
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## WxHttpAdapter 微信小程序 / uniapp 适配器
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
import { ApiRequest } from "@minson/ms-utils";
|
|
411
|
+
import { WxHttpAdapter } from "@minson/ms-utils/wx";
|
|
412
|
+
|
|
413
|
+
const request = new ApiRequest({
|
|
414
|
+
url: "https://api.example.com",
|
|
415
|
+
adapter: new WxHttpAdapter(wx), // uniapp 项目传 uni
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## 本地编译并安装到 test 项目
|
|
420
|
+
|
|
421
|
+
项目内已经提供脚本,用来验证真实 npm 包安装效果:
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
cd main
|
|
425
|
+
npm run build:test
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
该命令会自动:
|
|
429
|
+
|
|
430
|
+
1. 编译 `main/dist`
|
|
431
|
+
2. 打包 `minson-ms-utils-*.tgz`
|
|
432
|
+
3. 安装到 `../test`
|
|
433
|
+
|
|
434
|
+
然后可以运行:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
cd ../test
|
|
438
|
+
node test.js
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## 发布前检查
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
cd main
|
|
445
|
+
npm run typecheck
|
|
446
|
+
npm run test
|
|
447
|
+
npm run build
|
|
448
|
+
npm pack --dry-run
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
`npm pack --dry-run` 应只看到 `dist/`、`README.md`、`LICENSE` 和 package metadata。
|
|
452
|
+
|
|
453
|
+
## API 工具设计说明
|
|
454
|
+
|
|
455
|
+
这个模块不是为了替代 axios、ky、ofetch 这类底层 HTTP client。
|
|
456
|
+
它解决的是另一个痛点:在 Web、Node、微信小程序、uniapp 等环境里,尽量复用同一份业务 API 接口代码。
|
|
457
|
+
|
|
458
|
+
核心请求模块只暴露 `ApiRequest`、`ApiConfig`、`ApiResponse`、`ApiAdapter`、`ApiInterceptor` 等 `Api*` 命名;底层适配器仍保留 `FetchHttpAdapter`、`XhrHttpAdapter`、`AxiosHttpAdapter` 这类名称,表示它们负责对接具体 HTTP 实现。
|
|
459
|
+
|
|
460
|
+
### 整体分层
|
|
461
|
+
|
|
462
|
+
| 层级 | 类型 | 职责 |
|
|
463
|
+
| --- | --- | --- |
|
|
464
|
+
| API 编排器 | `ApiRequest` | 串起配置、适配器、拦截器、缓存、错误处理和多 base url 重试 |
|
|
465
|
+
| 单次配置 | `ApiConfig` | 描述一次请求,并提供链式 `useXxx()` 能力 |
|
|
466
|
+
| 响应对象 | `ApiResponse` | 统一包装 status、data、headers、原始响应 |
|
|
467
|
+
| 适配器 | `ApiAdapter` | 抽象底层请求能力,不绑定 axios、fetch、xhr、wx |
|
|
468
|
+
| 拦截器 | `ApiInterceptor` | 抽象请求前后处理,业务可继承扩展 |
|
|
469
|
+
|
|
470
|
+
### 适配器
|
|
471
|
+
|
|
472
|
+
适配器负责把同一份 `ApiConfig` 转成不同运行时的请求调用。
|
|
473
|
+
|
|
474
|
+
| 适配器 | 入口 | 适用环境 | 说明 |
|
|
475
|
+
| --- | --- | --- | --- |
|
|
476
|
+
| `XhrHttpAdapter` | `@minson/ms-utils` | 浏览器 | 支持上传进度、下载进度、取消请求 |
|
|
477
|
+
| `FetchHttpAdapter` | `@minson/ms-utils` | Node / 浏览器 / Worker | 通用 fetch 实现,支持取消;下载进度依赖 ReadableStream |
|
|
478
|
+
| `AxiosHttpAdapter` | `@minson/ms-utils/axios` | 使用 axios 的项目 | axios 构造注入,不内置打包 axios |
|
|
479
|
+
| `WxHttpAdapter` | `@minson/ms-utils/wx` | 微信小程序 / uniapp | 适配 `wx.request`、`wx.uploadFile` 或 `uni.request`、`uni.uploadFile` |
|
|
480
|
+
| `AjaxHttpAdapter` | 源码路径按需引用 | jQuery/uni 风格 ajax | 适合已有 ajax runtime 的老项目 |
|
|
481
|
+
|
|
482
|
+
默认适配器策略:
|
|
483
|
+
|
|
484
|
+
```js
|
|
485
|
+
// 浏览器优先 XHR,方便拿上传/下载进度;其它环境走 fetch。
|
|
486
|
+
const api = new ApiRequest({
|
|
487
|
+
url: "https://api.example.com",
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
等价于:
|
|
492
|
+
|
|
493
|
+
```js
|
|
494
|
+
const adapter = typeof XMLHttpRequest !== "undefined"
|
|
495
|
+
? new XhrHttpAdapter()
|
|
496
|
+
: new FetchHttpAdapter();
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### 容灾与故障切换
|
|
500
|
+
|
|
501
|
+
`ApiRequest` 的 `url` 支持数组。底层请求失败时,会按数组顺序切换到下一个 base url 重试。
|
|
502
|
+
|
|
503
|
+
```js
|
|
504
|
+
const api = new ApiRequest({
|
|
505
|
+
url: [
|
|
506
|
+
"https://api-a.example.com",
|
|
507
|
+
"https://api-b.example.com",
|
|
508
|
+
"https://api-c.example.com",
|
|
509
|
+
],
|
|
510
|
+
retryDelay: 1000,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const user = await api
|
|
514
|
+
.use({
|
|
515
|
+
url: "/user/info",
|
|
516
|
+
method: "GET",
|
|
517
|
+
data: { id: 1 },
|
|
518
|
+
})
|
|
519
|
+
.emit();
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
适用场景:
|
|
523
|
+
|
|
524
|
+
- 主备域名切换。
|
|
525
|
+
- 多网关容灾。
|
|
526
|
+
- 灰度网关失败回退。
|
|
527
|
+
- 小程序 / uniapp 不同环境域名兜底。
|
|
528
|
+
|
|
529
|
+
边界说明:
|
|
530
|
+
|
|
531
|
+
- `retryDelay` 单位是毫秒,默认 `1000`。
|
|
532
|
+
- 只在 adapter 层请求失败时切换,例如网络错误、超时、取消外的 reject。
|
|
533
|
+
- 业务状态失败不会自动切换域名,例如 `{ code: 500 }` 这种由 `ApiStatusInterceptor` 处理。
|
|
534
|
+
- 所有 base url 都失败后,抛出最后一次错误。
|
|
535
|
+
|
|
536
|
+
### 默认拦截器
|
|
537
|
+
|
|
538
|
+
`ApiRequest` 内置了几类默认拦截器,常规业务不用每个接口重复写。
|
|
539
|
+
|
|
540
|
+
| 拦截器 | 默认类型 | 职责 |
|
|
541
|
+
| --- | --- | --- |
|
|
542
|
+
| UI 拦截器 | `ApiUIInterceptor` | 根据 `loading`、`showErrorMessage` 统一处理 loading 和错误提示钩子 |
|
|
543
|
+
| 状态拦截器 | `ApiStatusInterceptor` | 统一处理 HTTP 状态、业务 code、message、data 拆包、未授权回调 |
|
|
544
|
+
| 缓存拦截器 | `ApiCacheInterceptor` | 根据 `cache` 配置拦截重复请求结果 |
|
|
545
|
+
| 业务拦截器 | `ApiInterceptor[]` | 项目自定义鉴权、签名、埋点、灰度、错误结构处理 |
|
|
546
|
+
|
|
547
|
+
如果后端响应结构简单,例如:
|
|
548
|
+
|
|
549
|
+
```js
|
|
550
|
+
{
|
|
551
|
+
code: 200,
|
|
552
|
+
message: "ok",
|
|
553
|
+
data: {}
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
可以直接配置 `ApiStatusInterceptor`:
|
|
558
|
+
|
|
559
|
+
```js
|
|
560
|
+
import { ApiRequest, ApiStatusInterceptor } from "@minson/ms-utils";
|
|
561
|
+
|
|
562
|
+
const api = new ApiRequest({
|
|
563
|
+
url: "https://api.example.com",
|
|
564
|
+
statusInterceptor: new ApiStatusInterceptor({
|
|
565
|
+
status: "code",
|
|
566
|
+
message: "message",
|
|
567
|
+
data: "data",
|
|
568
|
+
success: 200,
|
|
569
|
+
onUnauthorized: (message) => {
|
|
570
|
+
console.log("登录失效", message);
|
|
571
|
+
},
|
|
572
|
+
}),
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### 单次请求功能封装
|
|
577
|
+
|
|
578
|
+
`ApiConfig` 把常见请求能力封成链式方法,接口文件只描述业务参数,不散落环境判断。
|
|
579
|
+
|
|
580
|
+
| 方法 | 说明 |
|
|
581
|
+
| --- | --- |
|
|
582
|
+
| `useAuth()` | 标记需要登录态,给自定义拦截器使用 |
|
|
583
|
+
| `useTimeout(seconds)` | 设置超时时间 |
|
|
584
|
+
| `useHeaders(headers)` | 设置请求头 |
|
|
585
|
+
| `useWithCredentials()` | 携带 cookie / credentials |
|
|
586
|
+
| `useUpload()` | 标记上传请求,适配器会转 `FormData` 或平台上传 API |
|
|
587
|
+
| `useJsonBody()` | 按 JSON body 发送 |
|
|
588
|
+
| `useSign()` | 标记需要签名,给自定义签名拦截器使用 |
|
|
589
|
+
| `useLoading()` | 触发 UI loading 钩子 |
|
|
590
|
+
| `useShowErrorMessage()` | 控制是否展示错误消息 |
|
|
591
|
+
| `useCheckHttpStatus()` | 检查 HTTP status |
|
|
592
|
+
| `useBackFailResult()` | 业务失败时返回原始失败结果 |
|
|
593
|
+
| `useBackOnlyData()` | 成功时只返回业务 data |
|
|
594
|
+
| `useCache()` | 开启缓存拦截 |
|
|
595
|
+
| `useCancel(callback)` | 获取取消函数,外部可主动取消请求 |
|
|
596
|
+
| `useUploadProgress(callback)` | 上传进度回调,适配器支持多少给多少 |
|
|
597
|
+
| `useDownloadProgress(callback)` | 下载进度回调,适配器支持多少给多少 |
|
|
598
|
+
| `useProgress(callback)` | 同时绑定上传和下载进度 |
|
|
599
|
+
| `request(api)` | 使用指定 `ApiRequest` 执行当前配置 |
|
|
600
|
+
| `emit()` / `exec()` | 使用绑定的 `ApiRequest` 执行当前配置 |
|
|
601
|
+
|
|
602
|
+
示例:
|
|
603
|
+
|
|
604
|
+
```js
|
|
605
|
+
const result = await api
|
|
606
|
+
.use({
|
|
607
|
+
url: "/user/create",
|
|
608
|
+
method: "POST",
|
|
609
|
+
data: { name: "minson" },
|
|
610
|
+
})
|
|
611
|
+
.useAuth()
|
|
612
|
+
.useJsonBody()
|
|
613
|
+
.useLoading()
|
|
614
|
+
.useCancel((cancel) => {
|
|
615
|
+
// 需要取消时调用 cancel("用户取消")
|
|
616
|
+
})
|
|
617
|
+
.useProgress((progress) => {
|
|
618
|
+
console.log(progress.percent);
|
|
619
|
+
})
|
|
620
|
+
.emit();
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### 推荐的业务 API 写法
|
|
624
|
+
|
|
625
|
+
把运行时选择放在初始化处,把业务接口写成纯函数。
|
|
626
|
+
|
|
627
|
+
```js
|
|
628
|
+
import { ApiRequest } from "@minson/ms-utils";
|
|
629
|
+
|
|
630
|
+
// 返回示例:{ id: 1, name: "minson" }
|
|
631
|
+
const api = new ApiRequest({
|
|
632
|
+
url: "https://api.example.com",
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
export function getUser(id) {
|
|
636
|
+
return api
|
|
637
|
+
.use({
|
|
638
|
+
url: "/user/info",
|
|
639
|
+
method: "GET",
|
|
640
|
+
data: { id },
|
|
641
|
+
})
|
|
642
|
+
.useAuth()
|
|
643
|
+
.emit();
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### 复杂业务结构
|
|
648
|
+
|
|
649
|
+
复杂业务不需要把各种协议都塞进核心库里,直接继承 `ApiInterceptor`,重写 `before()` 或 `after()` 即可。
|
|
650
|
+
|
|
651
|
+
例如后端返回:
|
|
652
|
+
|
|
653
|
+
```js
|
|
654
|
+
{
|
|
655
|
+
meta: {
|
|
656
|
+
code: 0,
|
|
657
|
+
traceId: "trace-id",
|
|
658
|
+
},
|
|
659
|
+
payload: {},
|
|
660
|
+
error: {
|
|
661
|
+
message: "请求失败",
|
|
662
|
+
},
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
可以这样写业务状态拦截器:
|
|
667
|
+
|
|
668
|
+
```js
|
|
669
|
+
import {
|
|
670
|
+
ApiInterceptor,
|
|
671
|
+
ApiRequest,
|
|
672
|
+
} from "@minson/ms-utils";
|
|
673
|
+
|
|
674
|
+
class CustomStatusInterceptor extends ApiInterceptor {
|
|
675
|
+
async before(_config) {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async after(_config, response) {
|
|
680
|
+
const body = response.data;
|
|
681
|
+
|
|
682
|
+
if (body.meta.code !== 0) {
|
|
683
|
+
throw new Error(body.error?.message || "请求失败");
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
response.data = body.payload;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const api = new ApiRequest({
|
|
691
|
+
url: "https://api.example.com",
|
|
692
|
+
statusInterceptor: new CustomStatusInterceptor(),
|
|
693
|
+
});
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
这样业务 API 文件仍然只关心:路径、参数、返回类型;不同运行时只替换 adapter,不需要复制接口代码。
|