babel、typescript等js编译器无法处理html文件,因其设计不支持.html输入,强行解析会报syntaxerror或path.node is undefined;parse5是当前最稳的html ast解析器,兼容whatwg标准、错误容忍强,适合安全替换class等重构任务。

不能用 Babel、TypeScript 或其他 JS 编译器插件处理 HTML 文件——它们压根不支持 .html 输入,强行塞入只会触发 SyntaxError: Unexpected token 或后续 <code>path.node is undefined,不是配置问题,是设计边界硬限制。
为什么 parse5 是当前最稳的 HTML AST 解析起点
HTML 没有作用域、没有变量提升、没有 TDZ,Babel 的 path.scope 和 traverse 在它面前完全失效。真正能落地的解析器只有 parse5(符合 WHATWG 标准)、htmlparser2(快但不校验嵌套)或框架专属解析器(如 @vue/compiler-dom)。其中 parse5 兼容性好、错误容忍强、Node 浏览器双端可用,且输出结构清晰:tagName、attrs(数组,非对象)、childNodes、isSelfClosing 字段直接可用。
容易踩的坑包括:
-
attrs是只读数组,不能直接node.attrs.class = 'new',必须重建整个attrs数组 - 遍历
childNodes时没过滤TEXT_NODE,对文本节点调用getAttribute直接报错 - 忽略
isSelfClosing,把<img>改成<img>,序列化后 HTML 失效
安全替换 class 属性的实操逻辑
硬拼字符串或正则替换 class="btn" 极易漏掉边界:换行、空格、引号不统一、注释干扰。正确路径是遍历 AST 节点,提取并操作 attrs 数组。
关键步骤:
- 只处理
node.nodeType === ELEMENT_NODE的节点 - 用
node.attrs.find(a => a.name === 'class')定位属性,而非node.getAttribute('class')(DOM API,在 parse5 AST 上不存在) - 值分割用
.split(/\s+/).filter(Boolean),比.split(' ')更鲁棒,防首尾空格和多空格 - 修改后调用
parse5.serialize(ast)输出,它自动处理转义、自闭合标签、命名空间,不依赖手拼字符串
如何避免重构后样式断裂或语义倒退
自动化替换不是“全局搜替”,而是带上下文判断的精准操作。例如把 <div class="header"> 换成 <code><header></header>,必须加约束条件:
- 父节点是
且无data-no-rewrite属性 - 保留原始
class值(如<div class="header navbar-fixed"> → <code><header class="navbar-fixed"></header>),避免样式链断裂 - 对含内联 JS 的标签(如
<div onclick="toggleMenu()">),先加 <code>data-legacy-js标记,不强行删逻辑CI 中必须接入
html-validate,配置"semantic-elements": "error"规则;Git pre-commit hook 跑npx html-validate --config .htmlvalidate.json src/**/*.html,卡住新增非语义化容器。AST 重构真正的难点不在遍历,而在边界判定
比如
<p></p>是否该在末尾换行、<script></script>里的 JS 是否参与缩进、注释里含<!-- <div> -->怎么处理——这些都不是 AST 遍历本身的问题,而是输出层的格式策略选择。Prettier 浏览器版(加载prettier@3.3.3/esm/standalone.mjs+parser-html)是目前唯一能兼顾语法合法性和风格一致性的方案;自己手写序列化逻辑不仅 bundle 多出 200KB+,还会反复踩空格、引号、自闭合斜杠等坑。











