AI Markdown 渲染引擎设计与实现 (Vue2版本)
本文档详细阐述了一个支持流式渲染、动画效果和XSS防护的AI Markdown渲染引擎的核心模块设计与Vue2实现方案。
一、核心模块概览
该渲染引擎主要由以下四个核心模块构成:
- Parser (解析器) : 负责将Markdown文本转换为HTML字符串,并支持自定义渲染规则。
- Render (渲染器) : 负责将安全的HTML字符串解析为React/Vue虚拟DOM树,并支持自定义组件替换与动画集成。
- useStream (流式渲染Hook) : 负责处理不完整的Markdown流数据,通过缓存机制等待语法闭合后再进行渲染,保证输出的完整性。
- animationText (动画文本工具) : 为文本内容添加增量式淡入动画,提升用户体验。
二、核心模块详解
1. Parser (解析器)
-
职责: 基于
marked(或markdown-it,Vue2版本中使用) 库,将输入的Markdown格式文本解析为HTML字符串。 -
核心特性:
- 可配置的渲染器: 允许开发者自定义特定Markdown元素(如链接、图片、代码块)的渲染规则。
- 安全增强: 在Vue2版本的实现中,通过重写链接渲染器,自动为链接添加
target="_blank"和rel="noopener noreferrer"属性,既实现了新窗口打开,又有效预防了XSS攻击。 - 代码块特殊处理: 针对Mermaid等特殊代码块,可进行内容编码并返回预设的HTML格式。
2. Render (渲染器)
-
职责: 接收HTML字符串,并将其安全地转换为前端框架(React/Vue)的组件树进行渲染。
-
工作流程与技术要点:
-
安全清洗: 使用
DOMPurify库对输入的HTML字符串进行净化,彻底防止XSS攻击。 -
HTML解析: 利用
html-react-parser(React) 或DOMParser(Vue) 将净化后的HTML字符串解析为DOM节点树。 -
自定义组件替换与状态管理:
- 支持将特定的HTML标签(如
<mermaid>)替换为自定义的React/Vue组件。 - 关键机制:未闭合标签处理。在解析过程中,如果遇到尚未闭合的自定义标签(如代码块),渲染器会向该组件传递一个特殊状态(如
unclosedTags),告知其“渲染暂停”。只有当对应的闭合标签被匹配后,才更新状态(如unclosedTags: []),触发组件的正式渲染。 - 此机制的优势: 有效避免了因内容逐步加载而导致的UI“闪动”(Flash of Unstyled Code, FOUC),尤其在代码高亮场景中至关重要。
- 支持将特定的HTML标签(如
-
动画集成: 对于普通文本节点,会将其交由
animationText工具处理,以实现增量动画效果。
-
3. useStream (流式渲染Hook)
-
职责: 处理从AI接口流式返回的、可能不完整的Markdown片段。它维护一个缓存,暂存未形成完整语法的片段,待其闭合后再一并渲染。
-
工作流程:
- 监听输入的Markdown内容流。
- 若开启缓存模式 (
hasNextChunk),则将新到来的内容与缓存中的未完成片段合并。 - 智能缓存判断: 遍历合并后的内容,优先检查是否处于代码块内部(通过匹配 ``` 或 ~~~ 围栏)。若在围栏内,则持续缓存直至检测到有效的结束围栏。
- 语法闭合分析: 若不在代码块内,则使用语法分析器判断其他Markdown结构(如粗体、斜体、表格)是否已正确闭合。对已闭合的结构,将其从缓存中“提交”并准备渲染;对未闭合的,则继续缓存并更新其内部状态标记。
- 最终输出已完成的、语法完整的Markdown内容。
4. animationText (动画文本工具)
-
职责: 为文本块的增量更新添加优雅的淡入动画。
-
工作原理:
-
增量提取: 通过比较新旧文本,仅提取出新增的部分(
delta)。 -
分块与渲染: 将新增文本分割成多个小块(
chunks),并为每个小块包裹一个带有CSS动画类的<span>标签。 -
精准动画触发: 仅当小块首次被挂载到DOM时,CSS动画才会触发。
- 为何如此? 这得益于React/Vue的
key机制和Diff算法。当组件更新时,Diff算法会识别出哪些span元素是之前已存在的(复用旧节点),哪些是本次新增的(创建新节点)。CSS动画通常只在元素被首次插入DOM树时生效,因此只有新增的span才会播放动画,从而实现了增量动画的效果。
- 为何如此? 这得益于React/Vue的
-
三、Vue2 版本实现细节
1. Props 设计
-
streamConfig: 流式渲染配置hasNextChunk: Boolean,是否开启流式缓存,默认为false。enableAnimation: Boolean,是否为文本添加淡入动画,默认为true。
-
markdownConfig: Object,透传给markdown-it的配置对象。 -
openLinksInNewTab: Boolean,控制所有链接是否在新的浏览器标签页中打开。
2. 核心流程
整个渲染流程始于外部以“打字机”形式不断输出内容,驱动内部组件联动更新。
-
数据流输入:
AiMarkdown组件的contentprop 随外部输入不断增加。 -
流式处理 (useStreaming) :
content的变化触发useStreamingHook。- Hook根据
hasNextChunk决定是否启用缓存逻辑。若启用,则对新增文本进行复杂的多层级缓存判断(代码块 -> 其他Markdown语法)。 - 处理完毕后,
useStreaming返回一个语法完整的Markdown字符串。
-
HTML转换 (Parser) :
- 计算属性依赖于
useStreaming的输出,一旦有新的完整Markdown内容,便立即调用parser的render方法。 parser将其转换为HTML字符串,在此过程中,所有自定义渲染规则(如链接重写、代码块处理)被应用。
- 计算属性依赖于
-
VNode渲染 (Render) :
-
HTML字符串更新后,触发
renderContent的计算属性。 -
在
renderContent函数中:-
使用
DOMPurify清洗HTML。 -
使用
DOMParser将其解析为DOM文档对象,并获取body节点。 -
递归遍历DOM节点,对每个节点进行类型判断和转换:
- 文本节点: 若非数学公式,则通过Vue的
h()函数创建一个animationText组件的VNode,并将文本内容作为prop传入。animationText组件内部负责实现增量动画逻辑。 - 元素节点: 若为
<mermaid>等自定义标签,则使用h()函数创建对应的MermaidChart组件的VNode。否则,递归处理其子节点,直至整棵DOM树被完全转换为VNode树。
- 文本节点: 若非数学公式,则通过Vue的
-
-
最终,
renderContent返回一个完整的VNode树。
-
-
最终渲染: 在模板中,通过
<component :is="renderContent" />指令,将这个动态生成的VNode树渲染为真实的DOM。