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 字符串的渲染能力。这种方案相比正则替换更加健壮,也更容易应对复杂的组件传参和嵌套场景。

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

相关推荐
mldong3 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排3 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰5 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi7 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen7 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马8 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu8 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss