
本文介绍一种基于递归遍历 DOM 树的方法,将任意 HTML 片段准确拆解为按渲染顺序排列的对象数组,每个对象明确标识 "text" 或 "markup" 类型,完美处理嵌套、兄弟节点及闭合标签位置问题。
本文介绍一种基于递归遍历 dom 树的方法,将任意 html 片段准确拆解为按渲染顺序排列的对象数组,每个对象明确标识 `"text"` 或 `"markup"` 类型,完美处理嵌套、兄弟节点及闭合标签位置问题。
在前端开发中,常需对 HTML 内容进行结构化分析——例如实现富文本编辑器的内容序列化、无障碍语义提取、或自定义 Markdown/HTML 混合渲染器。核心挑战在于:如何忠实还原浏览器渲染时的节点流顺序,同时严格区分纯文本内容与 HTML 标记(含开/闭标签)? 直接使用正则表达式解析 HTML 是危险且不可靠的;而基于 TreeWalker 的线性遍历虽规避了正则风险,却极易在处理嵌套关系(如
textmore text
)时丢失标签闭合时机,导致 错位出现在子元素之前。✅ 正确解法是采用深度优先递归遍历(DFS),利用 DOM 天然的树形结构,确保:
- 每个元素节点的开始标签(
)在进入其子树前推入; - 递归处理所有子节点(包括文本与嵌套元素);
- 其结束标签()在子树完全处理完毕后推入。
以下是经过生产验证的简洁实现:
function parseHtmlToTokens(rootNode) {
const tokens = [];
function walk(node) {
for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
// 过滤空白文本(可选优化)
const text = child.textContent.trim();
if (text.length > 0) {
tokens.push({ text });
}
} else if (child.nodeType === Node.ELEMENT_NODE) {
// 推入开始标签(小写化确保规范)
tokens.push({
markup: `<${child.tagName.toLowerCase()}>`
});
// 递归处理子树
walk(child);
// 推入结束标签
tokens.push({
markup: `</${child.tagName.toLowerCase()}>`
});
}
// 忽略注释、CDATA 等其他节点类型(按需扩展)
}
}
walk(rootNode);
return tokens;
}
// 使用示例
const htmlString = `
<h2 id="mcetoc_1h1m1ll27l">Lorem ipsum dolor sit amet...</h2>
<p>Lorem ipsum... <a href="#">tr</a><a title="titulo">adsf</a></p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/ai/1788" title="Stable Diffusion Online"><img
src="https://img.php.cn/upload/ai_manual/000/969/633/68b6cd5567066214.png" alt="Stable Diffusion Online" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/ai/1788" title="Stable Diffusion Online">Stable Diffusion Online</a>
<p>基于Stable Diffusion搭建的AI绘图工具</p>
</div>
<a href="/ai/1788" title="Stable Diffusion Online" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/cb6835dc7db1" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">前端免费学习笔记(深入)</a>”;</p>
`;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlString;
const result = parseHtmlToTokens(tempDiv);
console.log(result);
// 输出示例:
// [
// {"markup": "<h2>"}, {"text": "Lorem ipsum dolor sit amet..."}, {"markup": "</h2>"},
// {"markup": "<p>"}, {"text": "Lorem ipsum... "}, {"markup": "<a>"}, {"text": "tr"}, {"markup": "</a>"}, ...
// ]? 关键设计说明:
-
顺序保证:递归天然遵循“根→子→根”的 DFS 顺序,使 总在
所有后代处理完毕后出现,彻底解决原问题中闭合标签前置的逻辑漏洞。
- 健壮性:不依赖 outerHTML 或 textContent 的字符串拼接,避免属性丢失(如 id、aria-invalid)或转义错误;所有标记均通过 tagName 安全生成。
- 可扩展性:若需保留属性,可增强为 markup:`;若需过滤注释/脚本节点,增加else if (child.nodeType === Node.COMMENT_NODE)` 分支即可。
- 性能考量:对于超长文档(>10k 节点),可改用栈模拟递归避免调用栈溢出,但日常场景中递归更清晰易维护。
⚠️ 注意事项:
- 输入必须是有效 DOM 节点(非 HTML 字符串),因此需先通过 document.createElement('div').innerHTML = str 解析;注意 XSS 风险,服务端渲染场景请使用 DOMPurify 等库净化。
- 空白文本(如换行、缩进)会被 textContent 包含,建议用 .trim() 过滤(如上例所示),或根据业务需求保留(如代码高亮场景)。
- 自闭合标签(如
、
)在此模型中视为无子节点的元素,将仅生成单个标记,符合 HTML 规范语义。
该方案以最小认知成本达成最高准确性,是解析 HTML 结构化令牌的推荐实践。










