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 小时前
API 层架构设计 — 从 RESTful 到 GraphQL 的范式演进
vue.js·后端·restful·graphql
MonkeyKing2 小时前
iOS Runtime 深度解析
前端·面试
程序员库里2 小时前
第 3 章:Tiptap 与 React 集成
前端·javascript·面试
码徒2 小时前
2026 前端技术十大趋势:84% 的开发者已经在用 AI 写代码了
前端·agent·ai编程
Joyee6912 小时前
RN 的新渲染器 Fabric
前端·react native