1. 糟糕的开头
那天是我接手一个老项目改版的第三天。前任开发者已经离职半年了,留下的是一个用某"可视化建站工具"自动生成的 HTML 模板——那玩意儿简直是行业的耻辱。我打开 index.html,整个人陷入了一种宁静的绝望。
那是一坨真正意义上的意大利面代码:<div> 嵌套 <div> 嵌套 <div>,所有内容塞在一行里,样式属性、自定义属性、数据属性的顺序乱得像打翻的麻将桌。我搜索一个 class 名,Ctrl+F 转了十几屏才找到。我试图手动整理,十五分钟后我意识到:这条路是死路。
我打开了一个知名的在线 HTML 格式化工具,把代码粘进去,等待……"正在上传并处理"。那份文件里有客户的内部系统界面代码,夹杂着几个 data-user-token 的隐藏字段。我盯着那个进度条,后背开始发凉。
2. 我的思考
为什么 HTML 格式化工具要经过服务器?答案和 JSON 格式化一样:惯性。早期的 Web 工具都是后端驱动的,服务端拿到你的 HTML,用 PHP/Python 解析,再把格式化结果返回给你。这套逻辑在 2005 年是标准做法。但在 2026 年,浏览器已经内置了一个近乎完美的 HTML 解析引擎——DOM API。它比你自己撸的任何正则表达式都更精准、更健壮,而且它就在用户的设备上,一分网络请求都不需要。
我的目标是:
- 100% 本地处理:代码不离开浏览器,哪怕包含 token 和密钥也无所谓
- 真正的结构感知:不是简单的字符串缩进,而是理解 HTML 的语义层级
- 智能处理边界情况:自闭合标签、预格式化内容、内联元素……都要处理妥当
- 即时反馈:粘进去,出来,不超过 50ms
3. 技术硬核区
HTML 格式化最大的坑,就是"不能用正则表达式"。很多人第一个想法是:用正则匹配标签,然后加缩进不就完了?这条路通向无底洞:正则会在属性值里的 > 字符、<style> 和 <script> 块内部疯狂误触发。HTML 是上下文敏感语言,正则根本不够格解析它。
daima.life 的 HTML 格式化工具采用的是「借力 DOMParser → 递归 DOM 遍历 → 重新序列化」的三段式策略:
// 第一阶段:让浏览器原生 Parser 做脏活
function parseHtml(rawHtml) {
const parser = new DOMParser();
// 浏览器会帮你自动补全缺失的闭合标签、修正嵌套错误
const doc = parser.parseFromString(rawHtml, 'text/html');
return doc.body;
}
// 第二阶段:递归遍历,按层级生成带缩进的字符串
function serializeNode(node, depth = 0, options = {}) {
const { indentSize = 2, inlineElements } = options;
const indent = ' '.repeat(depth * indentSize);
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
return text ? indent + text : '';
}
const tag = node.tagName.toLowerCase();
// 自闭合标签(void elements):不递归子节点
const voidElements = new Set([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img',
'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'
]);
if (voidElements.has(tag)) {
return indent + serializeAttrs(node);
}
// 预格式化元素:原封不动保留内容
const preformattedElements = new Set(['pre', 'textarea', 'script', 'style']);
if (preformattedElements.has(tag)) {
return indent + serializeAttrs(node) + node.innerHTML + tag;
}
// 内联元素(a,span,strong 等):同行显示
if (inlineElements.has(tag) && node.children.length === 0) {
return indent + serializeAttrs(node) + node.textContent + tag;
}
// 块级元素:递归处理子节点
const children = Array.from(node.childNodes)
.map(child => serializeNode(child, depth + 1, options))
.filter(s => s.trim() !== '')
.join('\n');
return indent + serializeAttrs(node) + '\n' + children + '\n' + indent + tag;
}
// 第三阶段:属性序列化(可按需排序)
function serializeAttrs(node) {
const attrs = Array.from(node.attributes);
// 属性排序策略:id → class → data-* → 其他 → 事件处理器
const priority = (name) => {
if (name === 'id') return 0;
if (name === 'class') return 1;
if (name.startsWith('data-')) return 3;
if (name.startsWith('on')) return 5;
return 2;
};
return attrs
.sort((a, b) => priority(a.name) - priority(b.name))
.map(attr => attr.name + '="' + attr.value + '"')
.join(' ');
}
这套三段式逻辑的精妙之处在于:
- 容错能力极强:DOMParser 是浏览器引擎级别的解析器,它能处理你想象得到的所有奇葩畸形 HTML,比你任何手写的解析器都更稳健
- 语义感知:通过区分 void elements、inline elements 和 preformatted elements,输出的代码不会因为机械缩进而破坏原有意图
- 属性排序作为彩蛋:按 id → class → data-* → 其他的顺序排列属性,让老代码里乱序属性也变得整整齐齐,这一点每次都让"老开发"们感动得想哭
对于超大 HTML 文件(比如那种几百 KB 的模板),我还用 requestIdleCallback 做了分批渲染——先马上显示格式化完的前半部分,趁浏览器"空闲"时再把剩下的补上。用户感知到的响应时间,几乎是零。
4. FAQ 模块
Q1:HTML 格式化工具会修改我代码的行为吗?
A:这是最值得认真回答的问题。daima.life 的工具在格式化时会经过 DOMParser 重新序列化,这意味着两件事:第一,畸形 HTML(比如未闭合的标签)会被自动修正;第二,极少数情况下,如果你的 HTML 里有依赖"错误结构"工作的奇技淫巧,格式化后的代码可能产生轻微的 DOM 结构差异。所以建议格式化后用我们工具箱里的 HTML 预览器对比一下渲染结果,确认无误再使用。
Q2:很多 HTML 里夹着Jinja2 / Thymeleaf / Mustache 模板语法,格式化会爆吗?
A:会,而且爆得很难看。这是 DOMParser-based 方案的天然局限——浏览器的 HTML 解析器遇到 {% 这种非 HTML 语法时,会直接把它当文本节点处理,导致原始的模板语法被破坏。我目前的解决方案是"模板路径保护":在格式化前先把所有模板表达式用唯一 ID 的占位符替换掉,格式化完成后再逐一还原。这个功能在工具的高级设置里可以开启。
Q3:格式化大型 HTML(比如 500KB+)的邮件模板,浏览器会卡死吗?
A:不会。我把完整的格式化逻辑封装在一个 Web Worker 里独立运行,主线程始终保持流畅响应。你可以对着 10 万行的邮件模板疯狂测试,页面依然丝滑如初。
Q4:和 Prettier 比,daima.life 的 HTML 格式化有什么优势?
A:Prettier 是工程师工具,它对 HTML 的格式化有一套非常严格的主张,有时候会打乱老项目的既有风格。daima.life 的格式化工具定位是"友好的代码整理器"——缩进尺寸(2/4 空格/Tab)、属性是否排序都可以自由配置。而且,你不需要安装 Node.js,打开浏览器就能用。
5. 结尾
回到那个项目。把老代码粘进 daima.life 的 HTML 格式化工具,输出的那一刻,我盯着那整整齐齐、层次分明的代码,感觉自己的血压稳了。之前那坨"意大利面",变成了一份清晰的 DOM 结构图谱。我在接下来的两个小时里顺利完成了改版,而不是还在手动对齐缩进。
一个好的格式化工具,本质上是代码世界里的"整理术"信徒。它的使命,是把混乱变成秩序,把疲惫变成流畅。而这一切,应该在你自己的设备上、你自己的浏览器里悄悄完成——不需要任何人做中间人,不需要任何代码离开你的视线。
如果你手边有一份折磨人的 HTML 意大利面,欢迎来 daima.life 试试一键解套。断网之后再来,你会更确信:这才是真正对你的代码负责的工具。