@nickyzj2023/utils 1.0.47 → 1.0.48

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/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@nickyzj2023/utils",
3
- "version": "1.0.47",
3
+ "version": "1.0.48",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/Nickyzj628/utils.git"
7
7
  },
8
+ "type": "module",
8
9
  "module": "dist/index.js",
10
+ "types": "dist/index.d.ts",
9
11
  "devDependencies": {
10
12
  "@biomejs/biome": "^2.3.14",
11
13
  "@types/bun": "^1.3.8",
@@ -18,7 +20,5 @@
18
20
  "scripts": {
19
21
  "docs": "typedoc src/index.ts --plugin typedoc-material-theme",
20
22
  "build": "bun build --target=bun --outdir ./dist --minify ./src/index.ts --packages external && tsc"
21
- },
22
- "type": "module",
23
- "types": "dist/index.d.ts"
23
+ }
24
24
  }
@@ -1,5 +1,5 @@
1
- import { isNil, isObject } from "./is";
2
- import { mergeObjects } from "./object";
1
+ import { isNil, isObject } from "../is";
2
+ import { mergeObjects } from "../object";
3
3
 
4
4
  // Bun 特有的 fetch 选项
5
5
  type BunFetchOptions = {
@@ -121,38 +121,3 @@ export const fetcher = (baseURL = "", baseOptions: RequestInit = {}) => {
121
121
  myFetch<T>(url, { ...options, method: "DELETE" }),
122
122
  };
123
123
  };
