稀土掘金技术社区

开源 | 我将用最优雅的方式在 Vue 中渲染 AI 应用返回的 Markdown 数据

序

想象一个这样的场景, 你的 AI 应用通过知识库查询,为你返回了几十条 Table 数据。产品希望用户能够对 Table 数据执行分页、筛选等操作。

你很容易想到使用你熟知的 UI 组件库, 来渲染 AI 返回的 Table 数据。效果看起来就像这样:

Image

这个库的地址在 

github.com/EralChen/vunk-markdown .

不过,你也不需要非常着急的使用它,在这篇文章中我将向你揭露一些技术细节,以便你来应对,像 「ECharts 图表渲染」、「流程图」、「大纲」、「地图」 等一切业务中可能存在的需求。认真看完,希望你会有所收获.

简单实现存在的问题

众所周知, AI 倾向于使用 Markdown 文本数据流向客户端传递信息。 一个简单的渲染,就像:

<scriptsetup>
// ... 省略其他配置;
const md = MarkdownIt({
    highlight,
})
const htmlText = computedAsync(async () => {
return md.render(props.source)
}, '')
</script>

<template>
<divv-html="htmlText"></div>
</template>

<!-- 使用 -->
<MarkdownRender:source="source"></MarkdownRender>

v-html 是一个全量渲染的过程, 并且他仅仅渲染 html 字符串。

这意味着,你不再享有 Vue DOM 更新时的 diff 算法优化, 同时也失去了使用 Vue 组件的权力。

你所期望的调用方式

以替换 Table 这个需求为例,你可以想象最直观的调用方式,就像:

<!-- 这是一个简单用例, 实际实现考虑到扩展性, 会略有不同 -->
<MarkdownRender:source="source">
<template #table="{ data, columns }">
<MyTables:data="data":columns="columns" />
</template>
</MarkdownRender>

如果将数据收集到对应的插槽的参数中,那么我们就能轻松使用任意组件库,来替换原有标签.

解析

无论你使用何种 Markdown 工具 (markdown-it、marked、remark), 要想在渲染之前提取数据, 必然绕不开对 markdown 原文本的解析.

这个过程需要封装在 MarkdownRender 中, 不被使用端感知.

这里仅以 markdown-it 为例, 你可以在 markdown-it demo 查看解析结果

image.png

要想让 markdown 解析成 Vue 组件关键是构建嵌套树状结构, 与 vDOM 结构对应

  1. 「Markdown 文本」 → 「markdown-it tokens」(扁平数组)
  2. 「markdown-it tokens」 → 「嵌套树状结构」
  3. 「树状结构」 → 「Vue 组件渲染」(由渲染器处理)
// setup
const renderItems = computed(() => tokensToTree(
  md.parse(props.source, {})
))
// template
<VkRenderer :source="renderItems">
   <slot />
</VkRenderer>

一个完整的解析后的数据结构示例 -- h1 标签结构, 如下:


{
"templateType": "GroupToken",
"tag": "h1",
"open": {}, // Markdown Tokon 省略
"close": {}, //  Markdown Tokon 省略
"children": [
        {
"type": "inline",
"tag": "",
"attrs": null,
"map": [
0,
1
            ],
"nesting": 0,
"level": 1,
"children": [
                {
"type": "text",
"tag": "",
"attrs": null,
"map": null,
"nesting": 0,
"level": 0,
"children": null,
"content": "标题一级",
"markup": "",
"info": "",
"meta": null,
"block": false,
"hidden": false,
"templateType": "text"
                }
            ],
"content": "标题一级",
"markup": "",
"info": "",
"meta": null,
"block": true,
"hidden": false,
"templateType": "inline"
        }
    ]
}

而我们唯一要做的, 就是将解析后的结果交给用户, 让用户自定义渲染采用的组件.

我想这就是全部! 是我们最终的目标!

策略渲染

如果你认真阅读, 解析章节, 那么你将发现

const renderItems = computed(() => tokensToTree( md.parse(props.source, {}) )) 
// template 
<VkRenderer :source="renderItems"> <slot /> </VkRenderer>

template 中的 VkRenderer 接收的source 正是解析后的树状结构.

再回看解析后的 json 数据, 解析的过程中, 每个 item 都被添加了 templateType 字段.

如果有一个组件能够匹配 templateType 字段, 来渲染内容. 那么我们将轻松实现

  • 「替换默认渲染」:为任何 Markdown 元素自定义渲染逻辑
  • 「增强交互性」:将静态内容转换为交互式组件
  • 「集成第三方库」:集成语法高亮、图表等功能
  • 「保持响应式」:利用 Vue 的响应式系统实现动态更新

而你只需要将策略写在组件的默认插槽内, 使用起来就像:

<scriptlang="ts"setup>
import { VkMarkdown, VkRendererTemplate } from'@vunk/markdown'
import { computed, ref } from'vue'

const source = `# Hello, Markdown!
This is a simple example of using **Markdown** in a Vue component.
`

</script>

<template>
<VkMarkdown
:source="source"
  >


<VkRendererTemplatetype="text">
<template #default={raw }>
            {{ raw.content }}
</template>
</VkRendererTemplate>

<VkRendererTemplatetype="GroupToken">
<!-- 省略实现 -->
</VkRendererTemplate>

<!-- 省略 inline 实现 -->
</VkMarkdown>
</template>

组件库所做的事

很感谢你能看到这里, 如你所见 「解析」 和 「策略渲染」 就是 vunk-markdown 的核心.

对于常见的渲染策略, 组件库中做了封装

TemplatesDefault 用于渲染常规 Markdown 内容

TemplateEcharts 用于 Echarts 图表渲染

TemplateMermaid 用于 mermaid 流程图渲染

Image

同时你可以, 在 example 中看到更多自定义渲染

AI编程资讯AI编码专区指南:

https://aicoding.juejin.cn/aicoding

图片
点击“阅读译文“了解详情~