@nickyzj2023/utils 1.0.48 → 1.0.50
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/AGENTS.md +58 -22
- package/dist/dom/index.d.ts +1 -0
- package/dist/dom/timeLog.d.ts +8 -0
- package/dist/function/index.d.ts +1 -0
- package/dist/function/loopUntil.d.ts +22 -0
- package/dist/hoc/index.d.ts +1 -0
- package/dist/hoc/withCache.d.ts +42 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +2 -0
- package/dist/is/index.d.ts +5 -0
- package/dist/is/isFalsy.d.ts +9 -0
- package/dist/is/isNil.d.ts +9 -0
- package/dist/is/isObject.d.ts +8 -0
- package/dist/is/isPrimitive.d.ts +9 -0
- package/dist/is/isTruthy.d.ts +8 -0
- package/dist/lru-cache.d.ts +18 -0
- package/dist/network/fetcher.d.ts +56 -0
- package/dist/network/getRealURL.d.ts +2 -0
- package/dist/network/image.d.ts +59 -0
- package/dist/network/index.d.ts +4 -0
- package/dist/network/to.d.ts +9 -0
- package/dist/number/index.d.ts +1 -0
- package/dist/number/randomInt.d.ts +7 -0
- package/dist/object/index.d.ts +3 -0
- package/dist/object/mapKeys.d.ts +17 -0
- package/dist/object/mapValues.d.ts +21 -0
- package/dist/object/mergeObjects.d.ts +12 -0
- package/dist/string/case.d.ts +32 -0
- package/dist/string/compact.d.ts +22 -0
- package/dist/string/index.d.ts +2 -0
- package/dist/time/debounce.d.ts +20 -0
- package/dist/time/index.d.ts +3 -0
- package/dist/time/sleep.d.ts +7 -0
- package/dist/time/throttle.d.ts +20 -0
- package/docs/assets/highlight.css +92 -99
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/functions/camelToSnake.html +178 -178
- package/docs/functions/capitalize.html +178 -178
- package/docs/functions/compactStr.html +182 -182
- package/docs/functions/debounce.html +187 -187
- package/docs/functions/decapitalize.html +178 -178
- package/docs/functions/fetcher.html +189 -189
- package/docs/functions/getRealURL.html +175 -175
- package/docs/functions/imageUrlToBase64.html +191 -187
- package/docs/functions/isFalsy.html +178 -178
- package/docs/functions/isNil.html +178 -178
- package/docs/functions/isObject.html +178 -178
- package/docs/functions/isPrimitive.html +178 -178
- package/docs/functions/isTruthy.html +178 -178
- package/docs/functions/loopUntil.html +180 -180
- package/docs/functions/mapKeys.html +180 -180
- package/docs/functions/mapValues.html +182 -182
- package/docs/functions/mergeObjects.html +184 -184
- package/docs/functions/randomInt.html +178 -178
- package/docs/functions/sleep.html +179 -179
- package/docs/functions/snakeToCamel.html +178 -178
- package/docs/functions/throttle.html +187 -187
- package/docs/functions/timeLog.html +178 -178
- package/docs/functions/to.html +180 -180
- package/docs/functions/withCache.html +185 -185
- package/docs/modules.html +174 -174
- package/docs/types/BunFetchOptions.html +176 -0
- package/docs/types/CamelToSnake.html +174 -174
- package/docs/types/Capitalize.html +174 -174
- package/docs/types/Decapitalize.html +174 -174
- package/docs/types/DeepMapKeys.html +174 -174
- package/docs/types/DeepMapValues.html +174 -174
- package/docs/types/Falsy.html +174 -174
- package/docs/types/ImageCompressionOptions.html +188 -184
- package/docs/types/Primitive.html +174 -174
- package/docs/types/RequestInit.html +174 -174
- package/docs/types/SetTtl.html +174 -174
- package/docs/types/SnakeToCamel.html +174 -174
- package/package.json +2 -2
- package/src/dom/index.ts +1 -0
- package/src/function/index.ts +1 -0
- package/src/{function.ts → function/loopUntil.ts} +36 -36
- package/src/hoc/index.ts +1 -0
- package/src/{hoc.ts → hoc/withCache.ts} +117 -117
- package/src/is/index.ts +5 -0
- package/src/is/isFalsy.ts +12 -0
- package/src/is/isNil.ts +11 -0
- package/src/is/isObject.ts +10 -0
- package/src/is/isPrimitive.ts +23 -0
- package/src/is/isTruthy.ts +10 -0
- package/src/lru-cache.ts +50 -50
- package/src/network/fetcher.ts +1 -1
- package/src/network/index.ts +1 -1
- package/src/number/index.ts +1 -0
- package/src/object/index.ts +3 -0
- package/src/object/mapKeys.ts +50 -0
- package/src/object/mapValues.ts +77 -0
- package/src/object/mergeObjects.ts +50 -0
- package/src/time/debounce.ts +34 -0
- package/src/time/index.ts +3 -0
- package/src/time/sleep.ts +11 -0
- package/src/time/throttle.ts +34 -0
- package/.github/workflows/docs.yml +0 -39
- package/src/is.ts +0 -70
- package/src/object.ts +0 -179
- package/src/time.ts +0 -81
- /package/src/{dom.ts → dom/timeLog.ts} +0 -0
- /package/src/{number.ts → number/randomInt.ts} +0 -0
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
type CacheEntry = {
|
|
2
|
-
value: any;
|
|
3
|
-
expiresAt: number; // 时间戳(毫秒)
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export type SetTtl = (seconds: number) => void;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 创建一个带缓存的高阶函数
|
|
10
|
-
*
|
|
11
|
-
* @template Args 被包装函数的参数类型数组
|
|
12
|
-
* @template Result 被包装函数的返回类型
|
|
13
|
-
*
|
|
14
|
-
* @param fn 需要被缓存的函数,参数里附带的 setTtl 方法用于根据具体情况改写过期时间
|
|
15
|
-
* @param ttlSeconds 以秒为单位的过期时间,-1 表示永不过期,默认 -1,会被回调函数里的 setTtl() 覆盖
|
|
16
|
-
*
|
|
17
|
-
* @returns 返回包装后的函数,以及缓存相关的额外方法
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* // 异步函数示例
|
|
21
|
-
* const fetchData = withCache(async function (url: string) {
|
|
22
|
-
* const data = await fetch(url).then((res) => res.json());
|
|
23
|
-
* this.setTtl(data.expiresIn); // 根据实际情况调整过期时间
|
|
24
|
-
* return data;
|
|
25
|
-
* });
|
|
26
|
-
*
|
|
27
|
-
* await fetchData(urlA);
|
|
28
|
-
* await fetchData(urlA); // 使用缓存结果
|
|
29
|
-
* await fetchData(urlB);
|
|
30
|
-
* await fetchData(urlB); // 使用缓存结果
|
|
31
|
-
*
|
|
32
|
-
* fetchData.clear(); // 清除缓存
|
|
33
|
-
* await fetchData(urlA); // 重新请求
|
|
34
|
-
* await fetchData(urlB); // 重新请求
|
|
35
|
-
*
|
|
36
|
-
* // 缓存过期前
|
|
37
|
-
* await sleep();
|
|
38
|
-
* fetchData.updateTtl(180); // 更新 ttl 并为所有未过期的缓存续期
|
|
39
|
-
* await fetchData(urlA); // 使用缓存结果
|
|
40
|
-
* await fetchData(urlB); // 使用缓存结果
|
|
41
|
-
*/
|
|
42
|
-
export const withCache = <Args extends any[], Result>(
|
|
43
|
-
fn: (this: { setTtl: SetTtl }, ...args: Args) => Result,
|
|
44
|
-
ttlSeconds = -1,
|
|
45
|
-
) => {
|
|
46
|
-
const cache = new Map<string, CacheEntry>();
|
|
47
|
-
|
|
48
|
-
const wrapped = (...args: Args): Result => {
|
|
49
|
-
const key = JSON.stringify(args);
|
|
50
|
-
const now = Date.now();
|
|
51
|
-
const entry = cache.get(key);
|
|
52
|
-
|
|
53
|
-
// 命中缓存且未过期
|
|
54
|
-
if (entry && now < entry.expiresAt) {
|
|
55
|
-
return entry.value;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let expiresAt = ttlSeconds === -1 ? Infinity : now + ttlSeconds * 1000;
|
|
59
|
-
|
|
60
|
-
// 创建上下文,包含 setTtl 方法
|
|
61
|
-
const thisArg = {
|
|
62
|
-
setTtl: (ttl: number) => {
|
|
63
|
-
expiresAt = now + ttl * 1000;
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const result = fn.apply(thisArg, args);
|
|
68
|
-
|
|
69
|
-
// 异步函数:缓存 Promise 的 resolved 值
|
|
70
|
-
if (result instanceof Promise) {
|
|
71
|
-
const promise = result.then((resolved) => {
|
|
72
|
-
cache.set(key, {
|
|
73
|
-
value: resolved,
|
|
74
|
-
expiresAt,
|
|
75
|
-
});
|
|
76
|
-
return resolved;
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// 将 promise 先塞进去避免重复请求
|
|
80
|
-
cache.set(key, {
|
|
81
|
-
value: promise,
|
|
82
|
-
expiresAt,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
return promise as Result;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 同步函数缓存
|
|
89
|
-
cache.set(key, {
|
|
90
|
-
value: result,
|
|
91
|
-
expiresAt,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return result;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/** 手动清除缓存 */
|
|
98
|
-
wrapped.clear = () => cache.clear();
|
|
99
|
-
|
|
100
|
-
/** 更新 TTL,同时刷新所有未过期缓存的时间 */
|
|
101
|
-
wrapped.updateTtl = (seconds: number) => {
|
|
102
|
-
// 更新默认 TTL
|
|
103
|
-
ttlSeconds = seconds;
|
|
104
|
-
|
|
105
|
-
// 给未过期缓存续期
|
|
106
|
-
const now = Date.now();
|
|
107
|
-
const newExpiresAt = now + seconds * 1000;
|
|
108
|
-
for (const [key, entry] of cache.entries()) {
|
|
109
|
-
if (entry.expiresAt > now) {
|
|
110
|
-
entry.expiresAt = newExpiresAt;
|
|
111
|
-
cache.set(key, entry);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
return wrapped;
|
|
117
|
-
};
|
|
1
|
+
type CacheEntry = {
|
|
2
|
+
value: any;
|
|
3
|
+
expiresAt: number; // 时间戳(毫秒)
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type SetTtl = (seconds: number) => void;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 创建一个带缓存的高阶函数
|
|
10
|
+
*
|
|
11
|
+
* @template Args 被包装函数的参数类型数组
|
|
12
|
+
* @template Result 被包装函数的返回类型
|
|
13
|
+
*
|
|
14
|
+
* @param fn 需要被缓存的函数,参数里附带的 setTtl 方法用于根据具体情况改写过期时间
|
|
15
|
+
* @param ttlSeconds 以秒为单位的过期时间,-1 表示永不过期,默认 -1,会被回调函数里的 setTtl() 覆盖
|
|
16
|
+
*
|
|
17
|
+
* @returns 返回包装后的函数,以及缓存相关的额外方法
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // 异步函数示例
|
|
21
|
+
* const fetchData = withCache(async function (url: string) {
|
|
22
|
+
* const data = await fetch(url).then((res) => res.json());
|
|
23
|
+
* this.setTtl(data.expiresIn); // 根据实际情况调整过期时间
|
|
24
|
+
* return data;
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* await fetchData(urlA);
|
|
28
|
+
* await fetchData(urlA); // 使用缓存结果
|
|
29
|
+
* await fetchData(urlB);
|
|
30
|
+
* await fetchData(urlB); // 使用缓存结果
|
|
31
|
+
*
|
|
32
|
+
* fetchData.clear(); // 清除缓存
|
|
33
|
+
* await fetchData(urlA); // 重新请求
|
|
34
|
+
* await fetchData(urlB); // 重新请求
|
|
35
|
+
*
|
|
36
|
+
* // 缓存过期前
|
|
37
|
+
* await sleep();
|
|
38
|
+
* fetchData.updateTtl(180); // 更新 ttl 并为所有未过期的缓存续期
|
|
39
|
+
* await fetchData(urlA); // 使用缓存结果
|
|
40
|
+
* await fetchData(urlB); // 使用缓存结果
|
|
41
|
+
*/
|
|
42
|
+
export const withCache = <Args extends any[], Result>(
|
|
43
|
+
fn: (this: { setTtl: SetTtl }, ...args: Args) => Result,
|
|
44
|
+
ttlSeconds = -1,
|
|
45
|
+
) => {
|
|
46
|
+
const cache = new Map<string, CacheEntry>();
|
|
47
|
+
|
|
48
|
+
const wrapped = (...args: Args): Result => {
|
|
49
|
+
const key = JSON.stringify(args);
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const entry = cache.get(key);
|
|
52
|
+
|
|
53
|
+
// 命中缓存且未过期
|
|
54
|
+
if (entry && now < entry.expiresAt) {
|
|
55
|
+
return entry.value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let expiresAt = ttlSeconds === -1 ? Infinity : now + ttlSeconds * 1000;
|
|
59
|
+
|
|
60
|
+
// 创建上下文,包含 setTtl 方法
|
|
61
|
+
const thisArg = {
|
|
62
|
+
setTtl: (ttl: number) => {
|
|
63
|
+
expiresAt = now + ttl * 1000;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const result = fn.apply(thisArg, args);
|
|
68
|
+
|
|
69
|
+
// 异步函数:缓存 Promise 的 resolved 值
|
|
70
|
+
if (result instanceof Promise) {
|
|
71
|
+
const promise = result.then((resolved) => {
|
|
72
|
+
cache.set(key, {
|
|
73
|
+
value: resolved,
|
|
74
|
+
expiresAt,
|
|
75
|
+
});
|
|
76
|
+
return resolved;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 将 promise 先塞进去避免重复请求
|
|
80
|
+
cache.set(key, {
|
|
81
|
+
value: promise,
|
|
82
|
+
expiresAt,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return promise as Result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 同步函数缓存
|
|
89
|
+
cache.set(key, {
|
|
90
|
+
value: result,
|
|
91
|
+
expiresAt,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** 手动清除缓存 */
|
|
98
|
+
wrapped.clear = () => cache.clear();
|
|
99
|
+
|
|
100
|
+
/** 更新 TTL,同时刷新所有未过期缓存的时间 */
|
|
101
|
+
wrapped.updateTtl = (seconds: number) => {
|
|
102
|
+
// 更新默认 TTL
|
|
103
|
+
ttlSeconds = seconds;
|
|
104
|
+
|
|
105
|
+
// 给未过期缓存续期
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
const newExpiresAt = now + seconds * 1000;
|
|
108
|
+
for (const [key, entry] of cache.entries()) {
|
|
109
|
+
if (entry.expiresAt > now) {
|
|
110
|
+
entry.expiresAt = newExpiresAt;
|
|
111
|
+
cache.set(key, entry);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return wrapped;
|
|
117
|
+
};
|
package/src/is/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type Falsy = false | 0 | -0 | 0n | "" | null | undefined;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 检测传入的值是否为**假值**(false、0、''、null、undefined、NaN等)
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* isFalsy(""); // true
|
|
8
|
+
* isFalsy(1); // false
|
|
9
|
+
*/
|
|
10
|
+
export const isFalsy = (value: any): value is Falsy => {
|
|
11
|
+
return !value;
|
|
12
|
+
};
|
package/src/is/isNil.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检测传入的值是否为**空值**(null、undefined)
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* isNil(null); // true
|
|
6
|
+
* isNil(undefined); // true
|
|
7
|
+
* isNil(1); // false
|
|
8
|
+
*/
|
|
9
|
+
export const isNil = (value: any): value is null | undefined => {
|
|
10
|
+
return value === null || value === undefined;
|
|
11
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type Primitive =
|
|
2
|
+
| number
|
|
3
|
+
| string
|
|
4
|
+
| boolean
|
|
5
|
+
| symbol
|
|
6
|
+
| bigint
|
|
7
|
+
| undefined
|
|
8
|
+
| null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 检测传入的值是否为**原始值**(number、string、boolean、symbol、bigint、undefined、null)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* isPrimitive(1); // true
|
|
15
|
+
* isPrimitive([]); // false
|
|
16
|
+
*/
|
|
17
|
+
export const isPrimitive = (value: any): value is Primitive => {
|
|
18
|
+
return (
|
|
19
|
+
value === null ||
|
|
20
|
+
value === undefined ||
|
|
21
|
+
(typeof value !== "object" && typeof value !== "function")
|
|
22
|
+
);
|
|
23
|
+
};
|
package/src/lru-cache.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 简易 LRU 缓存
|
|
3
|
-
* @example
|
|
4
|
-
* const cache = new LRUCache<string, number>(2);
|
|
5
|
-
* cache.set("a", 1);
|
|
6
|
-
* cache.set("b", 2);
|
|
7
|
-
* cache.set("c", 3); // 缓存已满,a 被淘汰
|
|
8
|
-
* cache.get("a"); // undefined
|
|
9
|
-
*/
|
|
10
|
-
class LRUCache<K, V> {
|
|
11
|
-
private cache: Map<K, V>;
|
|
12
|
-
private maxSize: number;
|
|
13
|
-
|
|
14
|
-
constructor(maxSize = 10) {
|
|
15
|
-
this.cache = new Map();
|
|
16
|
-
this.maxSize = maxSize;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
get(key: K): V | undefined {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// 重置缓存顺序
|
|
25
|
-
this.cache.delete(key);
|
|
26
|
-
this.cache.set(key, value);
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
set(key: K, value: V) {
|
|
31
|
-
// 刷新缓存
|
|
32
|
-
if (this.cache.has(key)) {
|
|
33
|
-
this.cache.delete(key);
|
|
34
|
-
}
|
|
35
|
-
// 删除最旧的缓存
|
|
36
|
-
else if (this.cache.size >= this.maxSize) {
|
|
37
|
-
const oldestKey = [...this.cache.keys()][0];
|
|
38
|
-
if (oldestKey) {
|
|
39
|
-
this.cache.delete(oldestKey);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
this.cache.set(key, value);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
has(key: K) {
|
|
46
|
-
return this.cache.has(key);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export default LRUCache;
|
|
1
|
+
/**
|
|
2
|
+
* 简易 LRU 缓存
|
|
3
|
+
* @example
|
|
4
|
+
* const cache = new LRUCache<string, number>(2);
|
|
5
|
+
* cache.set("a", 1);
|
|
6
|
+
* cache.set("b", 2);
|
|
7
|
+
* cache.set("c", 3); // 缓存已满,a 被淘汰
|
|
8
|
+
* cache.get("a"); // undefined
|
|
9
|
+
*/
|
|
10
|
+
class LRUCache<K, V> {
|
|
11
|
+
private cache: Map<K, V>;
|
|
12
|
+
private maxSize: number;
|
|
13
|
+
|
|
14
|
+
constructor(maxSize = 10) {
|
|
15
|
+
this.cache = new Map();
|
|
16
|
+
this.maxSize = maxSize;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get(key: K): V | undefined {
|
|
20
|
+
if (!this.cache.has(key)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const value = this.cache.get(key)!;
|
|
24
|
+
// 重置缓存顺序
|
|
25
|
+
this.cache.delete(key);
|
|
26
|
+
this.cache.set(key, value);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
set(key: K, value: V) {
|
|
31
|
+
// 刷新缓存
|
|
32
|
+
if (this.cache.has(key)) {
|
|
33
|
+
this.cache.delete(key);
|
|
34
|
+
}
|
|
35
|
+
// 删除最旧的缓存
|
|
36
|
+
else if (this.cache.size >= this.maxSize) {
|
|
37
|
+
const oldestKey = [...this.cache.keys()][0];
|
|
38
|
+
if (oldestKey) {
|
|
39
|
+
this.cache.delete(oldestKey);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.cache.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
has(key: K) {
|
|
46
|
+
return this.cache.has(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default LRUCache;
|
package/src/network/fetcher.ts
CHANGED
package/src/network/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { fetcher, type RequestInit } from "./fetcher";
|
|
1
|
+
export { type BunFetchOptions, fetcher, type RequestInit } from "./fetcher";
|
|
2
2
|
export { getRealURL } from "./getRealURL";
|
|
3
3
|
export { type ImageCompressionOptions, imageUrlToBase64 } from "./image";
|
|
4
4
|
export { to } from "./to";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { randomInt } from "./randomInt";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isObject } from "../is";
|
|
2
|
+
|
|
3
|
+
// 这里的 DeepMapKeys 只能承诺 key 是 string,无法推断出 key 的具体字面量变化
|
|
4
|
+
// 因为 TS 不支持根据任意函数反推 Key 的字面量类型
|
|
5
|
+
export type DeepMapKeys<T> =
|
|
6
|
+
T extends Array<infer U>
|
|
7
|
+
? Array<DeepMapKeys<U>>
|
|
8
|
+
: T extends object
|
|
9
|
+
? { [key: string]: DeepMapKeys<T[keyof T]> }
|
|
10
|
+
: T;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 递归处理对象里的 key
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* 无法完整推导出类型,只能做到有递归,key 全为 string,value 为同层级的所有类型的联合
|
|
17
|
+
*
|
|
18
|
+
* @template T 要转换的对象
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const obj = { a: { b: 1 } };
|
|
22
|
+
* const result = mapKeys(obj, (key) => key.toUpperCase());
|
|
23
|
+
* console.log(result); // { A: { B: 1 } }
|
|
24
|
+
*/
|
|
25
|
+
export const mapKeys = <T>(
|
|
26
|
+
obj: T,
|
|
27
|
+
getNewKey: (key: string) => string,
|
|
28
|
+
): DeepMapKeys<T> => {
|
|
29
|
+
// 递归处理数组
|
|
30
|
+
if (Array.isArray(obj)) {
|
|
31
|
+
return obj.map((item) => mapKeys(item, getNewKey)) as any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 处理普通对象
|
|
35
|
+
if (isObject(obj)) {
|
|
36
|
+
const keys = Object.keys(obj);
|
|
37
|
+
return keys.reduce(
|
|
38
|
+
(result, key) => {
|
|
39
|
+
const newKey = getNewKey(key);
|
|
40
|
+
const value = (obj as any)[key];
|
|
41
|
+
result[newKey] = mapKeys(value, getNewKey);
|
|
42
|
+
return result;
|
|
43
|
+
},
|
|
44
|
+
{} as Record<string, any>,
|
|
45
|
+
) as DeepMapKeys<T>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 处理非数组/对象
|
|
49
|
+
return obj as any;
|
|
50
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { isObject } from "../is";
|
|
2
|
+
|
|
3
|
+
// 这里的 DeepMapValues 尝试保留 key,但将 value 类型替换为 R
|
|
4
|
+
// 注意:如果原 value 是对象,我们递归处理结构,而不是把整个对象变成 R
|
|
5
|
+
export type DeepMapValues<T, R> =
|
|
6
|
+
T extends Array<infer U>
|
|
7
|
+
? Array<DeepMapValues<U, R>>
|
|
8
|
+
: T extends object
|
|
9
|
+
? { [K in keyof T]: T[K] extends object ? DeepMapValues<T[K], R> : R }
|
|
10
|
+
: R;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 递归处理对象里的 value
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* 无法完整推导出类型,所有 value 最终都会变为 any
|
|
17
|
+
*
|
|
18
|
+
* @template T 要转换的对象
|
|
19
|
+
* @template R 转换后的值类型,为 any,无法进一步推导
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const obj = { a: 1, b: { c: 2 } };
|
|
23
|
+
* const result = mapValues(obj, (value, key) => isPrimitive(value) ? value + 1 : value);
|
|
24
|
+
* console.log(result); // { a: 2, b: { c: 3 } }
|
|
25
|
+
*/
|
|
26
|
+
export const mapValues = <T, R = any>(
|
|
27
|
+
obj: T,
|
|
28
|
+
getNewValue: (value: any, key: string | number) => R,
|
|
29
|
+
options?: {
|
|
30
|
+
/** 过滤函数,返回 true 表示保留该字段 */
|
|
31
|
+
filter?: (value: any, key: string | number) => boolean;
|
|
32
|
+
},
|
|
33
|
+
): DeepMapValues<T, R> => {
|
|
34
|
+
const { filter } = options ?? {};
|
|
35
|
+
|
|
36
|
+
// 处理数组
|
|
37
|
+
if (Array.isArray(obj)) {
|
|
38
|
+
const mappedArray = obj.map((item, index) => {
|
|
39
|
+
// 如果元素是对象,则递归处理
|
|
40
|
+
if (isObject(item)) {
|
|
41
|
+
return mapValues(item, getNewValue, options);
|
|
42
|
+
}
|
|
43
|
+
// 如果元素是原始值,则直接应用 getNewValue(此时 key 为数组下标)
|
|
44
|
+
return getNewValue(item, index);
|
|
45
|
+
});
|
|
46
|
+
// 如果有过滤器,则过滤一遍元素
|
|
47
|
+
if (filter) {
|
|
48
|
+
return mappedArray.filter((item, index) => filter(item, index)) as any;
|
|
49
|
+
}
|
|
50
|
+
return mappedArray as any;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 处理普通对象
|
|
54
|
+
if (isObject(obj)) {
|
|
55
|
+
const keys = Object.keys(obj);
|
|
56
|
+
return keys.reduce((result, key) => {
|
|
57
|
+
const value = (obj as any)[key];
|
|
58
|
+
let newValue: any;
|
|
59
|
+
// 如果值为对象或数组,则递归处理
|
|
60
|
+
if (isObject(value) || Array.isArray(value)) {
|
|
61
|
+
newValue = mapValues(value, getNewValue, options);
|
|
62
|
+
}
|
|
63
|
+
// 否则直接应用 getNewValue
|
|
64
|
+
else {
|
|
65
|
+
newValue = getNewValue(value, key);
|
|
66
|
+
}
|
|
67
|
+
// 如果存在过滤器,则看情况保留该字段
|
|
68
|
+
if (!filter || filter(newValue, key)) {
|
|
69
|
+
result[key] = newValue;
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}, {} as any) as DeepMapValues<T, R>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 处理非数组/对象
|
|
76
|
+
return obj as any;
|
|
77
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isObject, isPrimitive } from "../is";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 深度合并两个对象,规则如下:
|
|
5
|
+
* 1. 原始值覆盖:如果两个值都是原始类型,则用后者覆盖;
|
|
6
|
+
* 2. 数组拼接:如果两个值都是数组,则拼接为大数组;
|
|
7
|
+
* 3. 对象递归合并:如果两个值都是对象,则进行递归深度合并;
|
|
8
|
+
*
|
|
9
|
+
* @template T 第一个对象
|
|
10
|
+
* @template U 第二个对象
|
|
11
|
+
* @param {T} obj1 要合并的第一个对象,相同字段会被 obj2 覆盖
|
|
12
|
+
* @param {U} obj2 要合并的第二个对象
|
|
13
|
+
*/
|
|
14
|
+
export const mergeObjects = <
|
|
15
|
+
T extends Record<string, any>,
|
|
16
|
+
U extends Record<string, any>,
|
|
17
|
+
>(
|
|
18
|
+
obj1: T,
|
|
19
|
+
obj2: U,
|
|
20
|
+
) => {
|
|
21
|
+
const result: Record<string, any> = { ...obj1 };
|
|
22
|
+
|
|
23
|
+
for (const key of Object.keys(obj2)) {
|
|
24
|
+
const val1 = result[key];
|
|
25
|
+
const val2 = obj2[key];
|
|
26
|
+
|
|
27
|
+
// 如果都是原始值,则用后者覆盖
|
|
28
|
+
if (isPrimitive(val1) && isPrimitive(val2)) {
|
|
29
|
+
result[key] = val2;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 如果都是数组,则拼接为大数组
|
|
34
|
+
if (Array.isArray(val1) && Array.isArray(val2)) {
|
|
35
|
+
result[key] = val1.concat(val2);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 如果都是对象,则深度递归合并
|
|
40
|
+
if (isObject(val1) && isObject(val2)) {
|
|
41
|
+
result[key] = mergeObjects(val1, val2);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 其他情况用后者覆盖
|
|
46
|
+
result[key] = val2;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result as T & U;
|
|
50
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 防抖:在指定时间内只执行最后一次调用
|
|
3
|
+
* @param fn 要防抖的函数
|
|
4
|
+
* @param delay 延迟时间,默认 300ms
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* 连续触发时,只有最后一次会执行。适合用于搜索框输入、窗口大小调整等场景。
|
|
8
|
+
* 例如:用户输入"hello"过程中,不会触发搜索,只有停下来时才执行。
|
|
9
|
+
*
|
|
10
|
+
* 防抖 vs 节流:
|
|
11
|
+
* - 防抖:等待触发停止后才执行(最后一次)
|
|
12
|
+
* - 节流:按固定节奏执行(每隔多久执行一次)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const search = debounce((keyword: string) => {
|
|
16
|
+
* console.log('搜索:', keyword);
|
|
17
|
+
* });
|
|
18
|
+
* search('hello'); // 300ms 后执行
|
|
19
|
+
*/
|
|
20
|
+
export const debounce = <T extends (...args: any[]) => any>(
|
|
21
|
+
fn: T,
|
|
22
|
+
delay = 300,
|
|
23
|
+
) => {
|
|
24
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
25
|
+
|
|
26
|
+
return (...args: Parameters<T>) => {
|
|
27
|
+
if (timer) {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
timer = setTimeout(() => {
|
|
31
|
+
fn(...args);
|
|
32
|
+
}, delay);
|
|
33
|
+
};
|
|
34
|
+
};
|