Vue3 高阶技巧:使用 AST 将 HTML 字符串优雅渲染为自定义组件

💡 背景与痛点

在日常的业务开发中,我们偶尔会遇到这样一种棘手的需求:

后端返回了一段 HTML 字符串,这段字符串不仅包含标准的 DOM 结构,还混杂了自定义格式的文本 。我们需要将这个字符串渲染到页面上,并且将这些自定义格式的文本渲染成我们自己的 Vue 自定义组件。

举个例子,假设我们有这样一段字符串:

html 复制代码
这是一段含有表格的文本
<table>
  <tr>
    <th>引用</th>
    <th>公式</th>
  </tr>
  <tr>
    <td>[[https://www.baidu.com]]</td>
    <td>{{$e=mc^2$}}</td>
  </tr>
</table>

思考:对于这种字符串,最直接的方法是什么?

很多人第一时间会想到:正则替换 !我们可以将自定义格式的文本(如 [[...]] 或 {{$...$}})通过正则替换为对应自定义组件的 HTML 字符串。

**但是,现实往往很骨感。**如果我们的自定义组件比较复杂,尤其是含有各种交互和复杂的 props 参数时,简单的正则替换就显得力不从心了,甚至根本无法实现。


🛠️ 破局思路:AST + 虚拟 DOM (VNode)

既然正则替换走不通,我们不妨换一种思路:

HTML 字符串本质上就是一个 DOM 结构。我们完全可以将其解析为 AST(抽象语法树)形式的 DOM 树 ,然后遍历这棵 AST 树 。在遍历的过程中,一旦遇到我们的"自定义格式文本",就将其替换为自定义组件的 VNode(虚拟节点)。最后,借助 Vue 强大的渲染函数,将这棵全新的 VNode 树渲染到页面上!

这种方案不仅优雅,而且扩展性极强。下面,我们就来一步步实现它。


🚀 详细实现步骤

1️⃣ 第一步:将 HTML 字符串转换为 DOM 树 (AST)

这一步非常简单,我们不需要引入复杂的第三方 AST 解析库,直接使用浏览器原生内置的 DOMParser 即可将 HTML 字符串解析为真正的 DOM 树。

typescript 复制代码
// 假设 this.htmlString 是我们需要解析的 HTML 字符串
const parser = new DOMParser();
// 将字符串解析为 DOM 文档
const doc = parser.parseFromString(this.htmlString, "text/html");
// 获取 body 下的子节点数组,这就是我们需要的 AST 形式的 DOM 树
const ast = Array.from(doc.body.childNodes);

此时的 ast 数组,就代表了这棵 DOM 树的最顶层节点集合。

2️⃣ 第二步:遍历 AST 树,生成 Vue 的 VNode 树

这是最核心的一步!我们需要遍历刚才得到的 AST 树。根据节点的 nodeType 来判断其类型,从而生成对应的 Vue VNode。

首先,我们处理 Text 文本节点 。在这里,我们将根据业务逻辑进行正则匹配,将特殊格式替换为我们想要的组件(通过 Vue3 的 h 函数返回 VNode)。

typescript 复制代码
import { h } from "vue";
import katex from "katex";

const parseText = (text) => {
  // 匹配 [[链接]] 或 {{$公式$}}
  const regex = /(\[\[.*?\]\]|\{\{\$.*?\$\}\})/g;
  const parts = text.split(regex);

  return parts.map((part) => {
    // 处理 [[链接]]
    if (part.startsWith("[[") && part.endsWith("]]")) {
      const url = part.slice(2, -2);
      // 渲染为 a 标签组件 (Vue 3 扁平化 props)
      return h(
        "a",
        {
          href: url,
          target: "_blank",
          style: { color: "#409EFF", textDecoration: "none" },
        },
        url,
      );
    }
    // 处理 {{$公式$}}
    else if (part.startsWith("{{$") && part.endsWith("$}}")) {
      const math = part.slice(3, -3);
      try {
        // 渲染为 KaTeX 公式组件
        const html = katex.renderToString(math, { throwOnError: false });
        return h("span", {
          innerHTML: html,
        });
      } catch (e) {
        // 解析失败时,回退显示红色文本
        return h("span", { style: { color: "red" } }, part);
      }
    }
    // 普通文本直接返回
    return part;
  });
};

接着,我们处理 Element 元素节点 。我们需要提取出标签名和属性,然后递归 调用自身来处理子节点,最后使用 h 函数将它们组合起来。

typescript 复制代码
const parseNode = (node) => {
  // 1. 处理文本节点
  if (node.nodeType === Node.TEXT_NODE) {
    return parseText(node.textContent || "");
  }
  // 2. 处理普通元素节点
  else if (node.nodeType === Node.ELEMENT_NODE) {
    const tagName = node.tagName.toLowerCase();

    // 提取元素的所有属性
    const attrs = {};
    for (let i = 0; i < node.attributes.length; i++) {
      const attr = node.attributes[i];
      attrs[attr.name] = attr.value;
    }

    // 递归遍历子节点,生成子 VNode 数组
    const children = Array.from(node.childNodes).flatMap(parseNode);

    // 生成当前元素的 VNode
    return h(tagName, attrs, children);
  }

  return null;
};

3️⃣ 第三步:调用 Vue 的渲染函数将 VNode 渲染到页面

为了让代码更加简便和响应式,我们可以直接使用 Vue 的 computed 计算属性。当 HTML 字符串发生变化时,自动重新生成 DOM 树,并转化为 VNode 树。

最后,我们通过一个简单的函数式组件将 VNode 树渲染到页面上。

完整组件代码演示如下:

vue 复制代码
<template>
  <div class="custom-render-container">
    <RenderVnode :vnode="vnode" v-for="(vnode, index) in vnodes" :key="index" />
  </div>
</template>

<script setup>
import { computed, defineProps, h } from "vue";

const props = defineProps({
  htmlString: {
    type: String,
    required: true,
  },
});

// 核心逻辑:递归遍历 DOM 节点,转换为 Vue 的 VNode
// ... (此处省略上文的 parseText 和 parseNode 函数代码) ...

// 功能组件:用于在模板中直接渲染 VNode 或文本
const RenderVnode = (props) => props.vnode;

// 计算属性:将输入的 HTML 字符串转化为 VNode 树
const vnodes = computed(() => {
  if (!props.htmlString) return [];

  // 1. 使用浏览器内置的 DOMParser 解析 HTML 字符串为 DOM 树(即 AST)
  const parser = new DOMParser();
  const doc = parser.parseFromString(props.htmlString, "text/html");

  // 2. 转换为 VNode 树
  return Array.from(doc.body.childNodes).flatMap(parseNode);
});
</script>

🎉 最终效果

经过上面的一番操作,原本死板的 HTML 字符串,现在已经能够完美地将其中的特殊标记渲染为我们想要的交互式 Vue 组件了!

📝 总结

通过 DOMParser 获取 HTML 的 AST,再结合 Vue3 的 h 函数动态生成 VNode,我们可以极大地拓展 HTML 字符串的渲染能力。这种方案相比正则替换更加健壮,也更容易应对复杂的组件传参和嵌套场景。

如果你在开发中也遇到了类似的需求,不妨尝试一下这个方案!

相关推荐
一 乐2 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
admin and root3 小时前
「AI安全篇」实战AntiDebug自动化JS逆向加解密MCP
javascript·人工智能·网络安全·自动化·漏洞挖掘·cnvd·src赏金
前端snow3 小时前
ai agent --- postgreSQL 关系型数据库
前端
卷无止境4 小时前
ECharts:把数据变成故事的可视化利器
前端
Eric_见嘉4 小时前
在职前端 Skill 和 MCP 分享
前端·后端·agent
liangshanbo12154 小时前
面试题:如何优化 Webpack 的打包速度?
前端·webpack·node.js
wengad5 小时前
从 wangEditor 迁到 wangEditor-next
前端·javascript·vue.js
行者-全栈开发6 小时前
【前端/JS框架】CVE-2026-3854:GitHub Enterprise Server X-Stat注入XSS漏洞深度剖析与紧急修复指南
前端·javascript·github
计算机魔术师7 小时前
AMD砸82亿买下李飞飞,这次不卖芯片卖什么?
前端
hzxpaipai7 小时前
杭州企业官网怎么建设?从策划到上线的5步建站方法
运维·服务器·前端·网络