slimjson 1.0.4 → 1.1.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/.claude/settings.local.json +7 -2
- package/.idea/git_toolbox_prj.xml +15 -0
- package/.idea/modules.xml +8 -0
- package/.idea/slimjson.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +519 -361
- package/README_EN.md +508 -350
- package/compress-file.js +41 -41
- package/compress-ratio.js +70 -70
- package/compress-test.js +436 -436
- package/compress.js +268 -146
- package/decompress-file.js +42 -42
- package/esm.mjs +5 -4
- package/package.json +24 -24
- package/test.js +975 -975
- package/data/searchGroup.json +0 -96365
package/compress-test.js
CHANGED
|
@@ -1,436 +1,436 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 压缩率测试脚本
|
|
3
|
-
* 运行:node compress-test.js
|
|
4
|
-
*/
|
|
5
|
-
const { compress, decompress, stringify } = require('./compress');
|
|
6
|
-
|
|
7
|
-
// ============================================================
|
|
8
|
-
// 工具函数
|
|
9
|
-
// ============================================================
|
|
10
|
-
|
|
11
|
-
/** 计算JSON字符串字节大小 */
|
|
12
|
-
function getByteSize(obj) {
|
|
13
|
-
return Buffer.byteLength(JSON.stringify(obj), 'utf8');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** 格式化字节数 */
|
|
17
|
-
function formatBytes(bytes) {
|
|
18
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
19
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
20
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** 随机整数 [min, max] */
|
|
24
|
-
function randInt(min, max) {
|
|
25
|
-
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 随机选择数组元素 */
|
|
29
|
-
function randChoice(arr) {
|
|
30
|
-
return arr[randInt(0, arr.length - 1)];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 随机中文名 */
|
|
34
|
-
function randChineseName() {
|
|
35
|
-
const surnames = ['张', '李', '王', '刘', '陈', '杨', '黄', '赵', '周', '吴', '郑', '孙', '马', '朱', '胡', '林', '郭', '何', '罗', '高'];
|
|
36
|
-
const names = ['伟', '芳', '娜', '敏', '静', '丽', '强', '磊', '军', '洋', '勇', '艳', '杰', '涛', '明', '超', '秀英', '华', '平', '刚', '玉兰', '桂英', '秀珍', '婷', '浩', '宇', '欣', '怡', '子轩', '子涵', '梓萱', '一诺'];
|
|
37
|
-
return randChoice(surnames) + randChoice(names) + (Math.random() > 0.5 ? randChoice(names) : '');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** 随机邮箱 */
|
|
41
|
-
function randEmail() {
|
|
42
|
-
const domains = ['qq.com', '163.com', 'gmail.com', 'outlook.com', 'icloud.com'];
|
|
43
|
-
const prefix = 'user' + randInt(1000, 9999);
|
|
44
|
-
return `${prefix}@${randChoice(domains)}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 随机手机号 */
|
|
48
|
-
function randPhone() {
|
|
49
|
-
const prefixes = ['138', '139', '186', '187', '150', '151', '177', '188'];
|
|
50
|
-
return randChoice(prefixes) + randInt(10000000, 99999999).toString();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** 随机日期字符串 */
|
|
54
|
-
function randDate(startYear = 1990, endYear = 2024) {
|
|
55
|
-
const year = randInt(startYear, endYear);
|
|
56
|
-
const month = randInt(1, 12);
|
|
57
|
-
const day = randInt(1, 28);
|
|
58
|
-
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** 随机地址 */
|
|
62
|
-
function randAddress() {
|
|
63
|
-
const cities = ['北京市', '上海市', '广州市', '深圳市', '杭州市', '南京市', '武汉市', '成都市', '西安市', '重庆市'];
|
|
64
|
-
const districts = ['朝阳区', '海淀区', '浦东新区', '天河区', '南山区', '西湖区', '江宁区', '洪山区', '雁塔区', '渝北区'];
|
|
65
|
-
return randChoice(cities) + randChoice(districts) + randInt(1, 999) + '号';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ============================================================
|
|
69
|
-
// 数据生成器
|
|
70
|
-
// ============================================================
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 1. 简单用户数组
|
|
74
|
-
* - 字段:id, name, age, email, phone, address, createdAt
|
|
75
|
-
*/
|
|
76
|
-
function generateSimpleUsers(count) {
|
|
77
|
-
const users = [];
|
|
78
|
-
for (let i = 1; i <= count; i++) {
|
|
79
|
-
users.push({
|
|
80
|
-
id: i,
|
|
81
|
-
name: randChineseName(),
|
|
82
|
-
age: randInt(18, 70),
|
|
83
|
-
email: randEmail(),
|
|
84
|
-
phone: randPhone(),
|
|
85
|
-
address: randAddress(),
|
|
86
|
-
createdAt: randDate(2020, 2024)
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
return users;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* 2. 带嵌套对象的用户数组
|
|
94
|
-
* - 字段:id, name, profile: { avatar, bio, website, social: { weibo, wechat } }
|
|
95
|
-
*/
|
|
96
|
-
function generateNestedUsers(count) {
|
|
97
|
-
const users = [];
|
|
98
|
-
for (let i = 1; i <= count; i++) {
|
|
99
|
-
users.push({
|
|
100
|
-
id: i,
|
|
101
|
-
name: randChineseName(),
|
|
102
|
-
profile: {
|
|
103
|
-
avatar: `https://example.com/avatar/${randInt(1, 1000)}.jpg`,
|
|
104
|
-
bio: `这是第${i}个用户的个人简介,来自${randAddress()}`,
|
|
105
|
-
website: Math.random() > 0.5 ? `https://user${i}.example.com` : null,
|
|
106
|
-
social: {
|
|
107
|
-
weibo: Math.random() > 0.3 ? `weibo_${randInt(10000, 99999)}` : null,
|
|
108
|
-
wechat: Math.random() > 0.3 ? `wx_${randInt(10000, 99999)}` : null
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
return users;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* 3. 带对象数组的订单数组
|
|
118
|
-
* - 字段:orderId, customer, items: [{ productId, name, price, quantity }], total, status
|
|
119
|
-
*/
|
|
120
|
-
function generateOrders(count, itemsPerOrder = [1, 5]) {
|
|
121
|
-
const statuses = ['pending', 'paid', 'shipped', 'delivered', 'cancelled'];
|
|
122
|
-
const orders = [];
|
|
123
|
-
for (let i = 1; i <= count; i++) {
|
|
124
|
-
const itemCount = randInt(itemsPerOrder[0], itemsPerOrder[1]);
|
|
125
|
-
const items = [];
|
|
126
|
-
let total = 0;
|
|
127
|
-
for (let j = 1; j <= itemCount; j++) {
|
|
128
|
-
const price = randInt(10, 1000) * 100; // 分为单位
|
|
129
|
-
const quantity = randInt(1, 5);
|
|
130
|
-
items.push({
|
|
131
|
-
productId: `PROD-${randInt(10000, 99999)}`,
|
|
132
|
-
name: `商品${randInt(1, 1000)}`,
|
|
133
|
-
price: price,
|
|
134
|
-
quantity: quantity
|
|
135
|
-
});
|
|
136
|
-
total += price * quantity;
|
|
137
|
-
}
|
|
138
|
-
orders.push({
|
|
139
|
-
orderId: `ORD-${randInt(100000, 999999)}`,
|
|
140
|
-
customer: randChineseName(),
|
|
141
|
-
items: items,
|
|
142
|
-
total: total,
|
|
143
|
-
status: randChoice(statuses),
|
|
144
|
-
createdAt: randDate(2023, 2024)
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
return orders;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* 4. 复杂嵌套结构 - 学校数据
|
|
152
|
-
* - 年级 -> 班级 -> 学生 -> 成绩、家长信息
|
|
153
|
-
*/
|
|
154
|
-
function generateSchoolData(gradeCount, classPerGrade, studentPerClass) {
|
|
155
|
-
const grades = ['一年级', '二年级', '三年级', '四年级', '五年级', '六年级'];
|
|
156
|
-
const subjects = ['语文', '数学', '英语', '科学', '体育', '美术'];
|
|
157
|
-
const genders = ['男', '女'];
|
|
158
|
-
const data = [];
|
|
159
|
-
|
|
160
|
-
for (let g = 0; g < gradeCount; g++) {
|
|
161
|
-
for (let c = 1; c <= classPerGrade; c++) {
|
|
162
|
-
const students = [];
|
|
163
|
-
for (let s = 1; s <= studentPerClass; s++) {
|
|
164
|
-
// 随机缺失某些成绩
|
|
165
|
-
const scores = {};
|
|
166
|
-
for (const subject of subjects) {
|
|
167
|
-
if (Math.random() > 0.2) { // 80%概率有成绩
|
|
168
|
-
scores[subject] = randInt(60, 100);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const parents = [];
|
|
173
|
-
const parentCount = randInt(1, 2);
|
|
174
|
-
for (let p = 0; p < parentCount; p++) {
|
|
175
|
-
parents.push({
|
|
176
|
-
name: randChineseName(),
|
|
177
|
-
relationship: p === 0 ? '父亲' : '母亲',
|
|
178
|
-
phone: randPhone(),
|
|
179
|
-
occupation: randChoice(['教师', '医生', '工程师', '商人', '职员', '自由职业'])
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
students.push({
|
|
184
|
-
id: `${g}${c}${s.toString().padStart(2, '0')}`,
|
|
185
|
-
name: randChineseName(),
|
|
186
|
-
gender: randChoice(genders),
|
|
187
|
-
age: g + 7,
|
|
188
|
-
scores: scores,
|
|
189
|
-
parents: parents,
|
|
190
|
-
address: Math.random() > 0.3 ? randAddress() : null
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
data.push({
|
|
195
|
-
grade: grades[g] || `${g + 1}年级`,
|
|
196
|
-
class: `${c}班`,
|
|
197
|
-
classTeacher: {
|
|
198
|
-
name: randChineseName(),
|
|
199
|
-
age: randInt(30, 55),
|
|
200
|
-
phone: randPhone(),
|
|
201
|
-
subjects: [randChoice(subjects.slice(0, 3))]
|
|
202
|
-
},
|
|
203
|
-
students: students
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return data;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* 5. 稀疏字段数组 - 模拟后端返回不完整数据
|
|
212
|
-
* - 每个对象有不同的字段子集
|
|
213
|
-
*/
|
|
214
|
-
function generateSparseData(count, totalFields = 20) {
|
|
215
|
-
const fieldNames = [];
|
|
216
|
-
for (let i = 1; i <= totalFields; i++) {
|
|
217
|
-
fieldNames.push(`field_${i}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const data = [];
|
|
221
|
-
for (let i = 1; i <= count; i++) {
|
|
222
|
-
const obj = { id: i };
|
|
223
|
-
// 每个对象随机选择50%-80%的字段
|
|
224
|
-
const fieldCount = randInt(Math.floor(totalFields * 0.5), Math.floor(totalFields * 0.8));
|
|
225
|
-
const selectedFields = [...fieldNames].sort(() => Math.random() - 0.5).slice(0, fieldCount);
|
|
226
|
-
|
|
227
|
-
for (const field of selectedFields) {
|
|
228
|
-
obj[field] = randInt(0, 1000);
|
|
229
|
-
}
|
|
230
|
-
data.push(obj);
|
|
231
|
-
}
|
|
232
|
-
return data;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* 6. 深层嵌套结构
|
|
237
|
-
* - organization -> departments -> teams -> members -> tasks
|
|
238
|
-
*/
|
|
239
|
-
function generateDeepNested(orgCount = 2, deptPerOrg = 3, teamPerDept = 4, memberPerTeam = 5) {
|
|
240
|
-
const data = [];
|
|
241
|
-
for (let o = 1; o <= orgCount; o++) {
|
|
242
|
-
const departments = [];
|
|
243
|
-
for (let d = 1; d <= deptPerOrg; d++) {
|
|
244
|
-
const teams = [];
|
|
245
|
-
for (let t = 1; t <= teamPerDept; t++) {
|
|
246
|
-
const members = [];
|
|
247
|
-
for (let m = 1; m <= memberPerTeam; m++) {
|
|
248
|
-
const taskCount = randInt(1, 5);
|
|
249
|
-
const tasks = [];
|
|
250
|
-
for (let tk = 1; tk <= taskCount; tk++) {
|
|
251
|
-
tasks.push({
|
|
252
|
-
taskId: `TASK-${randInt(10000, 99999)}`,
|
|
253
|
-
title: `任务${tk}`,
|
|
254
|
-
status: randChoice(['todo', 'in_progress', 'done']),
|
|
255
|
-
priority: randChoice(['low', 'medium', 'high']),
|
|
256
|
-
dueDate: randDate(2024, 2025)
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
members.push({
|
|
260
|
-
memberId: `M${o}${d}${t}${m}`,
|
|
261
|
-
name: randChineseName(),
|
|
262
|
-
role: randChoice(['leader', 'member', 'intern']),
|
|
263
|
-
tasks: tasks,
|
|
264
|
-
skills: randInt(1, 5) > 2 ? ['JavaScript', 'Python', 'Go', 'Rust'].slice(0, randInt(1, 4)) : []
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
teams.push({
|
|
268
|
-
teamId: `TEAM-${o}-${d}-${t}`,
|
|
269
|
-
name: `团队${t}`,
|
|
270
|
-
members: members
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
departments.push({
|
|
274
|
-
deptId: `DEPT-${o}-${d}`,
|
|
275
|
-
name: `部门${d}`,
|
|
276
|
-
teams: teams
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
data.push({
|
|
280
|
-
orgId: o,
|
|
281
|
-
orgName: `组织${o}`,
|
|
282
|
-
departments: departments
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
return data;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ============================================================
|
|
289
|
-
// 测试执行
|
|
290
|
-
// ============================================================
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* 验证解压正确性:
|
|
294
|
-
* - compress → decompress → compress 应该得到相同结果(roundtrip)
|
|
295
|
-
* - 缺失字段会被填充为 null,这是预期的规范化行为
|
|
296
|
-
*/
|
|
297
|
-
function verifyRoundtrip(original, compressed, opts) {
|
|
298
|
-
const decompressed = decompress(compressed);
|
|
299
|
-
const recompressed = compress(decompressed, opts);
|
|
300
|
-
// 二次压缩后结构应该完全一致
|
|
301
|
-
return JSON.stringify(compressed) === JSON.stringify(recompressed);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function runTest(name, data) {
|
|
305
|
-
const originalSize = getByteSize(data);
|
|
306
|
-
|
|
307
|
-
// 默认(不 trim)
|
|
308
|
-
const compressed = compress(data);
|
|
309
|
-
const compressedSize = Buffer.byteLength(stringify(compressed), 'utf8');
|
|
310
|
-
const isCorrect = verifyRoundtrip(data, compressed);
|
|
311
|
-
const ratio = ((originalSize - compressedSize) / originalSize * 100).toFixed(2);
|
|
312
|
-
|
|
313
|
-
// trimTrailingNulls
|
|
314
|
-
const compressedTrim = compress(data, { trimTrailingNulls: true });
|
|
315
|
-
const compressedTrimSize = Buffer.byteLength(stringify(compressedTrim), 'utf8');
|
|
316
|
-
const isCorrectTrim = verifyRoundtrip(data, compressedTrim, { trimTrailingNulls: true });
|
|
317
|
-
const ratioTrim = ((originalSize - compressedTrimSize) / originalSize * 100).toFixed(2);
|
|
318
|
-
|
|
319
|
-
const diff = compressedSize - compressedTrimSize;
|
|
320
|
-
const diffStr = diff > 0 ? `-${formatBytes(diff)}` : diff === 0 ? '—' : `+${formatBytes(-diff)}`;
|
|
321
|
-
|
|
322
|
-
console.log(`\n${'='.repeat(72)}`);
|
|
323
|
-
console.log(`测试: ${name}`);
|
|
324
|
-
console.log('-'.repeat(72));
|
|
325
|
-
console.log(`对象数量: ${data.length}`);
|
|
326
|
-
console.log(`原始大小: ${formatBytes(originalSize)}`);
|
|
327
|
-
console.log(`不 trim: ${formatBytes(compressedSize).padStart(10)} (${ratio}%) ${isCorrect ? '✓' : '✗'}`);
|
|
328
|
-
console.log(`trim: ${formatBytes(compressedTrimSize).padStart(10)} (${ratioTrim}%) ${isCorrectTrim ? '✓' : '✗'}`);
|
|
329
|
-
console.log(`差值: ${diffStr}`);
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
name,
|
|
333
|
-
count: data.length,
|
|
334
|
-
originalSize,
|
|
335
|
-
compressedSize,
|
|
336
|
-
ratio: parseFloat(ratio),
|
|
337
|
-
compressedTrimSize,
|
|
338
|
-
ratioTrim: parseFloat(ratioTrim),
|
|
339
|
-
diff,
|
|
340
|
-
isCorrect
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function main() {
|
|
345
|
-
console.log('\n╔══════════════════════════════════════════════════════════╗');
|
|
346
|
-
console.log('║ JSON 数组压缩率测试 ║');
|
|
347
|
-
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
348
|
-
|
|
349
|
-
const results = [];
|
|
350
|
-
|
|
351
|
-
// 1. 简单用户数组 - 不同规模
|
|
352
|
-
console.log('\n\n【一、简单用户数组测试】');
|
|
353
|
-
results.push(runTest('简单用户 100条', generateSimpleUsers(100)));
|
|
354
|
-
results.push(runTest('简单用户 1000条', generateSimpleUsers(1000)));
|
|
355
|
-
results.push(runTest('简单用户 10000条', generateSimpleUsers(10000)));
|
|
356
|
-
|
|
357
|
-
// 2. 嵌套对象数组
|
|
358
|
-
console.log('\n\n【二、嵌套对象数组测试】');
|
|
359
|
-
results.push(runTest('嵌套用户 100条', generateNestedUsers(100)));
|
|
360
|
-
results.push(runTest('嵌套用户 1000条', generateNestedUsers(1000)));
|
|
361
|
-
results.push(runTest('嵌套用户 5000条', generateNestedUsers(5000)));
|
|
362
|
-
|
|
363
|
-
// 3. 带对象数组的订单
|
|
364
|
-
console.log('\n\n【三、订单数组测试(每单1-5商品)】');
|
|
365
|
-
results.push(runTest('订单 100条', generateOrders(100)));
|
|
366
|
-
results.push(runTest('订单 500条', generateOrders(500)));
|
|
367
|
-
results.push(runTest('订单 2000条', generateOrders(2000)));
|
|
368
|
-
|
|
369
|
-
// 4. 学校数据 - 复杂嵌套
|
|
370
|
-
console.log('\n\n【四、学校数据测试(复杂嵌套)】');
|
|
371
|
-
results.push(runTest('学校数据 小(2年级×2班×10生)', generateSchoolData(2, 2, 10)));
|
|
372
|
-
results.push(runTest('学校数据 中(6年级×4班×30生)', generateSchoolData(6, 4, 30)));
|
|
373
|
-
results.push(runTest('学校数据 大(6年级×6班×50生)', generateSchoolData(6, 6, 50)));
|
|
374
|
-
|
|
375
|
-
// 5. 稀疏字段数组
|
|
376
|
-
console.log('\n\n【五、稀疏字段数组测试】');
|
|
377
|
-
results.push(runTest('稀疏字段 100条×20字段', generateSparseData(100, 20)));
|
|
378
|
-
results.push(runTest('稀疏字段 500条×30字段', generateSparseData(500, 30)));
|
|
379
|
-
results.push(runTest('稀疏字段 2000条×50字段', generateSparseData(2000, 50)));
|
|
380
|
-
|
|
381
|
-
// 6. 深层嵌套
|
|
382
|
-
console.log('\n\n【六、深层嵌套测试】');
|
|
383
|
-
results.push(runTest('深层嵌套 小', generateDeepNested(2, 2, 3, 4)));
|
|
384
|
-
results.push(runTest('深层嵌套 中', generateDeepNested(3, 4, 5, 6)));
|
|
385
|
-
results.push(runTest('深层嵌套 大', generateDeepNested(5, 5, 8, 8)));
|
|
386
|
-
|
|
387
|
-
// 汇总
|
|
388
|
-
console.log('\n\n');
|
|
389
|
-
console.log('╔══════════════════════════════════════════════════════════════════════════╗');
|
|
390
|
-
console.log('║ 测试结果汇总 ║');
|
|
391
|
-
console.log('╚══════════════════════════════════════════════════════════════════════════╝');
|
|
392
|
-
console.log('\n');
|
|
393
|
-
|
|
394
|
-
const avgRatio = results.reduce((sum, r) => sum + r.ratio, 0) / results.length;
|
|
395
|
-
const avgRatioTrim = results.reduce((sum, r) => sum + r.ratioTrim, 0) / results.length;
|
|
396
|
-
const totalDiff = results.reduce((sum, r) => sum + r.diff, 0);
|
|
397
|
-
const bestCase = results.reduce((best, r) => r.ratioTrim > best.ratioTrim ? r : best);
|
|
398
|
-
const worstCase = results.reduce((worst, r) => r.ratioTrim < worst.ratioTrim ? r : worst);
|
|
399
|
-
|
|
400
|
-
console.log(`总测试数: ${results.length}`);
|
|
401
|
-
console.log(`平均压缩率(不 trim): ${avgRatio.toFixed(2)}%`);
|
|
402
|
-
console.log(`平均压缩率(trim): ${avgRatioTrim.toFixed(2)}%`);
|
|
403
|
-
console.log(`总节省: ${formatBytes(totalDiff)}`);
|
|
404
|
-
console.log(`最佳压缩: ${bestCase.name} (trim ${bestCase.ratioTrim}%)`);
|
|
405
|
-
console.log(`最差压缩: ${worstCase.name} (trim ${worstCase.ratioTrim}%)`);
|
|
406
|
-
|
|
407
|
-
console.log('\n详细结果:');
|
|
408
|
-
console.log('-'.repeat(100));
|
|
409
|
-
console.log(
|
|
410
|
-
`${'测试名称'.padEnd(34)} ${'数量'.padStart(6)}` +
|
|
411
|
-
`${'原始'.padStart(12)} ${'不 trim'.padStart(12)} ${'压缩率'.padStart(7)}` +
|
|
412
|
-
`${'trim'.padStart(12)} ${'压缩率'.padStart(7)} ${'差值'.padStart(12)}`
|
|
413
|
-
);
|
|
414
|
-
console.log('-'.repeat(100));
|
|
415
|
-
for (const r of results) {
|
|
416
|
-
const diffStr = r.diff > 0 ? `-${formatBytes(r.diff)}` : r.diff === 0 ? '—' : `+${formatBytes(-r.diff)}`;
|
|
417
|
-
console.log(
|
|
418
|
-
`${r.name.padEnd(34)} ${r.count.toString().padStart(6)}` +
|
|
419
|
-
`${formatBytes(r.originalSize).padStart(12)}` +
|
|
420
|
-
`${formatBytes(r.compressedSize).padStart(12)} ${r.ratio.toString().padStart(6)}%` +
|
|
421
|
-
`${formatBytes(r.compressedTrimSize).padStart(12)} ${r.ratioTrim.toString().padStart(6)}%` +
|
|
422
|
-
`${diffStr.padStart(12)}`
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
console.log('\n结论:');
|
|
427
|
-
console.log('-'.repeat(70));
|
|
428
|
-
console.log('1. 字段名越长、数量越多,压缩效果越好');
|
|
429
|
-
console.log('2. 嵌套结构(对象数组)压缩效果显著');
|
|
430
|
-
console.log('3. 稀疏字段(缺失字段多)压缩率反而最高(null 被省略为空槽)');
|
|
431
|
-
console.log('4. 原始类型数组字段不会被压缩(保持原样)');
|
|
432
|
-
console.log('5. 深层嵌套结构能获得更好的压缩效果');
|
|
433
|
-
console.log('6. 安全的字符串省略引号(key + value),进一步减少文本体积');
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
main();
|
|
1
|
+
/**
|
|
2
|
+
* 压缩率测试脚本
|
|
3
|
+
* 运行:node compress-test.js
|
|
4
|
+
*/
|
|
5
|
+
const { compress, decompress, stringify } = require('./compress');
|
|
6
|
+
|
|
7
|
+
// ============================================================
|
|
8
|
+
// 工具函数
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
/** 计算JSON字符串字节大小 */
|
|
12
|
+
function getByteSize(obj) {
|
|
13
|
+
return Buffer.byteLength(JSON.stringify(obj), 'utf8');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 格式化字节数 */
|
|
17
|
+
function formatBytes(bytes) {
|
|
18
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
19
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
20
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 随机整数 [min, max] */
|
|
24
|
+
function randInt(min, max) {
|
|
25
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 随机选择数组元素 */
|
|
29
|
+
function randChoice(arr) {
|
|
30
|
+
return arr[randInt(0, arr.length - 1)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 随机中文名 */
|
|
34
|
+
function randChineseName() {
|
|
35
|
+
const surnames = ['张', '李', '王', '刘', '陈', '杨', '黄', '赵', '周', '吴', '郑', '孙', '马', '朱', '胡', '林', '郭', '何', '罗', '高'];
|
|
36
|
+
const names = ['伟', '芳', '娜', '敏', '静', '丽', '强', '磊', '军', '洋', '勇', '艳', '杰', '涛', '明', '超', '秀英', '华', '平', '刚', '玉兰', '桂英', '秀珍', '婷', '浩', '宇', '欣', '怡', '子轩', '子涵', '梓萱', '一诺'];
|
|
37
|
+
return randChoice(surnames) + randChoice(names) + (Math.random() > 0.5 ? randChoice(names) : '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 随机邮箱 */
|
|
41
|
+
function randEmail() {
|
|
42
|
+
const domains = ['qq.com', '163.com', 'gmail.com', 'outlook.com', 'icloud.com'];
|
|
43
|
+
const prefix = 'user' + randInt(1000, 9999);
|
|
44
|
+
return `${prefix}@${randChoice(domains)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 随机手机号 */
|
|
48
|
+
function randPhone() {
|
|
49
|
+
const prefixes = ['138', '139', '186', '187', '150', '151', '177', '188'];
|
|
50
|
+
return randChoice(prefixes) + randInt(10000000, 99999999).toString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 随机日期字符串 */
|
|
54
|
+
function randDate(startYear = 1990, endYear = 2024) {
|
|
55
|
+
const year = randInt(startYear, endYear);
|
|
56
|
+
const month = randInt(1, 12);
|
|
57
|
+
const day = randInt(1, 28);
|
|
58
|
+
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 随机地址 */
|
|
62
|
+
function randAddress() {
|
|
63
|
+
const cities = ['北京市', '上海市', '广州市', '深圳市', '杭州市', '南京市', '武汉市', '成都市', '西安市', '重庆市'];
|
|
64
|
+
const districts = ['朝阳区', '海淀区', '浦东新区', '天河区', '南山区', '西湖区', '江宁区', '洪山区', '雁塔区', '渝北区'];
|
|
65
|
+
return randChoice(cities) + randChoice(districts) + randInt(1, 999) + '号';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================
|
|
69
|
+
// 数据生成器
|
|
70
|
+
// ============================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 1. 简单用户数组
|
|
74
|
+
* - 字段:id, name, age, email, phone, address, createdAt
|
|
75
|
+
*/
|
|
76
|
+
function generateSimpleUsers(count) {
|
|
77
|
+
const users = [];
|
|
78
|
+
for (let i = 1; i <= count; i++) {
|
|
79
|
+
users.push({
|
|
80
|
+
id: i,
|
|
81
|
+
name: randChineseName(),
|
|
82
|
+
age: randInt(18, 70),
|
|
83
|
+
email: randEmail(),
|
|
84
|
+
phone: randPhone(),
|
|
85
|
+
address: randAddress(),
|
|
86
|
+
createdAt: randDate(2020, 2024)
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return users;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 2. 带嵌套对象的用户数组
|
|
94
|
+
* - 字段:id, name, profile: { avatar, bio, website, social: { weibo, wechat } }
|
|
95
|
+
*/
|
|
96
|
+
function generateNestedUsers(count) {
|
|
97
|
+
const users = [];
|
|
98
|
+
for (let i = 1; i <= count; i++) {
|
|
99
|
+
users.push({
|
|
100
|
+
id: i,
|
|
101
|
+
name: randChineseName(),
|
|
102
|
+
profile: {
|
|
103
|
+
avatar: `https://example.com/avatar/${randInt(1, 1000)}.jpg`,
|
|
104
|
+
bio: `这是第${i}个用户的个人简介,来自${randAddress()}`,
|
|
105
|
+
website: Math.random() > 0.5 ? `https://user${i}.example.com` : null,
|
|
106
|
+
social: {
|
|
107
|
+
weibo: Math.random() > 0.3 ? `weibo_${randInt(10000, 99999)}` : null,
|
|
108
|
+
wechat: Math.random() > 0.3 ? `wx_${randInt(10000, 99999)}` : null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return users;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 3. 带对象数组的订单数组
|
|
118
|
+
* - 字段:orderId, customer, items: [{ productId, name, price, quantity }], total, status
|
|
119
|
+
*/
|
|
120
|
+
function generateOrders(count, itemsPerOrder = [1, 5]) {
|
|
121
|
+
const statuses = ['pending', 'paid', 'shipped', 'delivered', 'cancelled'];
|
|
122
|
+
const orders = [];
|
|
123
|
+
for (let i = 1; i <= count; i++) {
|
|
124
|
+
const itemCount = randInt(itemsPerOrder[0], itemsPerOrder[1]);
|
|
125
|
+
const items = [];
|
|
126
|
+
let total = 0;
|
|
127
|
+
for (let j = 1; j <= itemCount; j++) {
|
|
128
|
+
const price = randInt(10, 1000) * 100; // 分为单位
|
|
129
|
+
const quantity = randInt(1, 5);
|
|
130
|
+
items.push({
|
|
131
|
+
productId: `PROD-${randInt(10000, 99999)}`,
|
|
132
|
+
name: `商品${randInt(1, 1000)}`,
|
|
133
|
+
price: price,
|
|
134
|
+
quantity: quantity
|
|
135
|
+
});
|
|
136
|
+
total += price * quantity;
|
|
137
|
+
}
|
|
138
|
+
orders.push({
|
|
139
|
+
orderId: `ORD-${randInt(100000, 999999)}`,
|
|
140
|
+
customer: randChineseName(),
|
|
141
|
+
items: items,
|
|
142
|
+
total: total,
|
|
143
|
+
status: randChoice(statuses),
|
|
144
|
+
createdAt: randDate(2023, 2024)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return orders;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 4. 复杂嵌套结构 - 学校数据
|
|
152
|
+
* - 年级 -> 班级 -> 学生 -> 成绩、家长信息
|
|
153
|
+
*/
|
|
154
|
+
function generateSchoolData(gradeCount, classPerGrade, studentPerClass) {
|
|
155
|
+
const grades = ['一年级', '二年级', '三年级', '四年级', '五年级', '六年级'];
|
|
156
|
+
const subjects = ['语文', '数学', '英语', '科学', '体育', '美术'];
|
|
157
|
+
const genders = ['男', '女'];
|
|
158
|
+
const data = [];
|
|
159
|
+
|
|
160
|
+
for (let g = 0; g < gradeCount; g++) {
|
|
161
|
+
for (let c = 1; c <= classPerGrade; c++) {
|
|
162
|
+
const students = [];
|
|
163
|
+
for (let s = 1; s <= studentPerClass; s++) {
|
|
164
|
+
// 随机缺失某些成绩
|
|
165
|
+
const scores = {};
|
|
166
|
+
for (const subject of subjects) {
|
|
167
|
+
if (Math.random() > 0.2) { // 80%概率有成绩
|
|
168
|
+
scores[subject] = randInt(60, 100);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const parents = [];
|
|
173
|
+
const parentCount = randInt(1, 2);
|
|
174
|
+
for (let p = 0; p < parentCount; p++) {
|
|
175
|
+
parents.push({
|
|
176
|
+
name: randChineseName(),
|
|
177
|
+
relationship: p === 0 ? '父亲' : '母亲',
|
|
178
|
+
phone: randPhone(),
|
|
179
|
+
occupation: randChoice(['教师', '医生', '工程师', '商人', '职员', '自由职业'])
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
students.push({
|
|
184
|
+
id: `${g}${c}${s.toString().padStart(2, '0')}`,
|
|
185
|
+
name: randChineseName(),
|
|
186
|
+
gender: randChoice(genders),
|
|
187
|
+
age: g + 7,
|
|
188
|
+
scores: scores,
|
|
189
|
+
parents: parents,
|
|
190
|
+
address: Math.random() > 0.3 ? randAddress() : null
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
data.push({
|
|
195
|
+
grade: grades[g] || `${g + 1}年级`,
|
|
196
|
+
class: `${c}班`,
|
|
197
|
+
classTeacher: {
|
|
198
|
+
name: randChineseName(),
|
|
199
|
+
age: randInt(30, 55),
|
|
200
|
+
phone: randPhone(),
|
|
201
|
+
subjects: [randChoice(subjects.slice(0, 3))]
|
|
202
|
+
},
|
|
203
|
+
students: students
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return data;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 5. 稀疏字段数组 - 模拟后端返回不完整数据
|
|
212
|
+
* - 每个对象有不同的字段子集
|
|
213
|
+
*/
|
|
214
|
+
function generateSparseData(count, totalFields = 20) {
|
|
215
|
+
const fieldNames = [];
|
|
216
|
+
for (let i = 1; i <= totalFields; i++) {
|
|
217
|
+
fieldNames.push(`field_${i}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const data = [];
|
|
221
|
+
for (let i = 1; i <= count; i++) {
|
|
222
|
+
const obj = { id: i };
|
|
223
|
+
// 每个对象随机选择50%-80%的字段
|
|
224
|
+
const fieldCount = randInt(Math.floor(totalFields * 0.5), Math.floor(totalFields * 0.8));
|
|
225
|
+
const selectedFields = [...fieldNames].sort(() => Math.random() - 0.5).slice(0, fieldCount);
|
|
226
|
+
|
|
227
|
+
for (const field of selectedFields) {
|
|
228
|
+
obj[field] = randInt(0, 1000);
|
|
229
|
+
}
|
|
230
|
+
data.push(obj);
|
|
231
|
+
}
|
|
232
|
+
return data;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 6. 深层嵌套结构
|
|
237
|
+
* - organization -> departments -> teams -> members -> tasks
|
|
238
|
+
*/
|
|
239
|
+
function generateDeepNested(orgCount = 2, deptPerOrg = 3, teamPerDept = 4, memberPerTeam = 5) {
|
|
240
|
+
const data = [];
|
|
241
|
+
for (let o = 1; o <= orgCount; o++) {
|
|
242
|
+
const departments = [];
|
|
243
|
+
for (let d = 1; d <= deptPerOrg; d++) {
|
|
244
|
+
const teams = [];
|
|
245
|
+
for (let t = 1; t <= teamPerDept; t++) {
|
|
246
|
+
const members = [];
|
|
247
|
+
for (let m = 1; m <= memberPerTeam; m++) {
|
|
248
|
+
const taskCount = randInt(1, 5);
|
|
249
|
+
const tasks = [];
|
|
250
|
+
for (let tk = 1; tk <= taskCount; tk++) {
|
|
251
|
+
tasks.push({
|
|
252
|
+
taskId: `TASK-${randInt(10000, 99999)}`,
|
|
253
|
+
title: `任务${tk}`,
|
|
254
|
+
status: randChoice(['todo', 'in_progress', 'done']),
|
|
255
|
+
priority: randChoice(['low', 'medium', 'high']),
|
|
256
|
+
dueDate: randDate(2024, 2025)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
members.push({
|
|
260
|
+
memberId: `M${o}${d}${t}${m}`,
|
|
261
|
+
name: randChineseName(),
|
|
262
|
+
role: randChoice(['leader', 'member', 'intern']),
|
|
263
|
+
tasks: tasks,
|
|
264
|
+
skills: randInt(1, 5) > 2 ? ['JavaScript', 'Python', 'Go', 'Rust'].slice(0, randInt(1, 4)) : []
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
teams.push({
|
|
268
|
+
teamId: `TEAM-${o}-${d}-${t}`,
|
|
269
|
+
name: `团队${t}`,
|
|
270
|
+
members: members
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
departments.push({
|
|
274
|
+
deptId: `DEPT-${o}-${d}`,
|
|
275
|
+
name: `部门${d}`,
|
|
276
|
+
teams: teams
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
data.push({
|
|
280
|
+
orgId: o,
|
|
281
|
+
orgName: `组织${o}`,
|
|
282
|
+
departments: departments
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return data;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============================================================
|
|
289
|
+
// 测试执行
|
|
290
|
+
// ============================================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 验证解压正确性:
|
|
294
|
+
* - compress → decompress → compress 应该得到相同结果(roundtrip)
|
|
295
|
+
* - 缺失字段会被填充为 null,这是预期的规范化行为
|
|
296
|
+
*/
|
|
297
|
+
function verifyRoundtrip(original, compressed, opts) {
|
|
298
|
+
const decompressed = decompress(compressed);
|
|
299
|
+
const recompressed = compress(decompressed, opts);
|
|
300
|
+
// 二次压缩后结构应该完全一致
|
|
301
|
+
return JSON.stringify(compressed) === JSON.stringify(recompressed);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function runTest(name, data) {
|
|
305
|
+
const originalSize = getByteSize(data);
|
|
306
|
+
|
|
307
|
+
// 默认(不 trim)
|
|
308
|
+
const compressed = compress(data);
|
|
309
|
+
const compressedSize = Buffer.byteLength(stringify(compressed), 'utf8');
|
|
310
|
+
const isCorrect = verifyRoundtrip(data, compressed);
|
|
311
|
+
const ratio = ((originalSize - compressedSize) / originalSize * 100).toFixed(2);
|
|
312
|
+
|
|
313
|
+
// trimTrailingNulls
|
|
314
|
+
const compressedTrim = compress(data, { trimTrailingNulls: true });
|
|
315
|
+
const compressedTrimSize = Buffer.byteLength(stringify(compressedTrim), 'utf8');
|
|
316
|
+
const isCorrectTrim = verifyRoundtrip(data, compressedTrim, { trimTrailingNulls: true });
|
|
317
|
+
const ratioTrim = ((originalSize - compressedTrimSize) / originalSize * 100).toFixed(2);
|
|
318
|
+
|
|
319
|
+
const diff = compressedSize - compressedTrimSize;
|
|
320
|
+
const diffStr = diff > 0 ? `-${formatBytes(diff)}` : diff === 0 ? '—' : `+${formatBytes(-diff)}`;
|
|
321
|
+
|
|
322
|
+
console.log(`\n${'='.repeat(72)}`);
|
|
323
|
+
console.log(`测试: ${name}`);
|
|
324
|
+
console.log('-'.repeat(72));
|
|
325
|
+
console.log(`对象数量: ${data.length}`);
|
|
326
|
+
console.log(`原始大小: ${formatBytes(originalSize)}`);
|
|
327
|
+
console.log(`不 trim: ${formatBytes(compressedSize).padStart(10)} (${ratio}%) ${isCorrect ? '✓' : '✗'}`);
|
|
328
|
+
console.log(`trim: ${formatBytes(compressedTrimSize).padStart(10)} (${ratioTrim}%) ${isCorrectTrim ? '✓' : '✗'}`);
|
|
329
|
+
console.log(`差值: ${diffStr}`);
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
name,
|
|
333
|
+
count: data.length,
|
|
334
|
+
originalSize,
|
|
335
|
+
compressedSize,
|
|
336
|
+
ratio: parseFloat(ratio),
|
|
337
|
+
compressedTrimSize,
|
|
338
|
+
ratioTrim: parseFloat(ratioTrim),
|
|
339
|
+
diff,
|
|
340
|
+
isCorrect
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function main() {
|
|
345
|
+
console.log('\n╔══════════════════════════════════════════════════════════╗');
|
|
346
|
+
console.log('║ JSON 数组压缩率测试 ║');
|
|
347
|
+
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
348
|
+
|
|
349
|
+
const results = [];
|
|
350
|
+
|
|
351
|
+
// 1. 简单用户数组 - 不同规模
|
|
352
|
+
console.log('\n\n【一、简单用户数组测试】');
|
|
353
|
+
results.push(runTest('简单用户 100条', generateSimpleUsers(100)));
|
|
354
|
+
results.push(runTest('简单用户 1000条', generateSimpleUsers(1000)));
|
|
355
|
+
results.push(runTest('简单用户 10000条', generateSimpleUsers(10000)));
|
|
356
|
+
|
|
357
|
+
// 2. 嵌套对象数组
|
|
358
|
+
console.log('\n\n【二、嵌套对象数组测试】');
|
|
359
|
+
results.push(runTest('嵌套用户 100条', generateNestedUsers(100)));
|
|
360
|
+
results.push(runTest('嵌套用户 1000条', generateNestedUsers(1000)));
|
|
361
|
+
results.push(runTest('嵌套用户 5000条', generateNestedUsers(5000)));
|
|
362
|
+
|
|
363
|
+
// 3. 带对象数组的订单
|
|
364
|
+
console.log('\n\n【三、订单数组测试(每单1-5商品)】');
|
|
365
|
+
results.push(runTest('订单 100条', generateOrders(100)));
|
|
366
|
+
results.push(runTest('订单 500条', generateOrders(500)));
|
|
367
|
+
results.push(runTest('订单 2000条', generateOrders(2000)));
|
|
368
|
+
|
|
369
|
+
// 4. 学校数据 - 复杂嵌套
|
|
370
|
+
console.log('\n\n【四、学校数据测试(复杂嵌套)】');
|
|
371
|
+
results.push(runTest('学校数据 小(2年级×2班×10生)', generateSchoolData(2, 2, 10)));
|
|
372
|
+
results.push(runTest('学校数据 中(6年级×4班×30生)', generateSchoolData(6, 4, 30)));
|
|
373
|
+
results.push(runTest('学校数据 大(6年级×6班×50生)', generateSchoolData(6, 6, 50)));
|
|
374
|
+
|
|
375
|
+
// 5. 稀疏字段数组
|
|
376
|
+
console.log('\n\n【五、稀疏字段数组测试】');
|
|
377
|
+
results.push(runTest('稀疏字段 100条×20字段', generateSparseData(100, 20)));
|
|
378
|
+
results.push(runTest('稀疏字段 500条×30字段', generateSparseData(500, 30)));
|
|
379
|
+
results.push(runTest('稀疏字段 2000条×50字段', generateSparseData(2000, 50)));
|
|
380
|
+
|
|
381
|
+
// 6. 深层嵌套
|
|
382
|
+
console.log('\n\n【六、深层嵌套测试】');
|
|
383
|
+
results.push(runTest('深层嵌套 小', generateDeepNested(2, 2, 3, 4)));
|
|
384
|
+
results.push(runTest('深层嵌套 中', generateDeepNested(3, 4, 5, 6)));
|
|
385
|
+
results.push(runTest('深层嵌套 大', generateDeepNested(5, 5, 8, 8)));
|
|
386
|
+
|
|
387
|
+
// 汇总
|
|
388
|
+
console.log('\n\n');
|
|
389
|
+
console.log('╔══════════════════════════════════════════════════════════════════════════╗');
|
|
390
|
+
console.log('║ 测试结果汇总 ║');
|
|
391
|
+
console.log('╚══════════════════════════════════════════════════════════════════════════╝');
|
|
392
|
+
console.log('\n');
|
|
393
|
+
|
|
394
|
+
const avgRatio = results.reduce((sum, r) => sum + r.ratio, 0) / results.length;
|
|
395
|
+
const avgRatioTrim = results.reduce((sum, r) => sum + r.ratioTrim, 0) / results.length;
|
|
396
|
+
const totalDiff = results.reduce((sum, r) => sum + r.diff, 0);
|
|
397
|
+
const bestCase = results.reduce((best, r) => r.ratioTrim > best.ratioTrim ? r : best);
|
|
398
|
+
const worstCase = results.reduce((worst, r) => r.ratioTrim < worst.ratioTrim ? r : worst);
|
|
399
|
+
|
|
400
|
+
console.log(`总测试数: ${results.length}`);
|
|
401
|
+
console.log(`平均压缩率(不 trim): ${avgRatio.toFixed(2)}%`);
|
|
402
|
+
console.log(`平均压缩率(trim): ${avgRatioTrim.toFixed(2)}%`);
|
|
403
|
+
console.log(`总节省: ${formatBytes(totalDiff)}`);
|
|
404
|
+
console.log(`最佳压缩: ${bestCase.name} (trim ${bestCase.ratioTrim}%)`);
|
|
405
|
+
console.log(`最差压缩: ${worstCase.name} (trim ${worstCase.ratioTrim}%)`);
|
|
406
|
+
|
|
407
|
+
console.log('\n详细结果:');
|
|
408
|
+
console.log('-'.repeat(100));
|
|
409
|
+
console.log(
|
|
410
|
+
`${'测试名称'.padEnd(34)} ${'数量'.padStart(6)}` +
|
|
411
|
+
`${'原始'.padStart(12)} ${'不 trim'.padStart(12)} ${'压缩率'.padStart(7)}` +
|
|
412
|
+
`${'trim'.padStart(12)} ${'压缩率'.padStart(7)} ${'差值'.padStart(12)}`
|
|
413
|
+
);
|
|
414
|
+
console.log('-'.repeat(100));
|
|
415
|
+
for (const r of results) {
|
|
416
|
+
const diffStr = r.diff > 0 ? `-${formatBytes(r.diff)}` : r.diff === 0 ? '—' : `+${formatBytes(-r.diff)}`;
|
|
417
|
+
console.log(
|
|
418
|
+
`${r.name.padEnd(34)} ${r.count.toString().padStart(6)}` +
|
|
419
|
+
`${formatBytes(r.originalSize).padStart(12)}` +
|
|
420
|
+
`${formatBytes(r.compressedSize).padStart(12)} ${r.ratio.toString().padStart(6)}%` +
|
|
421
|
+
`${formatBytes(r.compressedTrimSize).padStart(12)} ${r.ratioTrim.toString().padStart(6)}%` +
|
|
422
|
+
`${diffStr.padStart(12)}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
console.log('\n结论:');
|
|
427
|
+
console.log('-'.repeat(70));
|
|
428
|
+
console.log('1. 字段名越长、数量越多,压缩效果越好');
|
|
429
|
+
console.log('2. 嵌套结构(对象数组)压缩效果显著');
|
|
430
|
+
console.log('3. 稀疏字段(缺失字段多)压缩率反而最高(null 被省略为空槽)');
|
|
431
|
+
console.log('4. 原始类型数组字段不会被压缩(保持原样)');
|
|
432
|
+
console.log('5. 深层嵌套结构能获得更好的压缩效果');
|
|
433
|
+
console.log('6. 安全的字符串省略引号(key + value),进一步减少文本体积');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
main();
|