不能用 babel 或 typescript 编译器处理 html,因其解析器不支持 html 语法且 ast 缺失 html 特有字段;应选 parse5(语义保真)或 cheerio(便捷但不校验),vue/angular 模板需专用编译器;修改 class 等属性须安全合并,自闭合标签需显式设置 isselfclosing,遍历时需区分节点类型,替换逻辑需明确定义结构一致性标准。

为什么不能用 Babel 或 TypeScript 编译器处理 HTML
因为 @babel/parser 遇到 <div> 会直接抛出 <code>SyntaxError: Unexpected token ;TypeScript 编译器只接受 <code>.ts/.tsx,且其 AST 不包含 tagName、attrs 这类 HTML 特有字段。硬塞 HTML 字符串进去,解析阶段就失败——不是配置问题,是设计边界问题。
常见错误包括:在 Webpack 插件里调用 babel.transformSync(htmlString),或误以为 ts.createSourceFile 能加载 HTML 文件。这些操作根本不会进入 AST 遍历环节,连“重构”两个字都谈不上。
该用哪个 HTML 解析器做 AST 重构
选 parse5 还是 cheerio,取决于你要不要浏览器兼容性语义:
-
parse5严格遵循 HTML5 标准,能正确处理<table> 嵌套、自动闭合规则(如把 <code><p>hello</p> <div>world</div>补全为两个<p></p>),适合需要语义保真的批量重构 -
cheerio接口类似 jQuery,写起来快,但不校验嵌套合法性,<ul><div> 也能 parse 成功,后续生成的 HTML 可能在真实浏览器中被重排 <li>如果项目含 Vue 模板,必须用 <code>@vue/compiler-dom;含 Angular 模板,则用@angular/compiler——它们暴露的 AST 才带v-if、*ngIf等指令节点 - 用正则
/\s+/g拆分再去重,比手动拼字符串可靠 -
parse5的attrs是数组,不是对象,修改需重建整个attrs数组:[...oldAttrs.filter(a => a.key !== 'class'), { key: 'class', value: newClassStr }] - 遇到空格/换行/制表符混排的 class 值(如
class=" btn \n primary "),先.trim().split(/\s+/).filter(Boolean)再合并 -
parse5中自闭合标签靠isSelfClosing字段控制,不是靠标签名自动推断 - 手动创建节点时,
t.element({ tagName: 'img', isSelfClosing: true })必须显式声明 - 遍历子节点时,
node.nodeName === '#text'和node.nodeType === 1(Element)要分开判断,否则对文本节点调用getAttribute会报错
替换 class 属性时最容易踩的坑
直接覆盖 class 属性值会丢掉原有 class,比如把 class="btn primary" 改成 class="btn secondary",就丢了 primary。
安全做法是用工具函数提取并合并:
自闭合标签和序列化一致性问题
把 <img src="x"> 替换成 <picture>...</picture> 时,若新节点没设 isSelfClosing: false,parse5.serialize() 可能输出 <picture></picture>——这是非法 HTML。
关键点:
真正难的不是遍历树,而是定义“什么算一次安全替换”:是否保留原始缩进?属性顺序要不要标准化?<input type="text"> 和 <input type="text"> 是否视为同一结构?这些边界不厘清,自动化就容易产出合法但风格割裂的 HTML。