124
-
125
- /**
126
- * Go 语言风格的异步处理方式
127
- * @param promise 一个能被 await 的异步函数
128
- * @returns 如果成功,返回 [null, 异步函数结果],否则返回 [Error, undefined]
129
- *
130
- * @example
131
- * const [error, response] = await to(fetcher().get<Blog>("/blogs/hello-world"));
132
- */
133
- export const to = async <T, E = Error>(
134
- promise: Promise<T>,
135
- ): Promise<[null, T] | [E, undefined]> => {
136
- try {
137
- return [null, await promise];
138
- } catch (e) {
139
- return [e as E, undefined];
140
- }
141
- };
142
-
143
- /** 从 url 响应头获取真实链接 */
144
- export const getRealURL = async (originURL: string) => {
145
- const [error, response] = await to(
146
- fetch(originURL, {
147
- method: "HEAD", // 用 HEAD 减少数据传输
148
- redirect: "manual", // 手动处理重定向
149
- }),
150
- );
151
-
152
- if (error) {
153
- return originURL;
154
- }
155
-
156
- const location = response.headers.get("location");
157
- return location || originURL;
158
- };
@@ -0,0 +1,18 @@
1
+ import { to } from "./to";
2
+
3
+ /** 从 url 响应头获取真实链接 */
4
+ export const getRealURL = async (originURL: string) => {
5
+ const [error, response] = await to(
6
+ fetch(originURL, {
7
+ method: "HEAD", // 用 HEAD 减少数据传输
8
+ redirect: "manual", // 手动处理重定向
9
+ }),
10
+ );
11
+
12
+ if (error) {
13
+ return originURL;
14
+ }
15
+
16
+ const location = response.headers.get("location");
17
+ return location || originURL;
18
+ };
@@ -0,0 +1,202 @@
1
+ /**
2
+ * 将 ArrayBuffer 转换为 base64 字符串
3
+ * 兼容浏览器、Bun 和 Node.js
4
+ */
5
+ const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
6
+ const bytes = new Uint8Array(buffer);
7
+ let binary = "";
8
+ for (let i = 0; i < bytes.byteLength; i++) {
9
+ binary += String.fromCharCode(bytes[i]!);
10
+ }
11
+ return btoa(binary);
12
+ };
13
+
14
+ /**
15
+ * 尝试动态导入 sharp 模块
16
+ * 用于检测用户项目中是否安装了 sharp
17
+ */
18
+ const tryImportSharp = async () => {
19
+ try {
20
+ // 动态导入 sharp,避免在浏览器环境中报错
21
+ // 使用 new Function 避免 TypeScript 编译时解析该模块(sharp 是可选依赖)
22
+ const dynamicImport = new Function(
23
+ "modulePath",
24
+ "return import(modulePath)",
25
+ );
26
+ const sharpModule = await dynamicImport("sharp");
27
+ return sharpModule.default || sharpModule;
28
+ } catch {
29
+ return null;
30
+ }
31
+ };
32
+
33
+ /**
34
+ * 使用 sharp 压缩图片
35
+ */
36
+ const compressWithSharp = async (
37
+ sharp: any,
38
+ arrayBuffer: ArrayBuffer,
39
+ mime: string,
40
+ quality: number,
41
+ ): Promise<string> => {
42
+ const buffer = Buffer.from(arrayBuffer);
43
+ let sharpInstance = sharp(buffer);
44
+
45
+ // 根据 MIME 类型设置压缩选项
46
+ if (mime === "image/jpeg") {
47
+ sharpInstance = sharpInstance.jpeg({ quality: Math.round(quality * 100) });
48
+ } else if (mime === "image/png") {
49
+ // PNG 使用 compressionLevel (0-9),将 quality (0-1) 映射到 compressionLevel
50
+ const compressionLevel = Math.round((1 - quality) * 9);
51
+ sharpInstance = sharpInstance.png({ compressionLevel });
52
+ }
53
+
54
+ const compressedBuffer = await sharpInstance.toBuffer();
55
+ return `data:${mime};base64,${compressedBuffer.toString("base64")}`;
56
+ };
57
+
58
+ /**
59
+ * 图片压缩选项
60
+ */
61
+ export type ImageCompressionOptions = {
62
+ /** 压缩比率,默认 0.92 */
63
+ quality?: number;
64
+ /**
65
+ * 自定义压缩函数,用于覆盖默认压缩行为
66
+ * @param arrayBuffer 图片的 ArrayBuffer 数据
67
+ * @param mime 图片的 MIME 类型
68
+ * @param quality 压缩质量
69
+ * @returns 压缩后的 base64 字符串
70
+ */
71
+ compressor?: (
72
+ arrayBuffer: ArrayBuffer,
73
+ mime: string,
74
+ quality: number,
75
+ ) => Promise<string> | string;
76
+ /**
77
+ * 自定义 fetch 函数,用于使用自己封装的请求库读取图片
78
+ * 必须返回符合 Web 标准的 Response 对象
79
+ * @param url 图片地址
80
+ * @returns Promise<Response>
81
+ */
82
+ fetcher?: (url: string) => Promise<Response>;
83
+ };
84
+
85
+ /**
86
+ * 图片地址转 base64 数据
87
+ *
88
+ * @param imageUrl 图片地址
89
+ * @param options 可选配置
90
+ * @param options.quality 压缩比率,默认 0.92
91
+ * @param options.compressor 自定义压缩函数,用于覆盖默认压缩行为
92
+ *
93
+ * @example
94
+ * // 基本用法(浏览器自动使用 Canvas 压缩,Node.js/Bun 自动检测并使用 sharp)
95
+ * imageUrlToBase64("https://example.com/image.jpg");
96
+ *
97
+ * @example
98
+ * // 使用自定义 fetch 函数(如 axios 封装)
99
+ * imageUrlToBase64("https://example.com/image.jpg", {
100
+ * fetcher: async (url) => {
101
+ * // 使用 axios 或其他请求库,但必须返回 Response 对象
102
+ * const response = await axios.get(url, { responseType: 'arraybuffer' });
103
+ * return new Response(response.data, {
104
+ * status: response.status,
105
+ * statusText: response.statusText,
106
+ * headers: response.headers
107
+ * });
108
+ * }
109
+ * });
110
+ *
111
+ * @example
112
+ * // 使用自定义压缩函数覆盖默认行为
113
+ * imageUrlToBase64("https://example.com/image.jpg", {
114
+ * quality: 0.8,
115
+ * compressor: async (buffer, mime, quality) => {
116
+ * // 自定义压缩逻辑
117
+ * return `data:${mime};base64,...`;
118
+ * }
119
+ * });
120
+ */
121
+ export const imageUrlToBase64 = async (
122
+ imageUrl: string,
123
+ options: ImageCompressionOptions = {},
124
+ ): Promise<string> => {
125
+ const { quality = 0.92, compressor, fetcher = fetch } = options;
126
+
127
+ if (!imageUrl.startsWith("http")) {
128
+ throw new Error("图片地址必须以http或https开头");
129
+ }
130
+
131
+ // 使用自定义 fetch 获取图片数据
132
+ const response = await fetcher(imageUrl);
133
+ if (!response.ok) {
134
+ throw new Error(`获取图片失败: ${response.statusText}`);
135
+ }
136
+
137
+ const mime = response.headers.get("Content-Type") || "image/jpeg";
138
+ const arrayBuffer = await response.arrayBuffer();
139
+
140
+ // 对于非 JPEG/PNG 图片,直接返回 base64,不做压缩
141
+ if (mime !== "image/jpeg" && mime !== "image/png") {
142
+ const base64 = arrayBufferToBase64(arrayBuffer);
143
+ return `data:${mime};base64,${base64}`;
144
+ }
145
+
146
+ // 如果提供了自定义压缩函数,优先使用它
147
+ if (compressor) {
148
+ return await compressor(arrayBuffer, mime, quality);
149
+ }
150
+
151
+ // 浏览器环境:使用 OffscreenCanvas 压缩
152
+ if (typeof OffscreenCanvas !== "undefined") {
153
+ let bitmap: ImageBitmap | null = null;
154
+
155
+ try {
156
+ const blob = new Blob([arrayBuffer], { type: mime });
157
+ bitmap = await createImageBitmap(blob);
158
+
159
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
160
+ const ctx = canvas.getContext("2d");
161
+ if (!ctx) {
162
+ throw new Error("无法获取 OffscreenCanvas context");
163
+ }
164
+
165
+ ctx.drawImage(bitmap, 0, 0);
166
+ bitmap.close();
167
+ bitmap = null;
168
+
169
+ // OffscreenCanvas 使用 convertToBlob 获取压缩后的图片
170
+ const compressedBlob = await canvas.convertToBlob({
171
+ type: mime,
172
+ quality: quality,
173
+ });
174
+
175
+ // 将 Blob 转换为 base64
176
+ const compressedArrayBuffer = await compressedBlob.arrayBuffer();
177
+ const base64 = arrayBufferToBase64(compressedArrayBuffer);
178
+ return `data:${mime};base64,${base64}`;
179
+ } catch {
180
+ // OffscreenCanvas 压缩失败,返回原始 base64
181
+ bitmap?.close();
182
+ const base64 = arrayBufferToBase64(arrayBuffer);
183
+ return `data:${mime};base64,${base64}`;
184
+ }
185
+ }
186
+
187
+ // Node.js/Bun 环境:尝试使用 sharp 进行压缩
188
+ const sharp = await tryImportSharp();
189
+ if (sharp) {
190
+ try {
191
+ return await compressWithSharp(sharp, arrayBuffer, mime, quality);
192
+ } catch {
193
+ // sharp 压缩失败,返回原始 base64
194
+ const base64 = arrayBufferToBase64(arrayBuffer);
195
+ return `data:${mime};base64,${base64}`;
196
+ }
197
+ }
198
+
199
+ // 没有可用的压缩方式,直接返回原始 base64
200
+ const base64 = arrayBufferToBase64(arrayBuffer);
201
+ return `data:${mime};base64,${base64}`;
202
+ };
@@ -0,0 +1,4 @@
1
+ export { fetcher, type RequestInit } from "./fetcher";
2
+ export { getRealURL } from "./getRealURL";
3
+ export { type ImageCompressionOptions, imageUrlToBase64 } from "./image";
4
+ export { to } from "./to";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Go 语言风格的异步处理方式
3
+ * @param promise 一个能被 await 的异步函数
4
+ * @returns 如果成功,返回 [null, 异步函数结果],否则返回 [Error, undefined]
5
+ *
6
+ * @example
7
+ * const [error, response] = await to(fetcher().get<Blog>("/blogs/hello-world"));
8
+ */
9
+ export const to = async <T, E = Error>(
10
+ promise: Promise<T>,
11
+ ): Promise<[null, T] | [E, undefined]> => {
12
+ try {
13
+ return [null, await promise];
14
+ } catch (e) {
15
+ return [e as E, undefined];
16
+ }
17
+ };
@@ -0,0 +1,71 @@
1
+ export type SnakeToCamel<S extends string> =
2
+ S extends `${infer Before}_${infer After}`
3
+ ? After extends `${infer First}${infer Rest}`
4
+ ? `${Before}${Uppercase<First>}${SnakeToCamel<Rest>}`
5
+ : Before
6
+ : S;
7
+
8
+ /**
9
+ * 下划线命名法转为驼峰命名法
10
+ *
11
+ * @example
12
+ * snakeToCamel("user_name") // "userName"
13
+ */
14
+ export const snakeToCamel = <S extends string>(str: S): SnakeToCamel<S> => {
15
+ return str.replace(/_([a-zA-Z])/g, (match, pattern) =>
16
+ pattern.toUpperCase(),
17
+ ) as SnakeToCamel<S>;
18
+ };
19
+
20
+ export type CamelToSnake<S extends string> =
21
+ S extends `${infer First}${infer Rest}`
22
+ ? Rest extends Uncapitalize<Rest>
23
+ ? `${Lowercase<First>}${CamelToSnake<Rest>}`
24
+ : `${Lowercase<First>}_${CamelToSnake<Rest>}`
25
+ : Lowercase<S>;
26
+
27
+ /**
28
+ * 驼峰命名法转为下划线命名法
29
+ *
30
+ * @example
31
+ * camelToSnake("shouldComponentUpdate") // "should_component_update"
32
+ */
33
+ export const camelToSnake = <S extends string>(str: S): CamelToSnake<S> => {
34
+ return str.replace(
35
+ /([A-Z])/g,
36
+ (match, pattern) => `_${pattern.toLowerCase()}`,
37
+ ) as CamelToSnake<S>;
38
+ };
39
+
40
+ export type Capitalize<S extends string> = S extends `${infer P1}${infer Rest}`
41
+ ? P1 extends Capitalize<P1>
42
+ ? S
43
+ : `${Uppercase<P1>}${Rest}`
44
+ : S;
45
+
46
+ /**
47
+ * 字符串首字母大写
48
+ *
49
+ * @example
50
+ * capitalize("hello") // "Hello"
51
+ */
52
+ export const capitalize = <S extends string>(s: S): Capitalize<S> => {
53
+ return (s.charAt(0).toUpperCase() + s.slice(1)) as Capitalize<S>;
54
+ };
55
+
56
+ export type Decapitalize<S extends string> =
57
+ S extends `${infer P1}${infer Rest}`
58
+ ? P1 extends Lowercase<P1>
59
+ ? P1
60
+ : `${Lowercase<P1>}${Rest}`
61
+ : S;
62
+
63
+ /**
64
+ * 字符串首字母小写
65
+ *
66
+ * @example
67
+ * decapitalize("Hello") // "hello"
68
+ */
69
+ export const decapitalize = <S extends string>(s: S): Decapitalize<S> => {
70
+ return (s.charAt(0).toLowerCase() + s.slice(1)) as Decapitalize<S>;
71
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 将字符串压缩为单行精简格式
3
+ *
4
+ * @example
5
+ * // "Hello, world."
6
+ * compactStr(`
7
+ * Hello,
8
+ * world!
9
+ * `, {
10
+ * disableNewLineReplace: false,
11
+ * });
12
+ */
13
+ export const compactStr = (
14
+ text: string = "",
15
+ options?: {
16
+ /** 最大保留长度,设为 0 或 Infinity 则不截断,默认 Infinity */
17
+ maxLength?: number;
18
+ /** 是否将换行符替换为字面量 \n,默认开启 */
19
+ disableNewLineReplace?: boolean;
20
+ /** 是否合并连续的空格/制表符为一个空格,默认开启 */
21
+ disableWhitespaceCollapse?: boolean;
22
+ /** 截断后的后缀,默认为 "..." */
23
+ omission?: string;
24
+ },
25
+ ): string => {
26
+ if (!text) return "";
27
+
28
+ const {
29
+ maxLength = Infinity,
30
+ disableNewLineReplace = false,
31
+ disableWhitespaceCollapse = false,
32
+ omission = "...",
33
+ } = options ?? {};
34
+
35
+ let result = text;
36
+
37
+ // 处理换行符
38
+ if (!disableNewLineReplace) {
39
+ result = result.replace(/\r?\n/g, "\\n");
40
+ } else {
41
+ result = result.replace(/\r?\n/g, " ");
42
+ }
43
+
44
+ // 合并连续空格
45
+ if (!disableWhitespaceCollapse) {
46
+ result = result.replace(/\s+/g, " ");
47
+ }
48
+ result = result.trim();
49
+
50
+ // 截断多出来的文字
51
+ if (maxLength > 0 && result.length > maxLength) {
52
+ return result.slice(0, maxLength) + omission;
53
+ }
54
+
55
+ return result;
56
+ };
@@ -0,0 +1,12 @@
1
+ export {
2
+ type CamelToSnake,
3
+ type Capitalize,
4
+ camelToSnake,
5
+ capitalize,
6
+ type Decapitalize,
7
+ decapitalize,
8
+ type SnakeToCamel,
9
+ snakeToCamel,
10
+ } from "./case";
11
+
12
+ export { compactStr } from "./compact";
package/dist/dom.d.ts DELETED
@@ -1,8 +0,0 @@
1
- /**
2
- * 附带时间的 console.log
3
- * @param args
4
- *
5
- * @example
6
- * timeLog("Hello", "World"); // 14:30:00 Hello World
7
- */
8
- export declare const timeLog: (...args: any[]) => void;
@@ -1,22 +0,0 @@
1
- /**
2
- * 循环执行函数,直到符合停止条件
3
- *
4
- * @example
5
- * // 循环请求大语言模型,直到其不再调用工具
6
- * loopUntil(
7
- * async () => {
8
- * const completion = await chatCompletions();
9
- * completion.tool_calls?.forEach(chooseAndHandleTool)
10
- * return completion;
11
- * },
12
- * {
13
- * shouldStop: (completion) => !completion.tool_calls,
14
- * },
15
- * ),
16
- */
17
- export declare const loopUntil: <T>(fn: (count: number) => Promise<T>, options?: {
18
- /** 最大循环次数,默认 5 次 */
19
- maxRetries?: number;
20
- /** 停止循环条件,默认立即停止 */
21
- shouldStop?: (result: T) => boolean;
22
- }) => Promise<T>;
package/dist/hoc.d.ts DELETED
@@ -1,42 +0,0 @@
1
- export type SetTtl = (seconds: number) => void;
2
- /**
3
- * 创建一个带缓存的高阶函数
4
- *
5
- * @template Args 被包装函数的参数类型数组
6
- * @template Result 被包装函数的返回类型
7
- *
8
- * @param fn 需要被缓存的函数,参数里附带的 setTtl 方法用于根据具体情况改写过期时间
9
- * @param ttlSeconds 以秒为单位的过期时间,-1 表示永不过期,默认 -1,会被回调函数里的 setTtl() 覆盖
10
- *
11
- * @returns 返回包装后的函数,以及缓存相关的额外方法
12
- *
13
- * @example
14
- * // 异步函数示例
15
- * const fetchData = withCache(async function (url: string) {
16
- * const data = await fetch(url).then((res) => res.json());
17
- * this.setTtl(data.expiresIn); // 根据实际情况调整过期时间
18
- * return data;
19
- * });
20
- *
21
- * await fetchData(urlA);
22
- * await fetchData(urlA); // 使用缓存结果
23
- * await fetchData(urlB);
24
- * await fetchData(urlB); // 使用缓存结果
25
- *
26
- * fetchData.clear(); // 清除缓存
27
- * await fetchData(urlA); // 重新请求
28
- * await fetchData(urlB); // 重新请求
29
- *
30
- * // 缓存过期前
31
- * await sleep();
32
- * fetchData.updateTtl(180); // 更新 ttl 并为所有未过期的缓存续期
33
- * await fetchData(urlA); // 使用缓存结果
34
- * await fetchData(urlB); // 使用缓存结果
35
- */
36
- export declare const withCache: <Args extends any[], Result>(fn: (this: {
37
- setTtl: SetTtl;
38
- }, ...args: Args) => Result, ttlSeconds?: number) => {
39
- (...args: Args): Result;
40
- clear(): void;
41
- updateTtl(seconds: number): void;
42
- };
package/dist/index.d.ts DELETED
@@ -1,9 +0,0 @@
1
- export * from "./dom";
2
- export * from "./function";
3
- export * from "./hoc";
4
- export * from "./is";
5
- export * from "./network";
6
- export * from "./number";
7
- export * from "./object";
8
- export * from "./string";
9
- export * from "./time";
package/dist/index.js DELETED
@@ -1,2 +0,0 @@
1
- // @bun
2
- var A=(...I)=>{console.log(`${new Date().toLocaleTimeString()}`,...I)};var B=async(I,$)=>{let{maxRetries:M=5,shouldStop:W=()=>!0}=$??{};for(let z=0;z<M;z++){let H=await I(z);if(W(H))return H}throw Error(`\u8D85\u8FC7\u4E86\u6700\u5927\u5FAA\u73AF\u6B21\u6570\uFF08${M}\uFF09\u4E14\u672A\u6EE1\u8DB3\u505C\u6B62\u6267\u884C\u6761\u4EF6`)};var O=(I,$=-1)=>{let M=new Map,W=(...z)=>{let H=JSON.stringify(z),D=Date.now(),J=M.get(H);if(J&&D<J.expiresAt)return J.value;let G=$===-1?1/0:D+$*1000,Q={setTtl:(X)=>{G=D+X*1000}},_=I.apply(Q,z);if(_ instanceof Promise){let X=_.then((C)=>{return M.set(H,{value:C,expiresAt:G}),C});return M.set(H,{value:X,expiresAt:G}),X}return M.set(H,{value:_,expiresAt:G}),_};return W.clear=()=>M.clear(),W.updateTtl=(z)=>{$=z;let H=Date.now(),D=H+z*1000;for(let[J,G]of M.entries())if(G.expiresAt>H)G.expiresAt=D,M.set(J,G)},W};var Z=(I)=>{return I?.constructor===Object},P=(I)=>{return I===null||I===void 0||typeof I!=="object"&&typeof I!=="function"},V=(I)=>{return!I},N=(I)=>{return!!I},q=(I)=>{return I===null||I===void 0};var E=(I,$)=>{let M={...I};for(let W of Object.keys($)){let z=M[W],H=$[W];if(P(z)&&P(H)){M[W]=H;continue}if(Array.isArray(z)&&Array.isArray(H)){M[W]=z.concat(H);continue}if(Z(z)&&Z(H)){M[W]=E(z,H);continue}M[W]=H}return M},F=(I,$)=>{if(Array.isArray(I))return I.map((M)=>F(M,$));if(Z(I))return Object.keys(I).reduce((W,z)=>{let H=$(z),D=I[z];return W[H]=F(D,$),W},{});return I},T=(I,$,M)=>{let{filter:W}=M??{};if(Array.isArray(I)){let z=I.map((H,D)=>{if(Z(H))return T(H,$,M);return $(H,D)});if(W)return z.filter((H,D)=>W(H,D));return z}if(Z(I))return Object.keys(I).reduce((H,D)=>{let J=I[D],G;if(Z(J)||Array.isArray(J))G=T(J,$,M);else G=$(J,D);if(!W||W(G,D))H[D]=G;return H},{});return I};var d=(I="",$={})=>{let M=async(W,z={})=>{let H=new URL(I?`${I}${W}`:W),{params:D,parser:J,...G}=E($,z);if(Z(D))Object.entries(D).forEach(([X,C])=>{if(q(C))return;H.searchParams.append(X,C.toString())});if(Z(G.body))G.body=JSON.stringify(G.body),G.headers={...G.headers,"Content-Type":"application/json"};let Q=await fetch(H,G);if(!Q.ok){if(Q.headers.get("Content-Type")?.startsWith("application/json"))throw await Q.json();throw Error(Q.statusText)}return await(J?.(Q)??Q.json())};return{get:(W,z)=>M(W,{...z,method:"GET"}),post:(W,z,H)=>M(W,{...H,method:"POST",body:z}),put:(W,z,H)=>M(W,{...H,method:"PUT",body:z}),delete:(W,z)=>M(W,{...z,method:"DELETE"})}},K=async(I)=>{try{return[null,await I]}catch($){return[$,void 0]}},j=async(I)=>{let[$,M]=await K(fetch(I,{method:"HEAD",redirect:"manual"}));if($)return I;return M.headers.get("location")||I};var v=(I,$)=>{return Math.floor(Math.random()*($-I+1))+I};var b=(I)=>{return I.replace(/_([a-zA-Z])/g,($,M)=>M.toUpperCase())},y=(I)=>{return I.replace(/([A-Z])/g,($,M)=>`_${M.toLowerCase()}`)},p=(I)=>{return I.charAt(0).toUpperCase()+I.slice(1)},n=(I)=>{return I.charAt(0).toLowerCase()+I.slice(1)},Y=(I)=>{let $=new Uint8Array(I),M="";for(let W=0;W<$.byteLength;W++)M+=String.fromCharCode($[W]);return btoa(M)},u=async(I,$={})=>{let{quality:M=0.92,compressor:W}=$;if(!I.startsWith("http"))throw Error("\u56FE\u7247\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934");let z=await fetch(I);if(!z.ok)throw Error(`\u83B7\u53D6\u56FE\u7247\u5931\u8D25: ${z.statusText}`);let H=z.headers.get("Content-Type")||"image/jpeg",D=await z.arrayBuffer();if(H!=="image/jpeg"&&H!=="image/png"){let G=Y(D);return`data:${H};base64,${G}`}if(W)return await W(D,H,M);if(typeof OffscreenCanvas<"u"){let G=null;try{let Q=new Blob([D],{type:H});G=await createImageBitmap(Q);let _=new OffscreenCanvas(G.width,G.height),X=_.getContext("2d");if(!X)throw Error("\u65E0\u6CD5\u83B7\u53D6 canvas context");X.drawImage(G,0,0),G.close(),G=null;let x=await(await _.convertToBlob({type:H,quality:M})).arrayBuffer(),S=Y(x);return`data:${H};base64,${S}`}catch{G?.close();let Q=Y(D);return`data:${H};base64,${Q}`}}let J=Y(D);return`data:${H};base64,${J}`},o=(I="",$)=>{if(!I)return"";let{maxLength:M=1/0,disableNewLineReplace:W=!1,disableWhitespaceCollapse:z=!1,omission:H="..."}=$??{},D=I;if(!W)D=D.replace(/\r?\n/g,"\\n");else D=D.replace(/\r?\n/g," ");if(!z)D=D.replace(/\s+/g," ");if(D=D.trim(),M>0&&D.length>M)return D.slice(0,M)+H;return D};var i=async(I=150)=>{return new Promise(($)=>{setTimeout($,I)})},r=(I,$=300)=>{let M=null;return(...W)=>{if(M)clearTimeout(M);M=setTimeout(()=>{I(...W)},$)}},a=(I,$=300)=>{let M=null;return function(...W){if(!M)M=setTimeout(()=>{M=null,I.apply(this,W)},$)}};export{O as withCache,K as to,A as timeLog,a as throttle,b as snakeToCamel,i as sleep,v as randomInt,E as mergeObjects,T as mapValues,F as mapKeys,B as loopUntil,N as isTruthy,P as isPrimitive,Z as isObject,q as isNil,V as isFalsy,u as imageUrlToBase64,j as getRealURL,d as fetcher,n as decapitalize,r as debounce,o as compactStr,p as capitalize,y as camelToSnake};
package/dist/is.d.ts DELETED
@@ -1,43 +0,0 @@
1
- export type Primitive = number | string | boolean | symbol | bigint | undefined | null;
2
- export type Falsy = false | 0 | -0 | 0n | "" | null | undefined;
3
- /**
4
- * 检测传入的值是否为**普通对象**
5
- *
6
- * @example
7
- * const obj = { a: 1 };
8
- * isObject(obj); // true
9
- */
10
- export declare const isObject: (value: any) => value is Record<string, any>;
11
- /**
12
- * 检测传入的值是否为**原始值**(number、string、boolean、symbol、bigint、undefined、null)
13
- *
14
- * @example
15
- * isPrimitive(1); // true
16
- * isPrimitive([]); // false
17
- */
18
- export declare const isPrimitive: (value: any) => value is Primitive;
19
- /**
20
- * 检测传入的值是否为**假值**(false、0、''、null、undefined、NaN等)
21
- *
22
- * @example
23
- * isFalsy(""); // true
24
- * isFalsy(1); // false
25
- */
26
- export declare const isFalsy: (value: any) => value is Falsy;
27
- /**
28
- * 检测传入的值是否为**真值**
29
- *
30
- * @example
31
- * isTruthy(1); // true
32
- * isTruthy(""); // false
33
- */
34
- export declare const isTruthy: (value: any) => value is any;
35
- /**
36
- * 检测传入的值是否为**空值**(null、undefined)
37
- *
38
- * @example
39
- * isNil(null); // true
40
- * isNil(undefined); // true
41
- * isNil(1); // false
42
- */
43
- export declare const isNil: (value: any) => value is null | undefined;
@@ -1,18 +0,0 @@
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
- declare class LRUCache<K, V> {
11
- private cache;
12
- private maxSize;
13
- constructor(maxSize?: number);
14
- get(key: K): V | undefined;
15
- set(key: K, value: V): void;
16
- has(key: K): boolean;
17
- }
18
- export default LRUCache;
package/dist/network.d.ts DELETED
@@ -1,68 +0,0 @@
1
- type BunFetchOptions = {
2
- /** 代理服务器配置(仅 Bun 支持) */
3
- proxy?: string;
4
- };
5
- export type RequestInit = globalThis.RequestInit & BunFetchOptions & {
6
- params?: Record<string, any>;
7
- parser?: (response: Response) => Promise<any>;
8
- };
9
- /**
10
- * 基于 Fetch API 的请求客户端
11
- * @param baseURL 接口前缀
12
- * @param baseOptions 客户端级别的请求体,后续调用时传递相同参数会覆盖上去
13
- *
14
- * @remarks
15
- * 特性:
16
- * - 合并实例、调用时的相同请求体
17
- * - 在 params 里传递对象,自动转换为 queryString
18
- * - 在 body 里传递对象,自动 JSON.stringify
19
- * - 可选择使用 to() 转换请求结果为 [Error, Response]
20
- * - 可选择使用 withCache() 缓存请求结果
21
- * - 支持 proxy 选项(仅在 Bun 环境有效)
22
- *
23
- * @example
24
- *
25
- * // 用法1:直接发送请求
26
- * const res = await fetcher().get<Blog>("https://nickyzj.run:3030/blogs/hello-world");
27
- *
28
- * // 用法2:创建实例
29
- * const api = fetcher("https://nickyzj.run:3030", { headers: { Authorization: "Bearer token" } });
30
- * const res = await api.get<Blog>("/blogs/hello-world", { headers: {...}, params: { page: 1 } }); // 与实例相同的 headers 会覆盖上去,params 会转成 ?page=1 跟到 url 后面
31
- *
32
- * // 用法3:使用代理(仅 Bun 环境)
33
- * const api = fetcher("https://api.example.com", {
34
- * proxy: "http://127.0.0.1:7890"
35
- * });
36
- *
37
- * // 安全处理请求结果
38
- * const [error, data] = await to(api.get<Blog>("/blogs/hello-world"));
39
- * if (error) {
40
- * console.error(error);
41
- * return;
42
- * }
43
- * console.log(data);
44
- *
45
- * // 缓存请求结果
46
- * const getBlogs = withCache(api.get);
47
- * await getBlogs("/blogs");
48
- * await sleep();
49
- * await getBlogs("/blogs"); // 不发请求,使用缓存
50
- */
51
- export declare const fetcher: (baseURL?: string, baseOptions?: RequestInit) => {
52
- get: <T>(url: string, options?: Omit<RequestInit, "method">) => Promise<T>;
53
- post: <T>(url: string, body: any, options?: Omit<RequestInit, "method" | "body">) => Promise<T>;
54
- put: <T>(url: string, body: any, options?: Omit<RequestInit, "method" | "body">) => Promise<T>;
55
- delete: <T>(url: string, options?: Omit<RequestInit, "method" | "body">) => Promise<T>;
56
- };
57
- /**
58
- * Go 语言风格的异步处理方式
59
- * @param promise 一个能被 await 的异步函数
60
- * @returns 如果成功,返回 [null, 异步函数结果],否则返回 [Error, undefined]
61
- *
62
- * @example
63
- * const [error, response] = await to(fetcher().get<Blog>("/blogs/hello-world"));
64
- */
65
- export declare const to: <T, E = Error>(promise: Promise<T>) => Promise<[null, T] | [E, undefined]>;
66
- /** 从 url 响应头获取真实链接 */
67
- export declare const getRealURL: (originURL: string) => Promise<string>;
68
- export {};
package/dist/number.d.ts DELETED
@@ -1,7 +0,0 @@
1
- /**
2
- * 在指定闭区间内生成随机整数
3
- *
4
- * @example
5
- * randomInt(1, 10); // 1 <= x <= 10
6
- */
7
- export declare const randomInt: (min: number, max: number) => number;
package/dist/object.d.ts DELETED
@@ -1,50 +0,0 @@
1
- /**
2
- * 深度合并两个对象,规则如下:
3
- * 1. 原始值覆盖:如果两个值都是原始类型,则用后者覆盖;
4
- * 2. 数组拼接:如果两个值都是数组,则拼接为大数组;
5
- * 3. 对象递归合并:如果两个值都是对象,则进行递归深度合并;
6
- *
7
- * @template T 第一个对象
8
- * @template U 第二个对象
9
- * @param {T} obj1 要合并的第一个对象,相同字段会被 obj2 覆盖
10
- * @param {U} obj2 要合并的第二个对象
11
- */
12
- export declare const mergeObjects: <T extends Record<string, any>, U extends Record<string, any>>(obj1: T, obj2: U) => T & U;
13
- export type DeepMapKeys<T> = T extends Array<infer U> ? Array<DeepMapKeys<U>> : T extends object ? {
14
- [key: string]: DeepMapKeys<T[keyof T]>;
15
- } : T;
16
- /**
17
- * 递归处理对象里的 key
18
- *
19
- * @remarks
20
- * 无法完整推导出类型,只能做到有递归,key 全为 string,value 为同层级的所有类型的联合
21
- *
22
- * @template T 要转换的对象
23
- *
24
- * @example
25
- * const obj = { a: { b: 1 } };
26
- * const result = mapKeys(obj, (key) => key.toUpperCase());
27
- * console.log(result); // { A: { B: 1 } }
28
- */
29
- export declare const mapKeys: <T>(obj: T, getNewKey: (key: string) => string) => DeepMapKeys<T>;
30
- export type DeepMapValues<T, R> = T extends Array<infer U> ? Array<DeepMapValues<U, R>> : T extends object ? {
31
- [K in keyof T]: T[K] extends object ? DeepMapValues<T[K], R> : R;
32
- } : R;
33
- /**
34
- * 递归处理对象里的 value
35
- *
36
- * @remarks
37
- * 无法完整推导出类型,所有 value 最终都会变为 any
38
- *
39
- * @template T 要转换的对象
40
- * @template R 转换后的值类型,为 any,无法进一步推导
41
- *
42
- * @example
43
- * const obj = { a: 1, b: { c: 2 } };
44
- * const result = mapValues(obj, (value, key) => isPrimitive(value) ? value + 1 : value);
45
- * console.log(result); // { a: 2, b: { c: 3 } }
46
- */
47
- export declare const mapValues: <T, R = any>(obj: T, getNewValue: (value: any, key: string | number) => R, options?: {
48
- /** 过滤函数,返回 true 表示保留该字段 */
49
- filter?: (value: any, key: string | number) => boolean;
50
- }) => DeepMapValues<T, R>;
package/dist/string.d.ts DELETED
@@ -1,97 +0,0 @@
1
- export type SnakeToCamel<S extends string> = S extends `${infer Before}_${infer After}` ? After extends `${infer First}${infer Rest}` ? `${Before}${Uppercase<First>}${SnakeToCamel<Rest>}` : Before : S;
2
- /**
3
- * 下划线命名法转为驼峰命名法
4
- *
5
- * @example
6
- * snakeToCamel("user_name") // "userName"
7
- */
8
- export declare const snakeToCamel: <S extends string>(str: S) => SnakeToCamel<S>;
9
- export type CamelToSnake<S extends string> = S extends `${infer First}${infer Rest}` ? Rest extends Uncapitalize<Rest> ? `${Lowercase<First>}${CamelToSnake<Rest>}` : `${Lowercase<First>}_${CamelToSnake<Rest>}` : Lowercase<S>;
10
- /**
11
- * 驼峰命名法转为下划线命名法
12
- *
13
- * @example
14
- * camelToSnake("shouldComponentUpdate") // "should_component_update"
15
- */
16
- export declare const camelToSnake: <S extends string>(str: S) => CamelToSnake<S>;
17
- export type Capitalize<S extends string> = S extends `${infer P1}${infer Rest}` ? P1 extends Capitalize<P1> ? S : `${Uppercase<P1>}${Rest}` : S;
18
- /**
19
- * 字符串首字母大写
20
- *
21
- * @example
22
- * capitalize("hello") // "Hello"
23
- */
24
- export declare const capitalize: <S extends string>(s: S) => Capitalize<S>;
25
- export type Decapitalize<S extends string> = S extends `${infer P1}${infer Rest}` ? P1 extends Lowercase<P1> ? P1 : `${Lowercase<P1>}${Rest}` : S;
26
- /**
27
- * 字符串首字母小写
28
- *
29
- * @example
30
- * decapitalize("Hello") // "hello"
31
- */
32
- export declare const decapitalize: <S extends string>(s: S) => Decapitalize<S>;
33
- /**
34
- * 图片压缩选项
35
- */
36
- export type ImageCompressionOptions = {
37
- /** 压缩比率,默认 0.92 */
38
- quality?: number;
39
- /**
40
- * 自定义压缩函数,用于非浏览器环境(Node.js/Bun)
41
- * 如果提供,将使用此函数替代默认的 canvas 压缩
42
- * @param arrayBuffer 图片的 ArrayBuffer 数据
43
- * @param mime 图片的 MIME 类型
44
- * @param quality 压缩质量
45
- * @returns 压缩后的 base64 字符串
46
- */
47
- compressor?: (arrayBuffer: ArrayBuffer, mime: string, quality: number) => Promise<string> | string;
48
- };
49
- /**
50
- * 图片地址转 base64 数据
51
- *
52
- * @param imageUrl 图片地址
53
- * @param options 可选配置
54
- * @param options.quality 压缩比率,默认 0.92
55
- * @param options.compressor 自定义压缩函数,用于 Node.js/Bun 环境
56
- *
57
- * @example
58
- * // 基本用法(浏览器自动使用 Canvas 压缩)
59
- * imageUrlToBase64("https://example.com/image.jpg");
60
- *
61
- * @example
62
- * // Node.js/Bun 使用 sharp 压缩
63
- * import sharp from "sharp";
64
- *
65
- * imageUrlToBase64("https://example.com/image.jpg", {
66
- * quality: 0.8,
67
- * compressor: async (buffer, mime, quality) => {
68
- * const compressed = await sharp(Buffer.from(buffer))
69
- * .jpeg({ quality: Math.round(quality * 100) })
70
- * .toBuffer();
71
- * return `data:${mime};base64,${compressed.toString("base64")}`;
72
- * }
73
- * });
74
- */
75
- export declare const imageUrlToBase64: (imageUrl: string, options?: ImageCompressionOptions) => Promise<string>;
76
- /**
77
- * 将字符串压缩为单行精简格式
78
- *
79
- * @example
80
- * // "Hello, world."
81
- * compactStr(`
82
- * Hello,
83
- * world!
84
- * `, {
85
- * disableNewLineReplace: false,
86
- * });
87
- */
88
- export declare const compactStr: (text?: string, options?: {
89
- /** 最大保留长度,设为 0 或 Infinity 则不截断,默认 Infinity */
90
- maxLength?: number;
91
- /** 是否将换行符替换为字面量 \n,默认开启 */
92
- disableNewLineReplace?: boolean;
93
- /** 是否合并连续的空格/制表符为一个空格,默认开启 */
94
- disableWhitespaceCollapse?: boolean;
95
- /** 截断后的后缀,默认为 "..." */
96
- omission?: string;
97
- }) => string;
package/dist/time.d.ts DELETED
@@ -1,47 +0,0 @@
1
- /**
2
- * 延迟一段时间再执行后续代码
3
- * @param time 延迟时间,默认 150ms
4
- * @example
5
- * await sleep(1000); // 等待 1 秒执行后续代码
6
- */
7
- export declare const sleep: (time?: number) => Promise<unknown>;
8
- /**
9
- * 防抖:在指定时间内只执行最后一次调用
10
- * @param fn 要防抖的函数
11
- * @param delay 延迟时间,默认 300ms
12
- *
13
- * @remarks
14
- * 连续触发时,只有最后一次会执行。适合用于搜索框输入、窗口大小调整等场景。
15
- * 例如:用户输入"hello"过程中,不会触发搜索,只有停下来时才执行。
16
- *
17
- * 防抖 vs 节流:
18
- * - 防抖:等待触发停止后才执行(最后一次)
19
- * - 节流:按固定节奏执行(每隔多久执行一次)
20
- *
21
- * @example
22
- * const search = debounce((keyword: string) => {
23
- * console.log('搜索:', keyword);
24
- * });
25
- * search('hello'); // 300ms 后执行
26
- */
27
- export declare const debounce: <T extends (...args: any[]) => any>(fn: T, delay?: number) => (...args: Parameters<T>) => void;
28
- /**
29
- * 节流函数 - 在指定时间间隔内最多执行一次调用
30
- * @param fn 要节流的函数
31
- * @param delay 间隔时间,默认 300ms
32
- *
33
- * @remarks
34
- * 节流:连续触发时,按照固定间隔执行。适合用于滚动、拖拽等高频触发场景。
35
- * 例如:滚动页面时,每300ms最多执行一次回调,而不是每次滚动都执行。
36
- *
37
- * 防抖 vs 节流:
38
- * - 防抖:等待触发停止后才执行(最后一次)
39
- * - 节流:按固定节奏执行(每隔多久执行一次)
40
- *
41
- * @example
42
- * const handleScroll = throttle(() => {
43
- * console.log('滚动位置:', window.scrollY);
44
- * }, 200);
45
- * window.addEventListener('scroll', handleScroll);
46
- */
47
- export declare const throttle: <T extends (...args: any[]) => any>(fn: T, delay?: number) => (this: any, ...args: Parameters<T>) => void;
package/src/string.ts DELETED
@@ -1,259 +0,0 @@
1
- export type SnakeToCamel<S extends string> =
2
- S extends `${infer Before}_${infer After}`
3
- ? After extends `${infer First}${infer Rest}`
4
- ? `${Before}${Uppercase<First>}${SnakeToCamel<Rest>}`
5
- : Before
6
- : S;
7
-
8
- /**
9
- * 下划线命名法转为驼峰命名法
10
- *
11
- * @example
12
- * snakeToCamel("user_name") // "userName"
13
- */
14
- export const snakeToCamel = <S extends string>(str: S): SnakeToCamel<S> => {
15
- return str.replace(/_([a-zA-Z])/g, (match, pattern) =>
16
- pattern.toUpperCase(),
17
- ) as SnakeToCamel<S>;
18
- };
19
-
20
- export type CamelToSnake<S extends string> =
21
- S extends `${infer First}${infer Rest}`
22
- ? Rest extends Uncapitalize<Rest>
23
- ? `${Lowercase<First>}${CamelToSnake<Rest>}`
24
- : `${Lowercase<First>}_${CamelToSnake<Rest>}`
25
- : Lowercase<S>;
26
-
27
- /**
28
- * 驼峰命名法转为下划线命名法
29
- *
30
- * @example
31
- * camelToSnake("shouldComponentUpdate") // "should_component_update"
32
- */
33
- export const camelToSnake = <S extends string>(str: S): CamelToSnake<S> => {
34
- return str.replace(
35
- /([A-Z])/g,
36
- (match, pattern) => `_${pattern.toLowerCase()}`,
37
- ) as CamelToSnake<S>;
38
- };
39
-
40
- export type Capitalize<S extends string> = S extends `${infer P1}${infer Rest}`
41
- ? P1 extends Capitalize<P1>
42
- ? S
43
- : `${Uppercase<P1>}${Rest}`
44
- : S;
45
-
46
- /**
47
- * 字符串首字母大写
48
- *
49
- * @example
50
- * capitalize("hello") // "Hello"
51
- */
52
- export const capitalize = <S extends string>(s: S): Capitalize<S> => {
53
- return (s.charAt(0).toUpperCase() + s.slice(1)) as Capitalize<S>;
54
- };
55
-
56
- export type Decapitalize<S extends string> =
57
- S extends `${infer P1}${infer Rest}`
58
- ? P1 extends Lowercase<P1>
59
- ? P1
60
- : `${Lowercase<P1>}${Rest}`
61
- : S;
62
-
63
- /**
64
- * 字符串首字母小写
65
- *
66
- * @example
67
- * decapitalize("Hello") // "hello"
68
- */
69
- export const decapitalize = <S extends string>(s: S): Decapitalize<S> => {
70
- return (s.charAt(0).toLowerCase() + s.slice(1)) as Decapitalize<S>;
71
- };
72
-
73
- /**
74
- * 将 ArrayBuffer 转换为 base64 字符串
75
- * 兼容浏览器、Bun 和 Node.js
76
- */
77
- const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
78
- const bytes = new Uint8Array(buffer);
79
- let binary = "";
80
- for (let i = 0; i < bytes.byteLength; i++) {
81
- binary += String.fromCharCode(bytes[i]!);
82
- }
83
- return btoa(binary);
84
- };
85
-
86
- /**
87
- * 图片压缩选项
88
- */
89
- export type ImageCompressionOptions = {
90
- /** 压缩比率,默认 0.92 */
91
- quality?: number;
92
- /**
93
- * 自定义压缩函数,用于非浏览器环境(Node.js/Bun)
94
- * 如果提供,将使用此函数替代默认的 canvas 压缩
95
- * @param arrayBuffer 图片的 ArrayBuffer 数据
96
- * @param mime 图片的 MIME 类型
97
- * @param quality 压缩质量
98
- * @returns 压缩后的 base64 字符串
99
- */
100
- compressor?: (
101
- arrayBuffer: ArrayBuffer,
102
- mime: string,
103
- quality: number,
104
- ) => Promise<string> | string;
105
- };
106
-
107
- /**
108
- * 图片地址转 base64 数据
109
- *
110
- * @param imageUrl 图片地址
111
- * @param options 可选配置
112
- * @param options.quality 压缩比率,默认 0.92
113
- * @param options.compressor 自定义压缩函数,用于 Node.js/Bun 环境
114
- *
115
- * @example
116
- * // 基本用法(浏览器自动使用 Canvas 压缩)
117
- * imageUrlToBase64("https://example.com/image.jpg");
118
- *
119
- * @example
120
- * // Node.js/Bun 使用 sharp 压缩
121
- * import sharp from "sharp";
122
- *
123
- * imageUrlToBase64("https://example.com/image.jpg", {
124
- * quality: 0.8,
125
- * compressor: async (buffer, mime, quality) => {
126
- * const compressed = await sharp(Buffer.from(buffer))
127
- * .jpeg({ quality: Math.round(quality * 100) })
128
- * .toBuffer();
129
- * return `data:${mime};base64,${compressed.toString("base64")}`;
130
- * }
131
- * });
132
- */
133
- export const imageUrlToBase64 = async (
134
- imageUrl: string,
135
- options: ImageCompressionOptions = {},
136
- ): Promise<string> => {
137
- const { quality = 0.92, compressor } = options;
138
-
139
- if (!imageUrl.startsWith("http")) {
140
- throw new Error("图片地址必须以http或https开头");
141
- }
142
-
143
- // 使用 fetch 获取图片数据
144
- const response = await fetch(imageUrl);
145
- if (!response.ok) {
146
- throw new Error(`获取图片失败: ${response.statusText}`);
147
- }
148
-
149
- const mime = response.headers.get("Content-Type") || "image/jpeg";
150
- const arrayBuffer = await response.arrayBuffer();
151
-
152
- // 对于非 JPEG/PNG 图片,直接返回 base64,不做压缩
153
- if (mime !== "image/jpeg" && mime !== "image/png") {
154
- const base64 = arrayBufferToBase64(arrayBuffer);
155
- return `data:${mime};base64,${base64}`;
156
- }
157
-
158
- // 如果提供了自定义压缩函数(用于 Node.js/Bun 环境),使用它
159
- if (compressor) {
160
- return await compressor(arrayBuffer, mime, quality);
161
- }
162
-
163
- // 浏览器环境:使用 OffscreenCanvas 压缩(Chrome 69+, Firefox 105+, Safari 16.4+)
164
- if (typeof OffscreenCanvas !== "undefined") {
165
- let bitmap: ImageBitmap | null = null;
166
-
167
- try {
168
- const blob = new Blob([arrayBuffer], { type: mime });
169
- bitmap = await createImageBitmap(blob);
170
-
171
- const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
172
- const ctx = canvas.getContext("2d");
173
- if (!ctx) {
174
- throw new Error("无法获取 canvas context");
175
- }
176
-
177
- ctx.drawImage(bitmap, 0, 0);
178
- bitmap.close();
179
- bitmap = null;
180
-
181
- // OffscreenCanvas 使用 convertToBlob 获取压缩后的图片
182
- const compressedBlob = await canvas.convertToBlob({
183
- type: mime,
184
- quality: quality,
185
- });
186
-
187
- // 将 Blob 转换为 base64
188
- const compressedArrayBuffer = await compressedBlob.arrayBuffer();
189
- const base64 = arrayBufferToBase64(compressedArrayBuffer);
190
- return `data:${mime};base64,${base64}`;
191
- } catch {
192
- // Canvas 压缩失败,返回原始 base64
193
- bitmap?.close();
194
- const base64 = arrayBufferToBase64(arrayBuffer);
195
- return `data:${mime};base64,${base64}`;
196
- }
197
- }
198
-
199
- // 非浏览器环境且没有提供压缩器,直接返回原始 base64
200
- const base64 = arrayBufferToBase64(arrayBuffer);
201
- return `data:${mime};base64,${base64}`;
202
- };
203
-
204
- /**
205
- * 将字符串压缩为单行精简格式
206
- *
207
- * @example
208
- * // "Hello, world."
209
- * compactStr(`
210
- * Hello,
211
- * world!
212
- * `, {
213
- * disableNewLineReplace: false,
214
- * });
215
- */
216
- export const compactStr = (
217
- text: string = "",
218
- options?: {
219
- /** 最大保留长度,设为 0 或 Infinity 则不截断,默认 Infinity */
220
- maxLength?: number;
221
- /** 是否将换行符替换为字面量 \n,默认开启 */
222
- disableNewLineReplace?: boolean;
223
- /** 是否合并连续的空格/制表符为一个空格,默认开启 */
224
- disableWhitespaceCollapse?: boolean;
225
- /** 截断后的后缀,默认为 "..." */
226
- omission?: string;
227
- },
228
- ): string => {
229
- if (!text) return "";
230
-
231
- const {
232
- maxLength = Infinity,
233
- disableNewLineReplace = false,
234
- disableWhitespaceCollapse = false,
235
- omission = "...",
236
- } = options ?? {};
237
-
238
- let result = text;
239
-
240
- // 处理换行符
241
- if (!disableNewLineReplace) {
242
- result = result.replace(/\r?\n/g, "\\n");
243
- } else {
244
- result = result.replace(/\r?\n/g, " ");
245
- }
246
-
247
- // 合并连续空格
248
- if (!disableWhitespaceCollapse) {
249
- result = result.replace(/\s+/g, " ");
250
- }
251
- result = result.trim();
252
-
253
- // 截断多出来的文字
254
- if (maxLength > 0 && result.length > maxLength) {
255
- return result.slice(0, maxLength) + omission;
256
- }
257
-
258
- return result;
259
- };