
本文介绍一种基于递归遍历 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 结构化令牌的推荐实践。