一封被压成单行的邮件模板引发的疑问
场景很日常:运营同事转来一份 EDM 模板,说"在编辑器里改了个文案,提交后 diff 全红了"。打开一看,整段 HTML 被某个构建脚本压成了一行——标签之间无换行无缩进。我顺手把这一行粘进工具点了"美化",缩进和行数回来了,但 diff 仍然全红:文本节点周围多出若干空格。这让我想验证一个根本问题:美化与压缩是不是互逆操作?往返一次能否回到原始字节?十份不同结构的样本逐字节对照给出答案——互逆性只在一个方向成立。
20 组逐字节对照与不对称失败
十份结构差异明显的样本——纯 ASCII 预填串、含中文表格、成对 pre 块、textarea、内联 script、大写标签加单引号、行内文本空格、void 标签串、三层嵌套中文、混合 style——每份分别跑 minify(beautify(x)) 与 beautify(minify(x)),和原串逐字节比较。二十组对照,零组相等。但失败不对称:beautify(minify(x)) 等于 beautify(x) 十条全中,即先压后美化输出和直接美化完全相同;minify(beautify(x)) 等于 minify(x) 十条全不中。下面给 UTF-8 字节对照:
| 样本 | 原串 B | 美化 B | 压缩 B | mb B | bm B |
|---|---|---|---|---|---|
| S1 预填一行式 | 128 | 216 | 128 | 134 | 216 |
| S2 表格中文 | 72 | 96 | 54 | 58 | 96 |
| S3 pre 块 | 25 | 32 | 23 | 31 | 32 |
| S4 textarea | 52 | 69 | 52 | 66 | 69 |
| S5 内联 script | 93 | 110 | 93 | 107 | 110 |
| S6 大写加注释 | 60 | 74 | 57 | 59 | 74 |
| S7 行内文本 | 52 | 69 | 52 | 56 | 69 |
| S8 void 标签 | 40 | 56 | 39 | 45 | 56 |
| S9 三层嵌套 | 116 | 149 | 85 | 89 | 149 |
| S10 含 style | 102 | 152 | 98 | 120 | 152 |
bm 列每行与美化列一致,mb 列与压缩列或原串无一相等——minify 对 beautify 透明,beautify 对 minify 不可逆。
128、216、134:多余字节从哪来
预填串 128 字节纯 ASCII,在HTML 美化压缩面板点"美化"后变 216 字节——增量全来自行首缩进和换行符。再"压缩"回去得到 134 字节而非 128:多出 6 字节。定位偏移 34 发现 <title> Test </title>——beautify 把文本节点下推独立行并 trim,两侧各留一空格;minify 的 >\s+< 正则只匹配纯标签边界,文本独立行两侧的空格不在覆盖范围。Test、Hello、World 三个文本各贡献 2 字节,合计恰好 6。
pre 成对才受保护,未闭合即破防
minifyHTML 的保留机制依赖正则匹配成对开闭标签。实测成对样本 "<div> x <pre>L1 L2</pre> y </div>" 40 字节进 40 字节出,pre 内部空白原样不动;大写 PRE 同样命中。未闭合时 "<pre>keep me please" 24 字节被折叠成 19 字节——内部空格和换行全被通用正则吞掉。保护是条件性的:成对才进保留池,未闭合则退化。
统计条报的是字符,不是字节
updateStat 取的是 JS 字符串 .length——即 UTF-16 码元数,不是 UTF-8 字节数。中文样本显示 11 字符,实际 UTF-8 编码 19 字节;含代理对的样本显示 10 字符,实占 14 字节。比率也基于字符口径:预填首屏从 128 字符变 216 字符,变化 +68.8% 红字。做传输体积估算必须用 UTF-8 编码器重算,不能引用统计条数值。
图 1:S1 往返字节流向与不对称统计
幂等性分裂与占位符注入
beautify 对含成对 pre 的串不幂等:55 字符源逐轮变长 83→96→104 字符——b1 把 pre 拆成独立行后,b2 把 pre 体内换行行首当作原内容再叠缩进。不含保留块的串幂等:两次均 102 字符。minify 二次运行逐字节相同,无论含不含保留块。
minify 内部占位协议 ___PRESERVE_n___ 可被正文字面量注入。输入 "<div>___PRESERVE_0___</div><!--C C-->" 37 字节,输出 31 字节 "<div><!--C C--></div><!--C C-->"——正文占位符被 preserved[0](一条注释)替换,该注释出现两次。越界编号 ___PRESERVE_5___ 输出字面量 undefined。
findTagEnd 的引号感知与泄漏
beautifyHTML 用 findTagEnd(code, start) 定位标签结束。实测属性值含 > 时在第 13 字符收口——引号内 > 不截断;双引号套单引号交叉 "<div t="it's" u='q"r'>x" 在第 22 字符收口;引号泄漏(开引号未闭合)则一路吞到第 17 字符串尾。未闭合不报错,只让缩进异常加深。
图 2:findTagEnd 三种收口行为对照
注释复选框与格式不变量
同一含注释输入 44 字节,勾选保留时输出 43 字节——注释连同内部空白完整留下;取消勾选输出 19 字节——注释整段删除。属性引号与标签大小写两档下逐字节不变,单引号仍单引号、双引号仍双引号、DIV 不转 div。
缩进宽度是显示层概念
getIndent 三档(2 空格、4 空格、Tab)给不同前导字符数,但 minify 的 >\s+< 正则把这些全部删除——换档不影响 minify 输出,字节账里不出现缩进宽度变量。
FAQ
Q:美化再压缩能否得到直接压缩的结果?
不能。minify(beautify(x)) 与原串逐字节十条全不中——beautify 留下的文本独立行空格和保留块缩进是 minify 删不掉的残余。反过来 beautify(minify(x)) 等于 beautify(x) 十条全中。
Q:未闭合 pre 会不会报错?
不会。未闭合不满足成对正则,不进保留池,内部空白被通用正则折叠;beautify 也只加深缩进不抛异常。更多场景选择可参考旧文 HTML 美化压缩说明。
Q:统计条的数字能当字节数用吗?
不能。updateStat 取 UTF-16 码元数,11 字符可能实占 19 字节。字节账必须另算 UTF-8。
Q:正文写了 ___PRESERVE_0___ 会怎样?
被当成占位符还原成保留池第 0 号内容,导致注释重复。正常 HTML 不会触发这条件。