blog.cover

用MarkdownAST来渲染Vue组件

理论上运行时框架和编译时框架都能用这种方法

浏览418
评论0
2025年12月14日

传统派渲染Markdown

现在大模型火热,各种chat模式五花八门的,而chat模式下大模型输出内容时,常常需要动态渲染 Markdown。而这些由模型输出的 Markdown 内容往往类型丰富、结构复杂,甚至包含复杂且多样化的交互需求——比如最基础的代码块,不仅要实现高亮显示,还得在 <code> 区域上方展示语言名称,并附带一个“复制”按钮。

传统的做法是通过 marked、markdown-it 这些库解析 Markdown 字符串,然后生成 HTML 字符串,再用 v-html 渲染。这种方式简单直接,但是有很多问题:

  • 安全性差:原始 HTML 可能包含恶意脚本,极易引发 XSS 攻击,还需额外引入 DOMPurify 等库进行净化;

  • 不支持组件嵌入:无法在 Markdown 中插入自定义 Vue 组件或执行业务逻辑;

  • 交互能力受限:实现“复制代码”、“折叠段落”、“实时预览”等复杂交互行为的成本很高。

而 使用 Markdown AST(抽象语法树)来渲染组件,就可以突破这些限制,实现更灵活、安全、强大的渲染能力。

AST的能力

之前写过的关于AST的介绍→Complier&AST-前端工具链开发基础 | NoelOrin's Blog

简单来说,AST(抽象语法树)赋予我们对每个语法节点进行精确控制和定制处理的能力。 正因为代码被解析成了结构化的树形数据,这给我们带来了极大的灵活性,例如:

  • 自定义 Markdown 语法:识别 [!TIP] 并渲染为 <Alert type="tip" />;

  • 智能代码转换:在 Babel 中将 ?? 空值合并运算符降级为兼容写法;

  • 静态分析与检查:ESLint 通过 AST 检测未使用的变量或潜在 bug;

Markdown AST

传统方法的渲染流程是Markdown -> 转译HTML -> DOM

而使用AST的渲染流程Markdown -> 构建AST -> 生成VNodes (虚拟节点) -> DOM

构建AST

为了实现构建Markdown AST,我们需要一些工具。

其实 markdown-it也可以

Shell
      npm install unified remark-parse
    

首先来个小demo演示一下。

Javascript
      import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkGfm from 'remark-gfm';

const parser = unified()
    .use(remarkParse)
    .use(remarkGfm) // GFM语法插件 有这个才能用完整的markdown语法
    .use(remarkParse)

const ast = parser.parse("# 任意Markdown")
    

得到的Markdown AST结构如下↓ 可以看到,转化为ast的markdown抽象语法树没有丢失原来markdown文本的信息,我们得到了信息完整且结构完整的树,这样便于我们去进一步操作。

Json
      {
  "type": "root",
  "children": [
    {
      "type": "heading",
      "depth": 1,
      "children": [
        { "type": "text", "value": "任意Markdown" }
      ]
    }
  ]
}
    

AST 解析器

先写一个AST解析器,将AST节点转换为VNode。

Javascript
      const renderAst2Html = (node:any) => {
    if (!node) return null;
    switch (node.type) {
        // ========== 容器类 ==========
        case 'root':
            return h('div', { class: 'md-root' }, node.children.map(renderAst));

        case 'paragraph':
            return h('p', {}, node.children.map(renderAst));

        // ========== 代码块 ==========
        case 'code':
            return h(CodeBlock, {
                lang: node.lang || '',
                value: node.value,
                meta: node.meta || ''
            });
       // 可继续扩展...
    }
};
    

封装 Vue 动态组件

Vue
      <template>
        <component :is="VNodeTree" />
</template>

<script setup>
import { computed, shallowRef, watchEffect } from 'vue'
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkGfm from 'remark-gfm';
import CodeBlock from "~/components/CodeBlock.vue"; // 自己封装的代码块组件
import { h } from 'vue';  // 用render函数生成Vnode虚拟DOM

const props = defineProps({
    markdown: {
        type: String,
        default: ''
    }
})

const ast = shallowRef(null)

const parser = unified()
    .use(remarkParse)
    .use(remarkGfm) // Gfm语法插件 有这个才能用完整的markdown语法
    .use(remarkParse)

watchEffect(() => {
    ast.value = parser.parse(props.markdown)
})

// 生成将抽象语法树转化为虚拟DOM树 并替换部分标签节点为Vue组件
const renderAst2Html = (node:any) => {
  .......
};

const VNodeTree = computed(() => renderAst(ast.value))

</script>
    

这样就可以使用动态组件了渲染出来Markdown了

总结

切换至 AST 方案后,Markdown 渲染从“字符串渲染”升级为“结构化组件渲染”:

  • 性能更优:依托 Vue 原生 Diff 机制,无需 v-html;

  • 高度灵活:可将原生元素轻松为自定义 Vue 组件。

  • 架构清晰:解析、渲染、组件逻辑分离,代码更易维护;

  • 安全可靠:避免 XSS 风险,支持类型校验。

  • 开发体验:依靠虚拟DOM渲染组件,避免了侵入式的DOM操作,心智负担低。

附 Markdown所有节点类型名

节点类型 (node.type)描述常见属性对应 Markdown 示例
root文档根节点children整个文档
paragraph段落children普通文本行
heading标题depth (1-6), children# 标题
text纯文本value任意文字内容
emphasis斜体(* 或 _)children*斜体*
strong加粗(** 或 __)children**加粗**
delete删除线(需 GFM)children~~删除线~~
inlineCode行内代码value, lang?`code`
code代码块value, lang, meta js\nconsole.log()
blockquote引用块children> 引用
list列表(有序/无序)ordered, start, spread, children- item 或 1. item
listItem列表项checked (任务列表), children, spread- [x] 完成
thematicBreak分割线—---、***、___
link链接url, title?, children[text](url)
image图片url, title?, alt![alt](url)
html原生 HTML(若启用)value<div>raw html</div>
break换行(硬换行)—行尾两个空格 + 回车
table表格(需 GFM)align, children(含 tableRow)表格语法
tableRow表格行children(tableCell)`
tableCell表格单元格children单元格内容
definition链接/图片定义(引用式)identifier, url, title?[id]: url "title"
footnoteDefinition脚注定义(需插件)identifier, children[^1]: 注释
footnoteReference脚注引用identifier[^1]
math数学公式(需 remark-math)value, meta?$E=mc^2$
inlineMath行内数学公式value$$a^2$$
用MarkdownAST来渲染Vue组件
作者 NoelOrin
发布时间2025年12月14日
许可协议 CC BY-NC-SA 4.0