HTML 格式化与压缩原理:从解析模型到 minify 算法

一、为什么需要 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% 体积,但风险是:

  1. 嵌套错误难发现<p> 内嵌 <div> 会自动断开 <p>,开发者可能意识不到
  2. 可读性差:源码看起来「未闭合」
  3. 工具兼容性:某些老旧解析器(如 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. 布尔属性

某些属性为「存在即生效」,无属性值,如 disabledcheckedreadonly

<input type="checkbox" checked>

本工具识别 21 个常见布尔属性,输出无值形式以减小体积。

3. 空属性移除

class="" 这种空字符串属性可安全移除(不影响显示),但 alt="" 等可能有语义(无障碍场景表示「装饰性图片」)需谨慎。本工具默认不移除空属性,可在压缩选项中开启。

4. 属性值转义

属性值中的 &" 需要转义为 &amp;&quot;,避免破坏 HTML 解析。本工具序列化时自动转义这两个字符。

十、校验常见问题

校验模式基于 DOMParser 解析后扫描可疑写法:

1. 未编码的 < 与 &

HTML 文本节点中的 <& 应写为 &lt;&amp;

<!-- 错误 -->
<p>价格 < 100 & 数量 > 50</p>
<!-- 正确 -->
<p>价格 &lt; 100 &amp; 数量 &gt; 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 格式化与压缩工具 已集成上述全部能力,欢迎试用。