donar 0.0.1
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/.cspell/custom-dictionary.txt +5 -0
- package/.github/actions/base-check/action.yaml +27 -0
- package/.github/actions/env-setup/action.yaml +74 -0
- package/.github/workflows/pr-check.yaml +33 -0
- package/.github/workflows/release.yaml +112 -0
- package/.gitignore +41 -0
- package/.node-version +1 -0
- package/.npmignore +7 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/settings.json +44 -0
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/commitlint.config.ts +3 -0
- package/cspell.json +27 -0
- package/dist/components.esm.js +2 -0
- package/dist/css/components.C24nnsjt.css +1 -0
- package/dist/hooks.esm.js +185 -0
- package/dist/index.esm.js +4 -0
- package/dist/js/components.nFDoAkCq.js +198 -0
- package/dist/js/utils.CVb1iSAU.js +330 -0
- package/dist/types/components.d.ts +1 -0
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/src/components/async-custom-show/index.d.ts +22 -0
- package/dist/types/src/components/carousel/hooks.d.ts +74 -0
- package/dist/types/src/components/carousel/index.d.ts +88 -0
- package/dist/types/src/components/custom-show/index.d.ts +21 -0
- package/dist/types/src/components/error-boundary/index.d.ts +31 -0
- package/dist/types/src/components/index.d.ts +8 -0
- package/dist/types/src/hooks/async-guard.d.ts +9 -0
- package/dist/types/src/hooks/index.d.ts +5 -0
- package/dist/types/src/hooks/observer.d.ts +70 -0
- package/dist/types/src/hooks/state.d.ts +44 -0
- package/dist/types/src/hooks/storage.d.ts +25 -0
- package/dist/types/src/hooks/timer.d.ts +16 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/test/emiter-test.d.ts +1 -0
- package/dist/types/src/test/index.d.ts +1 -0
- package/dist/types/src/test/retry-async-test.d.ts +1 -0
- package/dist/types/src/utils/class-singleton.d.ts +6 -0
- package/dist/types/src/utils/concurrency.d.ts +17 -0
- package/dist/types/src/utils/debounce.d.ts +8 -0
- package/dist/types/src/utils/deep-copy.d.ts +11 -0
- package/dist/types/src/utils/dev.d.ts +8 -0
- package/dist/types/src/utils/download.d.ts +15 -0
- package/dist/types/src/utils/event-emitter/helpers.d.ts +36 -0
- package/dist/types/src/utils/event-emitter/index.d.ts +65 -0
- package/dist/types/src/utils/fetch-xhr/helpers.d.ts +28 -0
- package/dist/types/src/utils/fetch-xhr/index.d.ts +25 -0
- package/dist/types/src/utils/hash.d.ts +8 -0
- package/dist/types/src/utils/index.d.ts +15 -0
- package/dist/types/src/utils/is-deep-plain-equal.d.ts +15 -0
- package/dist/types/src/utils/json-convert.d.ts +66 -0
- package/dist/types/src/utils/micro-queue-scheduler.d.ts +14 -0
- package/dist/types/src/utils/raf-interval.d.ts +16 -0
- package/dist/types/src/utils/record-typed-map.d.ts +27 -0
- package/dist/types/src/utils/retry-async.d.ts +9 -0
- package/dist/types/src/utils/thenable.d.ts +15 -0
- package/dist/types/utils.d.ts +1 -0
- package/dist/utils.esm.js +2 -0
- package/eslint.config.ts +48 -0
- package/lint-staged.config.ts +13 -0
- package/oxfmt.config.ts +14 -0
- package/package.json +90 -0
- package/pnpm-workspace.yaml +3 -0
- package/scripts/sync-node-version/index.js +31 -0
- package/simple-git-hooks.js +4 -0
- package/src/components/async-custom-show/index.tsx +37 -0
- package/src/components/carousel/hooks.ts +312 -0
- package/src/components/carousel/index.module.scss +163 -0
- package/src/components/carousel/index.tsx +215 -0
- package/src/components/custom-show/index.tsx +31 -0
- package/src/components/error-boundary/index.tsx +48 -0
- package/src/components/index.ts +11 -0
- package/src/hooks/async-guard.ts +53 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/observer.ts +236 -0
- package/src/hooks/state.ts +140 -0
- package/src/hooks/storage.ts +103 -0
- package/src/hooks/timer.ts +42 -0
- package/src/index.ts +3 -0
- package/src/test/emiter-test.ts +19 -0
- package/src/test/index.ts +35 -0
- package/src/test/retry-async-test.ts +8 -0
- package/src/utils/class-singleton.ts +23 -0
- package/src/utils/concurrency.ts +49 -0
- package/src/utils/debounce.ts +20 -0
- package/src/utils/deep-copy.ts +132 -0
- package/src/utils/dev.ts +16 -0
- package/src/utils/download.ts +39 -0
- package/src/utils/event-emitter/helpers.ts +80 -0
- package/src/utils/event-emitter/index.ts +171 -0
- package/src/utils/fetch-xhr/helpers.ts +85 -0
- package/src/utils/fetch-xhr/index.ts +103 -0
- package/src/utils/hash.ts +25 -0
- package/src/utils/index.ts +18 -0
- package/src/utils/is-deep-plain-equal.ts +45 -0
- package/src/utils/json-convert.ts +257 -0
- package/src/utils/micro-queue-scheduler.ts +38 -0
- package/src/utils/raf-interval.ts +42 -0
- package/src/utils/record-typed-map.ts +38 -0
- package/src/utils/retry-async.ts +30 -0
- package/src/utils/thenable.ts +30 -0
- package/tsconfig.json +43 -0
- package/types/scss.d.ts +10 -0
- package/vite.config.ts +51 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { RecordTypedMap } from '../record-typed-map';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @author sonion
|
|
5
|
+
* @description 判断对象自身是否有指定属性
|
|
6
|
+
* @param obj 要判断的对象
|
|
7
|
+
* @param property 属性名
|
|
8
|
+
* @returns {boolean} - 是否有指定属性
|
|
9
|
+
*/
|
|
10
|
+
export const hasOwnProperty = <T extends Partial<Record<keyof T, unknown>>>(
|
|
11
|
+
obj: T,
|
|
12
|
+
property: string
|
|
13
|
+
) => Object.prototype.hasOwnProperty.call(obj, property);
|
|
14
|
+
|
|
15
|
+
/** 事件处理函数类型 */
|
|
16
|
+
export type EventHandler<P> = undefined extends P
|
|
17
|
+
? (payload?: P) => void
|
|
18
|
+
: (payload: P) => void;
|
|
19
|
+
|
|
20
|
+
/** 事件处理函数配置类型 */
|
|
21
|
+
export type EventHandlerOptions = {
|
|
22
|
+
/** 是否仅触发一次 */
|
|
23
|
+
once?: boolean;
|
|
24
|
+
/** 取消信号 */
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** 事件处理函数集合 */
|
|
29
|
+
export type EventCollection<P> = Map<
|
|
30
|
+
EventHandler<P>,
|
|
31
|
+
EventHandlerOptions | undefined
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @author sonion
|
|
36
|
+
* @description 创建事件处理集合,同名事件的集合
|
|
37
|
+
* 泛型 A,事件处理韩式参数类型
|
|
38
|
+
* @returns 事件处理函数集合
|
|
39
|
+
*/
|
|
40
|
+
export const createEventCollection = <
|
|
41
|
+
K extends keyof T,
|
|
42
|
+
T extends Record<string, unknown>,
|
|
43
|
+
>(): EventCollection<T[K]> => new Map() satisfies EventCollection<T[K]>;
|
|
44
|
+
|
|
45
|
+
/** 事件中心数据类型 `Map<事件名, Map<事件处理函数, 配置参数>>` */
|
|
46
|
+
export type EventCenter<T extends Record<string, unknown>> = RecordTypedMap<{
|
|
47
|
+
[K in keyof T]: EventCollection<T[K]>;
|
|
48
|
+
}>;
|
|
49
|
+
|
|
50
|
+
export type EventOptionExecutor<T extends Record<string, unknown>> = Record<
|
|
51
|
+
keyof EventHandlerOptions,
|
|
52
|
+
<K extends keyof T>(
|
|
53
|
+
events: EventCenter<T>,
|
|
54
|
+
eventName: K,
|
|
55
|
+
// 在emit工程中传入,因为是引用类型,可用于控制执行过程。如signal信号终止不再运行,就删除处理函数
|
|
56
|
+
eventExecutorParams: [EventHandler<T[K]>, EventHandlerOptions | undefined]
|
|
57
|
+
) => void
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
export const createEventOptionExecutor = <
|
|
61
|
+
T extends Record<string, unknown>,
|
|
62
|
+
>() =>
|
|
63
|
+
({
|
|
64
|
+
// 删除事件中心的任务,但当次还要运行
|
|
65
|
+
once: (events, eventName, eventExecutorParams) => {
|
|
66
|
+
events.get(eventName)?.delete(eventExecutorParams[0]);
|
|
67
|
+
events.get(eventName)?.size || events.delete(eventName); // 如果没有任务了,就删除该事件集合
|
|
68
|
+
},
|
|
69
|
+
signal: (events, eventName, eventExecutorParams) => {
|
|
70
|
+
if (!(eventExecutorParams[1]?.signal instanceof AbortSignal))
|
|
71
|
+
throw new TypeError(
|
|
72
|
+
'参数 signal 类型错误。必须是一个 AbortSignal 对象'
|
|
73
|
+
);
|
|
74
|
+
if (eventExecutorParams[1].signal.aborted) {
|
|
75
|
+
events.get(eventName)?.delete(eventExecutorParams[0]); // 删除事件中心的任务,且不再执行当次任务
|
|
76
|
+
events.get(eventName)?.size || events.delete(eventName); // 如果没有任务了,就删除该事件集合
|
|
77
|
+
Reflect.deleteProperty(eventExecutorParams, 0); // 索引不变
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
}) satisfies EventOptionExecutor<T>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type EventHandler,
|
|
3
|
+
type EventHandlerOptions,
|
|
4
|
+
type EventOptionExecutor,
|
|
5
|
+
type EventCenter,
|
|
6
|
+
createEventCollection,
|
|
7
|
+
createEventOptionExecutor,
|
|
8
|
+
hasOwnProperty,
|
|
9
|
+
} from './helpers';
|
|
10
|
+
import { createMicroQueueScheduler } from '../micro-queue-scheduler';
|
|
11
|
+
import { RecordTypedMap } from '../record-typed-map';
|
|
12
|
+
|
|
13
|
+
// 定义一个排除函数的类型
|
|
14
|
+
type NonFunction =
|
|
15
|
+
| null
|
|
16
|
+
| undefined
|
|
17
|
+
| number
|
|
18
|
+
| string
|
|
19
|
+
| boolean
|
|
20
|
+
| symbol
|
|
21
|
+
| bigint
|
|
22
|
+
| { [key: string]: unknown };
|
|
23
|
+
|
|
24
|
+
/** 返回包含 undefined 类型的键 */
|
|
25
|
+
type KeysWithUndefined<T> = {
|
|
26
|
+
// 遍历 T 的所有键 K,如可选会是和 undefined 的联合类型
|
|
27
|
+
// 所以用 undefined extends T[K] 来判断是否包含 undefined
|
|
28
|
+
[K in keyof T]: undefined extends T[K] ? K : never;
|
|
29
|
+
}[keyof T];
|
|
30
|
+
|
|
31
|
+
/** 返回不包含 undefined 类型的键 */
|
|
32
|
+
type KeysWithoutUndefined<T> = {
|
|
33
|
+
[K in keyof T]: undefined extends T[K] ? never : K;
|
|
34
|
+
}[keyof T];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @author sonion
|
|
38
|
+
* @description 自定义事件中心。
|
|
39
|
+
* @example
|
|
40
|
+
* const eventCenter = new EventEmitter();
|
|
41
|
+
* eventCenter.on('onChange', data=>console.log('没有配置的事件。参数:', data));
|
|
42
|
+
* eventCenter.on('onTest', data=>console.log('once 配置为 true 的事件。参数:', data), {once: true});
|
|
43
|
+
* const controller = new AbortController(); // 创建取消信号
|
|
44
|
+
* eventCenter.on('tap', data=>console.log('配置了 signal 的事件。参数:', data), {signal: controller.signal})
|
|
45
|
+
* controller.abort() // 取消
|
|
46
|
+
* eventCenter.emit('onChange', {happy: true});
|
|
47
|
+
*/
|
|
48
|
+
export class EventEmitter<T extends Record<string, NonFunction>> {
|
|
49
|
+
/** Map<事件名, Map<事件处理函数, 配置参数>> */
|
|
50
|
+
private events: EventCenter<T> = new RecordTypedMap();
|
|
51
|
+
|
|
52
|
+
/** 配置对象执行器,不同配置参数的不同处理。在emit中执行 */
|
|
53
|
+
private eventOptionExecutor: EventOptionExecutor<T>;
|
|
54
|
+
|
|
55
|
+
/** 自定义调度器,怎么执行事件处理函数 */
|
|
56
|
+
private scheduler: (eventHandel: () => void) => void;
|
|
57
|
+
|
|
58
|
+
constructor(scheduler?: (eventHandel: () => void) => void) {
|
|
59
|
+
this.scheduler = scheduler ?? createMicroQueueScheduler();
|
|
60
|
+
// 事件处理 options 执行器。如需添加处理参数,直接扩展 createEventOptionExecutor 返回对象属性
|
|
61
|
+
this.eventOptionExecutor = createEventOptionExecutor<T>();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @author sonion
|
|
66
|
+
* @description 注册事件
|
|
67
|
+
* @param eventName 事件名
|
|
68
|
+
* @param callback 事件处理函数
|
|
69
|
+
* @param options 配置对象。once:是否只运行一次, signal取消信号
|
|
70
|
+
*/
|
|
71
|
+
addEventListener<K extends keyof T>(
|
|
72
|
+
eventName: K,
|
|
73
|
+
callback: EventHandler<T[K]>,
|
|
74
|
+
options?: EventHandlerOptions
|
|
75
|
+
): void {
|
|
76
|
+
if (options) {
|
|
77
|
+
const keys = Object.keys(this.eventOptionExecutor);
|
|
78
|
+
if (typeof options !== 'object' || Array.isArray(options)) {
|
|
79
|
+
throw new TypeError(
|
|
80
|
+
'参数 options 类型错误。options 应该为一个配置对象。'
|
|
81
|
+
);
|
|
82
|
+
} else if (!keys.some((key) => hasOwnProperty(options, key))) {
|
|
83
|
+
throw new TypeError(`options 仅支持 ${keys.join('、')} 参数`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// events 为用class 包装的 Map 类型更友好
|
|
87
|
+
this.events.has(eventName) ||
|
|
88
|
+
this.events.set(eventName, createEventCollection<K, T>());
|
|
89
|
+
const collection = this.events.get(eventName);
|
|
90
|
+
collection?.set(callback, options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @author sonion
|
|
95
|
+
* @description 移除事件
|
|
96
|
+
* @param eventName 事件名
|
|
97
|
+
* @param callback 事件处理函数
|
|
98
|
+
*/
|
|
99
|
+
removeEventListener<K extends keyof T>(
|
|
100
|
+
eventName: K,
|
|
101
|
+
callback: EventHandler<T[K]>
|
|
102
|
+
) {
|
|
103
|
+
if (!this.events.has(eventName)) return;
|
|
104
|
+
this.events.get(eventName)?.delete(callback);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 添加事件 别名
|
|
108
|
+
on<K extends keyof T>(
|
|
109
|
+
eventName: K,
|
|
110
|
+
callback: EventHandler<T[K]>,
|
|
111
|
+
options?: EventHandlerOptions
|
|
112
|
+
) {
|
|
113
|
+
this.addEventListener(eventName, callback, options);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 移除事件 别名
|
|
117
|
+
off<K extends keyof T>(eventName: K, callback: EventHandler<T[K]>) {
|
|
118
|
+
this.removeEventListener(eventName, callback);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @author sonion
|
|
123
|
+
* @description 清空事件列表
|
|
124
|
+
* @param {string | void} [eventName] - 要清除的事件名。不传就清空所有事件列表
|
|
125
|
+
*/
|
|
126
|
+
clear(eventName?: keyof T) {
|
|
127
|
+
if (eventName) {
|
|
128
|
+
return this.events.delete(eventName);
|
|
129
|
+
}
|
|
130
|
+
return this.events.clear() ?? true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @author sonion
|
|
135
|
+
* @description 发布事件
|
|
136
|
+
* @param eventName 事件名
|
|
137
|
+
* @param data 事件参数
|
|
138
|
+
*/
|
|
139
|
+
emit<K extends KeysWithoutUndefined<T>>(eventName: K, data: T[K]): void;
|
|
140
|
+
emit<K extends KeysWithUndefined<T>>(eventName: K, data?: T[K]): void;
|
|
141
|
+
emit<K extends keyof T>(eventName: K, data?: T[K]) {
|
|
142
|
+
if (!this.events.has(eventName)) return;
|
|
143
|
+
// 遍历订阅对象,执行handler
|
|
144
|
+
this.scheduler(() => {
|
|
145
|
+
this.events.get(eventName)?.forEach((options, callback) => {
|
|
146
|
+
const eventExecutorParams: [
|
|
147
|
+
EventHandler<T[K]>,
|
|
148
|
+
EventHandlerOptions | undefined,
|
|
149
|
+
] = [callback, options];
|
|
150
|
+
|
|
151
|
+
if (options) {
|
|
152
|
+
const optionKeys = Object.keys(
|
|
153
|
+
options
|
|
154
|
+
) as (keyof EventHandlerOptions)[];
|
|
155
|
+
optionKeys.forEach((key) => {
|
|
156
|
+
hasOwnProperty(this.eventOptionExecutor, key) &&
|
|
157
|
+
this.eventOptionExecutor[key](
|
|
158
|
+
this.events,
|
|
159
|
+
eventName,
|
|
160
|
+
eventExecutorParams
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// 可能执行时事件可能已经被移除
|
|
165
|
+
// 利用 eventExecutorParams 是数组的引用类型特征
|
|
166
|
+
// eventOptionExecutor 处理后 eventHandler 内还存在才运行
|
|
167
|
+
eventExecutorParams[0] && callback(data as T[K]); // 必须在最后
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author sonion
|
|
3
|
+
* @description
|
|
4
|
+
* @param xhr - XMLHttpRequest 实例对象
|
|
5
|
+
* @param headers 请求头参数
|
|
6
|
+
*/
|
|
7
|
+
export const setRequestHeaders = (
|
|
8
|
+
xhr: XMLHttpRequest,
|
|
9
|
+
headers: RequestInit['headers']
|
|
10
|
+
) => {
|
|
11
|
+
if (headers) {
|
|
12
|
+
if (headers instanceof Headers) {
|
|
13
|
+
headers.forEach((value, key) => {
|
|
14
|
+
xhr.setRequestHeader(key, value);
|
|
15
|
+
});
|
|
16
|
+
} else if (Array.isArray(headers)) {
|
|
17
|
+
headers.forEach((header) => {
|
|
18
|
+
xhr.setRequestHeader(header[0], header[1]);
|
|
19
|
+
});
|
|
20
|
+
} else {
|
|
21
|
+
Object.keys(headers).forEach((key) => {
|
|
22
|
+
xhr.setRequestHeader(key, headers[key]);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @author sonion
|
|
30
|
+
* @description 处理请求体,将 ReadableStream 类型转换为 Blob 类型
|
|
31
|
+
* @param body - 请求体
|
|
32
|
+
* @param signal - 信号量
|
|
33
|
+
*/
|
|
34
|
+
export const handleRequestBody = async (
|
|
35
|
+
body: RequestInit['body'],
|
|
36
|
+
signal?: AbortSignal | null
|
|
37
|
+
) => {
|
|
38
|
+
if (!(body instanceof ReadableStream)) {
|
|
39
|
+
return body;
|
|
40
|
+
}
|
|
41
|
+
const chunks: Uint8Array[] = [];
|
|
42
|
+
const reader = body.getReader();
|
|
43
|
+
while (true) {
|
|
44
|
+
signal?.throwIfAborted(); // 信号被终止,就抛出错误
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done) break;
|
|
47
|
+
chunks.push(value);
|
|
48
|
+
}
|
|
49
|
+
return new Blob(chunks as BlobPart[]);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** 响应头分隔符 正则 */
|
|
53
|
+
const RegExpForSplitHeaders = /[\r\n]+/;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @author sonion
|
|
57
|
+
* @description 获取响应头
|
|
58
|
+
* @param {XMLHttpRequest} xhr XMLHttpRequest对象
|
|
59
|
+
*/
|
|
60
|
+
export const getResponseHeaders = function (xhr: XMLHttpRequest) {
|
|
61
|
+
const headerStr = xhr.getAllResponseHeaders();
|
|
62
|
+
const arr = headerStr.trim().split(RegExpForSplitHeaders);
|
|
63
|
+
const headerLines = arr.map((line) => {
|
|
64
|
+
const index = line.indexOf(':');
|
|
65
|
+
if (index <= -1) {
|
|
66
|
+
return [] as unknown as [string, string];
|
|
67
|
+
}
|
|
68
|
+
return [line.slice(0, index), line.slice(index + 1).trim()] satisfies [
|
|
69
|
+
string,
|
|
70
|
+
string,
|
|
71
|
+
];
|
|
72
|
+
});
|
|
73
|
+
return new Headers(headerLines);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @author sonion
|
|
78
|
+
* @description 自定义超时错误类
|
|
79
|
+
*/
|
|
80
|
+
export class CustomTimeoutError extends DOMException {
|
|
81
|
+
readonly isTimeout = true;
|
|
82
|
+
constructor(msg?: string) {
|
|
83
|
+
super(msg ?? 'signal is aborted without reason', 'AbortError');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setRequestHeaders,
|
|
3
|
+
handleRequestBody,
|
|
4
|
+
getResponseHeaders,
|
|
5
|
+
CustomTimeoutError,
|
|
6
|
+
} from './helpers';
|
|
7
|
+
|
|
8
|
+
export { CustomTimeoutError };
|
|
9
|
+
|
|
10
|
+
// fetch 请求参数中 xhr 支持的参数
|
|
11
|
+
type RequestInitFields =
|
|
12
|
+
| 'body'
|
|
13
|
+
| 'method'
|
|
14
|
+
| 'headers'
|
|
15
|
+
| 'credentials'
|
|
16
|
+
| 'signal';
|
|
17
|
+
|
|
18
|
+
export interface FetchXHRInit extends Pick<RequestInit, RequestInitFields> {
|
|
19
|
+
/**
|
|
20
|
+
* @description 超时时间,单位毫秒
|
|
21
|
+
*/
|
|
22
|
+
timeout?: number;
|
|
23
|
+
/**
|
|
24
|
+
* @description 上传进度回调
|
|
25
|
+
*/
|
|
26
|
+
onUploadProgress?: (loaded: number, total: number, e: ProgressEvent) => void;
|
|
27
|
+
/**
|
|
28
|
+
* @description 下载进度回调
|
|
29
|
+
*/
|
|
30
|
+
onDownloadProgress?: (
|
|
31
|
+
loaded: number,
|
|
32
|
+
total: number,
|
|
33
|
+
e: ProgressEvent
|
|
34
|
+
) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @author sonion
|
|
39
|
+
* @description 基于 XMLHttpRequest 实现的 fetch 函数。
|
|
40
|
+
* 除增加 timeout、onUploadProgress、onDownloadProgress 外,其他与 fetch 一致。
|
|
41
|
+
* @param url 请求地址
|
|
42
|
+
* @param init 请求配置。
|
|
43
|
+
*/
|
|
44
|
+
export const fetchXHR = async (url: string, init?: FetchXHRInit) => {
|
|
45
|
+
return new Promise<Response>((resolve, reject) => {
|
|
46
|
+
const {
|
|
47
|
+
body,
|
|
48
|
+
method = 'GET',
|
|
49
|
+
headers,
|
|
50
|
+
credentials,
|
|
51
|
+
signal,
|
|
52
|
+
timeout,
|
|
53
|
+
onUploadProgress,
|
|
54
|
+
onDownloadProgress,
|
|
55
|
+
} = init || {};
|
|
56
|
+
const xhr = new XMLHttpRequest();
|
|
57
|
+
const normalizedMethod = method.toUpperCase();
|
|
58
|
+
xhr.open(normalizedMethod, url, true); // 必须在设置请求头之前
|
|
59
|
+
// 设置请求头
|
|
60
|
+
setRequestHeaders(xhr, headers);
|
|
61
|
+
credentials === 'include' && (xhr.withCredentials = true); // cookie跨域是否携带
|
|
62
|
+
xhr.responseType = 'blob'; // 固定 blob,传给Response处理
|
|
63
|
+
typeof timeout === 'number' &&
|
|
64
|
+
!Number.isNaN(timeout) &&
|
|
65
|
+
(xhr.timeout = timeout); // 设置超时时间
|
|
66
|
+
|
|
67
|
+
onUploadProgress &&
|
|
68
|
+
(xhr.upload.onprogress = (e) => onUploadProgress(e.loaded, e.total, e));
|
|
69
|
+
|
|
70
|
+
onDownloadProgress &&
|
|
71
|
+
(xhr.onprogress = (e) => onDownloadProgress(e.loaded, e.total, e));
|
|
72
|
+
|
|
73
|
+
if (signal) {
|
|
74
|
+
const handleAbort = () => xhr.abort();
|
|
75
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
76
|
+
xhr.onloadend = () => signal?.removeEventListener('abort', handleAbort);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
xhr.onabort = () =>
|
|
80
|
+
reject(
|
|
81
|
+
new DOMException('signal is aborted without reason', 'AbortError')
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// 兼容 fetch 的超时写法。继承 DOMException name 也是 AbortError。扩展 isTimeout 标识是否超时。
|
|
85
|
+
xhr.ontimeout = () => reject(new CustomTimeoutError('请求超时'));
|
|
86
|
+
|
|
87
|
+
xhr.onerror = () => reject(new TypeError('网络错误'));
|
|
88
|
+
|
|
89
|
+
// 200系、400系、500系状态码都认为是成功
|
|
90
|
+
xhr.onload = () =>
|
|
91
|
+
resolve(
|
|
92
|
+
new Response(xhr.response, {
|
|
93
|
+
status: xhr.status,
|
|
94
|
+
statusText: xhr.statusText,
|
|
95
|
+
headers: getResponseHeaders(xhr),
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
handleRequestBody(body, signal).then((res) =>
|
|
100
|
+
xhr.send(['GET', 'HEAD'].includes(normalizedMethod) ? null : res)
|
|
101
|
+
); // 处理请求体,在发送
|
|
102
|
+
});
|
|
103
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type SupportedHashType = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @author sonion
|
|
5
|
+
* @description 字符串转哈希
|
|
6
|
+
* @param {string} message - 要转换的字符串
|
|
7
|
+
* @param {SupportedHashType} algorithm - 哈希算法,默认SHA-1
|
|
8
|
+
*/
|
|
9
|
+
export async function stringToHash(
|
|
10
|
+
message: string,
|
|
11
|
+
algorithm: SupportedHashType = 'SHA-1'
|
|
12
|
+
): Promise<string> {
|
|
13
|
+
try {
|
|
14
|
+
if (!message) {
|
|
15
|
+
throw new Error('message is empty');
|
|
16
|
+
}
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const data = encoder.encode(message);
|
|
19
|
+
const hashBuffer = await window.crypto.subtle.digest(algorithm, data);
|
|
20
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
21
|
+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
22
|
+
} catch (err) {
|
|
23
|
+
throw err instanceof Error ? err : new Error('string to hash failed');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from './thenable';
|
|
2
|
+
export * from './concurrency';
|
|
3
|
+
export * from './debounce';
|
|
4
|
+
export * from './hash';
|
|
5
|
+
export * from './download';
|
|
6
|
+
export * from './deep-copy';
|
|
7
|
+
export * from './raf-interval';
|
|
8
|
+
|
|
9
|
+
export * from './json-convert';
|
|
10
|
+
export * from './fetch-xhr';
|
|
11
|
+
export * from './class-singleton';
|
|
12
|
+
export * from './event-emitter';
|
|
13
|
+
export * from './micro-queue-scheduler';
|
|
14
|
+
export * from './record-typed-map';
|
|
15
|
+
export * from './retry-async';
|
|
16
|
+
export * from './is-deep-plain-equal';
|
|
17
|
+
|
|
18
|
+
import './dev';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author sonion
|
|
3
|
+
* @param value 要判断的值
|
|
4
|
+
* @description 是否为null或undefined
|
|
5
|
+
*/
|
|
6
|
+
export const isNil = (value: unknown): value is null | undefined =>
|
|
7
|
+
value === null || value === undefined;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @author sonion
|
|
11
|
+
* @description 深度比较两个未知类型的值是否相等。支持 Plain Object。
|
|
12
|
+
* 不支持比较函数、Symbol、Date、RegExp等。
|
|
13
|
+
* @param a 要比较的第一个值
|
|
14
|
+
* @param b 要比较的第二个值
|
|
15
|
+
* @returns 如果两个值相等则返回 true,否则返回 false
|
|
16
|
+
*/
|
|
17
|
+
export const isDeepPlainEqual = (a: unknown, b: unknown): boolean => {
|
|
18
|
+
if (isNil(a) || isNil(b)) return Object.is(a, b);
|
|
19
|
+
if (Object.is(a, b)) return true;
|
|
20
|
+
if (typeof a !== typeof b) return false;
|
|
21
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
22
|
+
if (a.length !== b.length) return false;
|
|
23
|
+
for (let i = 0, length = a.length; i < length; i++) {
|
|
24
|
+
// 可能有顺序不一致问题
|
|
25
|
+
if (!isDeepPlainEqual(a[i], b[i])) return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
// 不支持比较函数、Symbol、Date、RegExp等
|
|
30
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
31
|
+
const aKeys = Object.keys(a);
|
|
32
|
+
const bKeys = Object.keys(b);
|
|
33
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
34
|
+
for (const key of aKeys) {
|
|
35
|
+
if (!Object.hasOwnProperty.call(b, key)) return false; // 解决无属性和有属性但值为 undefined 的情况
|
|
36
|
+
if (
|
|
37
|
+
!isDeepPlainEqual(a[key as keyof typeof a], b[key as keyof typeof b])
|
|
38
|
+
) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
};
|