@base-web-kits/base-tools-web 1.0.2-alpha.5 → 1.0.2

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.
@@ -1,385 +0,0 @@
1
- import {
2
- appendUrlParam,
3
- cloneDeep,
4
- getObjectValue,
5
- isPlainObject,
6
- pickBy,
7
- toDayjs,
8
- } from '../../ts';
9
- import { getAppConfig } from '../config';
10
- import type { AppLogInfo } from '../config';
11
- import type {
12
- ChunkCallback,
13
- RequestConfigBase,
14
- RequestData,
15
- RequestTask,
16
- ResponseData,
17
- } from './request.d';
18
-
19
- export * from './request.d';
20
-
21
- /** 请求缓存 */
22
- const requestCache = new Map<string, { res: unknown; expire: number }>();
23
-
24
- /**
25
- * 基础请求 (返回 Promise 和 Task 对象)
26
- * 基于 fetch API 封装,支持流式请求
27
- * @param config 请求配置
28
- * @returns Promise<T> & { task?: RequestTask }
29
- * @example
30
- * // 在入口文件完成配置 (确保请求失败有toast提示,登录过期能够触发重新登录,log有日志输出)
31
- * setBaseToolsConfig({
32
- * toast: ({ msg, status }) => (status === 'fail' ? message.error(msg) : message.success(msg)),
33
- * showLoading: () => message.loading('加载中...'),
34
- * hideLoading: () => message.destroy(),
35
- * toLogin: () => reLogin(),
36
- * log(level, data) {
37
- * if (data.name === 'request') {
38
- * sendLog('request', data); // 请求日志
39
- * } else if (level === 'error') {
40
- * sendLog('error', data); // 错误日志
41
- * } else {
42
- * sendLog('action', data); // 操作日志
43
- * }
44
- * },
45
- * });
46
- *
47
- * // 封装项目的基础请求
48
- * export function requestApi<T>(config: RequestConfig) {
49
- * return request<T>({
50
- * header: { token: 'xx', version: 'xx', tid: 'xx' }, // 会自动过滤空值
51
- * // responseInterceptor: (res) => res, // 响应拦截,可预处理响应数据,如解密 (可选)
52
- * resKey: 'data',
53
- * msgKey: 'message',
54
- * codeKey: 'status',
55
- * successCode: [1],
56
- * reloginCode: [-10],
57
- * ...config,
58
- * });
59
- * }
60
- *
61
- * // 1. 基于上面 requestApi 的普通接口
62
- * export function apiGoodList(data: { page: number, size: number }) {
63
- * return requestApi<GoodItem[]>({ url: '/goods/list', data, resKey: 'data.list' });
64
- * }
65
- *
66
- * const goodList = await apiGoodList({ page:1, size:10 });
67
- *
68
- * // 2. 参数泛型的写法
69
- * export function apiGoodList(config: RequestConfig<{ page: number, size: number }>) {
70
- * return requestApi<GoodItem[]>({ url: '/goods/list', resKey: 'data.list', ...config });
71
- * }
72
- *
73
- * const goodList = await apiGoodList({ data: { page:1, size:10 }, showLoading: false });
74
- *
75
- * // 3. 基于上面 requestApi 的流式接口
76
- * export function apiChatStream(data: { question: string }) {
77
- * return requestApi<T>({
78
- * url: '/sse/chatStream',
79
- * data,
80
- * resKey: false,
81
- * showLoading: false,
82
- * responseType: 'arraybuffer',
83
- * enableChunked: true,
84
- * });
85
- * }
86
- *
87
- * const { task } = apiChatStream({question: '你好'}); // 发起流式请求
88
- *
89
- * task.onChunkReceived((res) => {
90
- * console.log('ArrayBuffer', res.data); // 接收流式数据
91
- * });
92
- *
93
- * task.offChunkReceived(); // 取消监听,中断流式接收 (调用时机:流式结束,组件销毁,页面关闭)
94
- * task.abort(); // 取消请求 (若流式传输中,会中断流并抛出异常)
95
- */
96
- export function request<T, D extends RequestData = RequestData>(config: RequestConfigBase<D>) {
97
- // 1. 初始化控制对象
98
- const controller = new AbortController();
99
- const signal = controller.signal;
100
- let chunkCallback: ChunkCallback | null = null;
101
-
102
- // 构造 Task 对象
103
- const task: RequestTask = {
104
- abort: () => controller.abort(),
105
- onChunkReceived: (cb) => {
106
- chunkCallback = cb;
107
- },
108
- offChunkReceived: () => {
109
- chunkCallback = null;
110
- },
111
- };
112
-
113
- // 2. 创建 Promise
114
- const promise = new Promise<T>((resolve, reject) => {
115
- const execute = async () => {
116
- const {
117
- url,
118
- data,
119
- header,
120
- method = 'GET',
121
- resKey,
122
- msgKey,
123
- codeKey,
124
- successKey,
125
- successCode,
126
- reloginCode,
127
- showLoading = true,
128
- toastError = true,
129
- enableChunked = false,
130
- cacheTime,
131
- responseInterceptor,
132
- responseType = 'json',
133
- timeout = 60000,
134
- } = config;
135
-
136
- const isGet = method === 'GET';
137
- const isObjectData = isPlainObject(data);
138
- const isArrayData = !isObjectData && Array.isArray(data);
139
-
140
- // 2.1 参数处理
141
- // 参数: 过滤undefined, 避免接口处理异常 (不可过滤 null 、 "" 、 false 、 0 这些有效值)
142
- const fillData = isObjectData ? pickBy(data, (val) => val !== undefined) : data;
143
-
144
- // 请求头: 过滤空值 (undefined 、null 、"" 、false 、0), 因为服务器端接收到的都是字符串
145
- const fillHeader = (header ? pickBy(header, (val) => !!val) : {}) as Record<string, string>;
146
-
147
- if (!isGet && fillData && (isObjectData || isArrayData) && !fillHeader['Content-Type']) {
148
- fillHeader['Content-Type'] = 'application/json';
149
- }
150
-
151
- // 2.2 处理 URL 和 Body
152
- const fillUrl =
153
- isGet && isObjectData ? appendUrlParam(url, fillData as Record<string, unknown>) : url;
154
-
155
- const fillBody =
156
- !isGet && fillData
157
- ? isObjectData || isArrayData
158
- ? JSON.stringify(fillData)
159
- : (fillData as BodyInit)
160
- : undefined;
161
-
162
- // 2.3 日志与缓存配置
163
- const logConfig = { ...config, data: fillData, header: fillHeader, url: fillUrl };
164
- const startTime = Date.now();
165
-
166
- // 2.4 检查缓存
167
- const isCache = cacheTime && cacheTime > 0;
168
- const cacheKey = isCache ? JSON.stringify({ url: fillUrl, data: fillData }) : '';
169
-
170
- if (isCache) {
171
- const res = checkCache(cacheKey);
172
- if (res) {
173
- logRequestInfo({
174
- status: 'success',
175
- config: logConfig,
176
- fromCache: true,
177
- startTime,
178
- res,
179
- });
180
- resolve(getResult(res, resKey) as T);
181
- return;
182
- }
183
- }
184
-
185
- // 2.5 UI 反馈
186
- const appConfig = getAppConfig();
187
- if (showLoading) appConfig.showLoading?.();
188
-
189
- // 2.6 设置超时
190
- let isTimeout = false;
191
- const timeoutId = setTimeout(() => {
192
- isTimeout = true;
193
- controller.abort();
194
- }, timeout);
195
-
196
- try {
197
- // 2.7 发起请求
198
- const response = await fetch(fillUrl, {
199
- method,
200
- headers: fillHeader,
201
- body: fillBody,
202
- signal,
203
- });
204
-
205
- if (!response.ok) {
206
- if (showLoading) appConfig.hideLoading?.();
207
- throw new Error(`HTTP Error ${response.status}: ${response.statusText}`);
208
- }
209
-
210
- // 2.8 处理流式响应
211
- if (enableChunked) {
212
- if (showLoading) appConfig.hideLoading?.();
213
-
214
- const res = await handleStreamResponse(response, chunkCallback);
215
-
216
- logRequestInfo({ status: 'success', config: logConfig, startTime, res });
217
-
218
- resolve(res as T);
219
- return;
220
- }
221
-
222
- // 2.9 处理普通响应
223
- const resData = await parseResponse(response, responseType);
224
-
225
- // 隐藏 Loading
226
- if (showLoading) appConfig.hideLoading?.();
227
-
228
- // 响应拦截
229
- const res = responseInterceptor ? responseInterceptor(resData) : resData;
230
-
231
- // 2.10 业务状态码解析
232
- const code = getObjectValue(res, codeKey);
233
- const scode = successKey ? getObjectValue(res, successKey) : code;
234
- const msg = getObjectValue(res, msgKey);
235
- const isSuccess = successCode.includes(scode);
236
- const isRelogin = reloginCode.includes(code);
237
-
238
- logRequestInfo({ status: 'success', config: logConfig, startTime, res });
239
-
240
- // 2.11 结果处理
241
- if (isSuccess) {
242
- // 业务正常
243
- if (isCache) requestCache.set(cacheKey, { res, expire: Date.now() + cacheTime });
244
- resolve(getResult(res, resKey) as T);
245
- } else if (isRelogin) {
246
- // 登录失效
247
- reject(res);
248
- appConfig.toLogin?.(); // 放在后面,确保reject执行后再跳转登录
249
- } else {
250
- // 业务错误
251
- if (toastError && msg) appConfig.toast?.({ status: 'fail', msg });
252
- reject(res);
253
- }
254
- } catch (e) {
255
- const status = 'fail';
256
- const isAbortError = e instanceof DOMException && e.name === 'AbortError'; // 取消请求不视为错误
257
-
258
- if (isAbortError && isTimeout) {
259
- if (toastError) appConfig.toast?.({ status, msg: '请求超时' });
260
- const timeoutError = new Error('Request Timeout');
261
- logRequestInfo({ status, config: logConfig, startTime, e: timeoutError });
262
- reject(timeoutError);
263
- return;
264
- }
265
-
266
- if (!isAbortError && toastError) appConfig.toast?.({ status, msg: '网络请求失败' });
267
- logRequestInfo({ status, config: logConfig, startTime, e });
268
- reject(e);
269
- } finally {
270
- if (timeoutId) clearTimeout(timeoutId);
271
- }
272
- };
273
-
274
- execute();
275
- }) as Promise<T> & { task?: RequestTask };
276
-
277
- // 3. 挂载 Task
278
- promise.task = task;
279
-
280
- return promise;
281
- }
282
-
283
- /**
284
- * 日志输出
285
- */
286
- function logRequestInfo(options: {
287
- config: RequestConfigBase<RequestData> & { url?: string };
288
- fromCache?: boolean;
289
- startTime: number;
290
- status: 'success' | 'fail';
291
- res?: unknown;
292
- e?: unknown;
293
- }) {
294
- const { log } = getAppConfig();
295
- const { isLog = true } = options.config;
296
-
297
- if (!log || !isLog) return;
298
-
299
- const { config, res, fromCache = false, startTime, status, e } = options;
300
- const { url, data, header, method, extraLog } = config;
301
- const endTime = Date.now();
302
- const fmt = 'YYYY-MM-DD HH:mm:ss.SSS';
303
-
304
- const info: AppLogInfo = {
305
- name: 'request',
306
- status,
307
- url,
308
- data,
309
- method,
310
- header,
311
- fromCache,
312
- startTime: toDayjs(startTime).format(fmt),
313
- endTime: toDayjs(endTime).format(fmt),
314
- duration: endTime - startTime,
315
- ...extraLog,
316
- };
317
-
318
- if (status === 'success') {
319
- info.res = cloneDeep(res); // 深拷贝,避免外部修改对象,造成输出不一致
320
- log('info', info);
321
- } else {
322
- info.e = e;
323
- log('error', info);
324
- }
325
- }
326
-
327
- /**
328
- * 获取 resKey 对应的数据
329
- */
330
- function getResult(res: unknown, resKey?: RequestConfigBase['resKey']) {
331
- if (!res || !resKey || typeof res !== 'object') return res;
332
- return getObjectValue(res, resKey);
333
- }
334
-
335
- /**
336
- * 检查缓存
337
- */
338
- function checkCache(cacheKey: string) {
339
- const cached = requestCache.get(cacheKey);
340
- if (!cached) return null;
341
- if (cached.expire <= Date.now()) {
342
- requestCache.delete(cacheKey);
343
- return null;
344
- }
345
- return cached.res;
346
- }
347
-
348
- /**
349
- * 处理流式响应
350
- */
351
- async function handleStreamResponse(response: Response, chunkCallback: ChunkCallback | null) {
352
- if (!response.body) throw new Error('Response body is null');
353
-
354
- const reader = response.body.getReader();
355
-
356
- while (true) {
357
- const { done, value } = await reader.read();
358
- if (done) break;
359
- if (chunkCallback && value) {
360
- chunkCallback({ data: value.buffer });
361
- }
362
- }
363
-
364
- return 'Stream Finished';
365
- }
366
-
367
- /**
368
- * 解析响应数据
369
- */
370
- async function parseResponse(response: Response, responseType: string) {
371
- let resData: ResponseData;
372
- if (responseType === 'arraybuffer') {
373
- resData = await response.arrayBuffer();
374
- } else if (responseType === 'text') {
375
- resData = await response.text();
376
- } else {
377
- const text = await response.text();
378
- try {
379
- resData = JSON.parse(text);
380
- } catch {
381
- resData = text;
382
- }
383
- }
384
- return resData;
385
- }
File without changes