一、为什么需要 HTML 格式化与压缩
HTML 是 Web 的基础,但实际项目中的 HTML 代码质量参差不齐:
- 服务器模板渲染产物:JSP / PHP / ERB 等模板生成的 HTML 通常缩进混乱、缺少换行
- 压缩部署后的代码:生产环境为减小体积,HTML 常被压缩为单行,调试时需要还原
- IDE 自动生成:某些可视化编辑器生成的 HTML 缩进风格不统一
- 复制粘贴的代码:来源各异的 HTML 片段拼在一起后风格杂乱
格式化(Pretty Print) 解决可读性问题:按层级缩进对齐,不改变语义。 压缩(Minify) 解决体积问题:移除注释与多余空白,减小传输体积。 校验(Lint) 解决规范问题:检测未编码字符、重复属性、嵌套错误等。
三者构成 HTML 代码质量工具链的核心环节。
二、HTML5 解析模型与容错规则
HTML5 不同于 XHTML 与 XML 的严格模式,它是一个 容错规范。浏览器解析时会按一套固定规则自动修复错误:
| 错误写法 | 浏览器自动修复 |
|---|---|
<p>段落<div>块</div></p> | 自动在 div 前闭合 p(p 不能包含 div) |
<b><i>文本</b></i> | 调整嵌套顺序为 <b><i>文本</i></b> |
<table><tr><td>cell</table> | 自动补全 </td></tr> 闭合 |
<input type=text> | 属性值自动补全引号 |
<div class=a class=b> | 重复属性只保留第一个 |
未闭合的 <li> | 遇到下一个 <li> 或父元素闭合时自动补全 |
这意味着 DOMParser 解析后的 DOM 树可能与原文本结构不同。本工具基于 DOMParser 解析,重新序列化时输出的是「浏览器规范化后」的 HTML,而非原始文本。这正是格式化与压缩能修正错误结构的基础。
三、空白处理规则
HTML 中的空白语义是初学者常踩的坑:
1. 默认空白折叠
连续的空格、制表符、换行符在 非 rawtext 元素 中会被折叠为单个空格:
<p> hello world </p>
<!-- 渲染为 "hello world" -->
2. pre 与 textarea 保留空白
<pre> 与 <textarea> 是 white-space 保留元素,源码中的空白与换行会原样显示:
<pre>line1
line2</pre>
3. 行内元素间的空白影响布局
行内元素之间的空白会渲染为单个空格,影响视觉布局:
<span>a</span> <span>b</span> <!-- 显示 "a b" -->
<span>a</span><span>b</span> <!-- 显示 "ab" -->
这也是压缩时不能简单移除所有空白的原因——行内元素间的空白若移除会改变显示效果。本工具的压缩模式默认合并空白为单空格(不全部移除),保留行内元素间的单空格布局。
4. 注释节点与空白
注释节点前后的空白在压缩时可安全移除,不影响显示。
四、void elements:无闭合标签的元素
HTML5 定义了 14 个 void elements(空元素),它们不能有子节点,因此不需要闭合标签:
area base br col embed hr img input
link meta param source track wbr
| 写法 | 合法性 |
|---|---|
<br> | ✓ HTML5 推荐 |
<br /> | ✓ XHTML 风格,HTML5 兼容 |
<br></br> | ✗ 非法(br 是 void) |
本工具统一输出 <br> 形式(不带斜杠),符合 HTML5 规范推荐。
五、可选闭合标签
HTML5 规范允许省略多个元素的闭合标签,浏览器会按规则自动补全:
| 元素 | 自动闭合条件 |
|---|---|
</html> | 文档末尾自动闭合 |
</head> | 遇到 body 元素时自动闭合 |
</body> | 文档末尾自动闭合 |
</li> | 遇到下一个 <li> 或父元素闭合时 |
</p> | 遇到块级元素时自动闭合 |
</td> </th> | 遇到下一个 td/th 或父元素闭合时 |
</tr> | 遇到下一个 tr 或父元素闭合时 |
</option> | 遇到下一个 option 或父元素闭合时 |
省略闭合标签可减小 5-10% 体积,但风险是:
- 嵌套错误难发现:
<p>内嵌<div>会自动断开<p>,开发者可能意识不到 - 可读性差:源码看起来「未闭合」
- 工具兼容性:某些老旧解析器(如 XML 模式)不支持省略
本工具压缩模式提供「省略可选闭合标签」选项,默认关闭,仅在追求极致体积时开启。
六、rawtext 元素:内容原样保留
四个元素的内容必须原样保留,不能缩进或归一化空白:
| 元素 | 原因 |
|---|---|
<pre> | 用户看到的就是源码原样(含空白换行) |
<textarea> | 用户输入的内容原样保留 |
<script> | JavaScript 代码,缩进会破坏语法(如模板字符串) |
<style> | CSS 代码,缩进会破坏语法 |
本工具遇到这四种元素时,直接输出 textContent,不递归缩进、不归一化空白。这也是为什么 JavaScript 与 CSS 应使用专门的格式化工具(如 Prettier、ESLint)而非 HTML 格式化工具。
七、美化算法核心
美化算法基于 DOMParser 解析后递归遍历 DOM 树:
function prettySerialize(node, indent, opts, lines):
for child in node.childNodes:
if child 是元素:
prettyElement(child, indent, opts, lines)
elif child 是文本:
归一化空白 → 添加缩进 + 文本
elif child 是注释:
保留时 → 添加缩进 + <!-- ... -->
elif child 是 DOCTYPE:
添加缩进 + <!DOCTYPE html>
function prettyElement(el, indent, opts, lines):
tag = el.tagName 小写化
attrs = 序列化属性
if tag 是 void element:
添加 indent + <tag attrs>
elif tag 是 rawtext:
添加 indent + <tag attrs>原内容</tag>
elif 无子节点:
添加 indent + <tag attrs></tag>
else:
添加 indent + <tag attrs>
prettySerialize(el, indent + opts.indent, opts, lines)
添加 indent + </tag>
核心是 递归缩进:每进入一层子节点,缩进增加一个单位(2/4 空格或 Tab)。文本节点归一化连续空白为单空格,注释节点按选项保留。
八、压缩算法核心
压缩算法追求最小体积,移除所有「不影响语义」的内容:
function minifySerialize(node, opts, output):
for child in node.childNodes:
if child 是元素:
minifyElement(child, opts, output)
elif child 是文本:
collapseWhitespace ? 合并空白为单空格 : 原样
elif child 是注释:
removeComments ? 跳过 : 保留
elif child 是 DOCTYPE:
输出 <!DOCTYPE html>
function minifyElement(el, opts, output):
tag = el.tagName 小写化
attrs = 序列化属性(移除空属性)
if tag 是 void:
output.push(<tag attrs>)
elif tag 是 rawtext:
output.push(<tag attrs>原内容</tag>)
elif 无子节点:
output.push(<tag attrs></tag>)
else:
递归压缩子节点
removeOptionalClose && OPTIONAL_CLOSE.has(tag)
? 输出 <tag attrs>子节点
: 输出 <tag attrs>子节点</tag>
压缩选项权衡
| 选项 | 体积收益 | 风险 |
|---|---|---|
| 移除注释 | 5-15% | 失去文档说明,IE 条件注释失效 |
| 合并空白 | 10-30% | 行内元素间单空格布局可能变化 |
| 移除空属性 | 1-3% | 影响小,安全 |
| 省略可选闭合标签 | 5-10% | 嵌套错误难发现,可读性差 |
九、属性序列化规则
属性序列化涉及多个细节:
1. 引号统一
HTML5 允许属性值不加引号(如 <input type=text>),但推荐加引号以避免歧义。本工具统一用双引号包裹:
<div class="container" id="main"></div>
2. 布尔属性
某些属性为「存在即生效」,无属性值,如 disabled、checked、readonly:
<input type="checkbox" checked>
本工具识别 21 个常见布尔属性,输出无值形式以减小体积。
3. 空属性移除
class="" 这种空字符串属性可安全移除(不影响显示),但 alt="" 等可能有语义(无障碍场景表示「装饰性图片」)需谨慎。本工具默认不移除空属性,可在压缩选项中开启。
4. 属性值转义
属性值中的 & 与 " 需要转义为 & 与 ",避免破坏 HTML 解析。本工具序列化时自动转义这两个字符。
十、校验常见问题
校验模式基于 DOMParser 解析后扫描可疑写法:
1. 未编码的 < 与 &
HTML 文本节点中的 < 与 & 应写为 < 与 &:
<!-- 错误 -->
<p>价格 < 100 & 数量 > 50</p>
<!-- 正确 -->
<p>价格 < 100 & 数量 > 50</p>
本工具用正则 <(?!\s*\/?[a-zA-Z!\/?]) 检测不在标签边界的 <。
2. 重复属性
<div class="a" class="b"></div>
<!-- 浏览器只保留第一个 class="a" -->
本工具检测元素内同名属性多次出现,提示「浏览器将只保留第一个」。
3. 嵌套错误
<p>段落 <div>块</div></p>
<!-- p 不能包含 div,浏览器自动在 div 前闭合 p -->
嵌套错误需通过 DOM 树结构反推,本工具暂未实现深度嵌套校验,仅提示基础问题。
十一、纯原生 TypeScript 实现要点
本站 HTML 格式化工具采用纯原生 TypeScript 零依赖实现,核心要点:
1. 使用 DOMParser 而非自写解析器
DOMParser 是浏览器原生 API,符合 HTML5 规范的容错解析。自写 HTML 解析器需处理数百种边界情况(实体、CDATA、注释、doctype、错误恢复),而 DOMParser 已内置这些能力。
2. 递归序列化保持 DOM 树结构
序列化时按 DFS 顺序遍历 DOM 树,每个元素输出开始标签、子节点、闭合标签,保证结构正确。void elements 与 rawtext elements 特殊处理。
3. 属性归一化
序列化时统一用双引号、布尔属性保留无值形式、空属性可选移除。这是「规范化 HTML」的标准做法。
4. 避免使用 innerHTML 序列化
element.innerHTML 也会序列化 DOM,但浏览器实现不一致(属性顺序、引号风格、空白处理各浏览器不同)。本工具自己实现序列化以保证输出一致。
5. 错误包装避免 UI 崩溃
DOMParser 在极端输入下可能抛错,所有 API 用 try-catch 包装为 FormatterResult 错误对象,UI 层只需检查 ok 字段。
十二、工具矩阵联动
HTML 格式化工具与本站其他工具形成联动:
| 工具 | 联动场景 |
|---|---|
| HTML 实体工具 | 实体工具处理字符级转义,本工具处理结构级格式化,互补使用 |
| Markdown 工具 | Markdown 渲染的 HTML 可粘贴到本工具美化 |
| JSON 工具 | JSON-LD 嵌入 HTML 后可整体美化 |
| URL 工具 | URL 中的 query 参数嵌入 HTML 属性时需先 URL 编码 |
| Diff 工具 | 对比两份 HTML 代码差异前先用本工具统一格式化 |
十三、常见陷阱
陷阱 1:用 innerHTML 序列化导致输出不一致
不同浏览器对 innerHTML 的实现不一致:Chrome 倾向于双引号,Firefox 偶尔输出单引号;属性顺序、空白处理也不同。应自己实现序列化保证一致输出。
陷阱 2:压缩时移除所有空白破坏行内布局
<span>价格</span> <span>100 元</span>
<!-- 移除空格后显示 "价格100 元",错误 -->
行内元素间的空白应保留为单空格,仅移除连续空白与首尾空白。
陷阱 3:对 script/style 内容缩进
<script>
const s = `多行
模板字符串`;
</script>
对 script 内容缩进会破坏模板字符串中的换行与缩进语义。rawtext 元素内容必须原样保留。
陷阱 4:用 XML 模式解析 HTML
DOMParser.parseFromString(input, 'text/xml') 会按 XML 严格模式解析,遇到未闭合标签直接报错。HTML 应使用 text/html 模式,享受容错能力。
陷阱 5:忽略 void elements 自闭合规则
<br></br> <!-- 非法,br 是 void element -->
void elements 不应有闭合标签,序列化时需特殊处理。
总结
HTML 格式化与压缩看似简单,涉及 HTML5 解析模型、空白语义、void elements、可选闭合标签、rawtext 处理、属性序列化等多个细节。基于浏览器原生 DOMParser 可省去自写解析器的复杂度,专注序列化算法的实现。
本站 HTML 格式化与压缩工具 已集成上述全部能力,欢迎试用。