x-markdown的实现方式

10 阅读6分钟

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)的组件树进行渲染。

  • 工作流程与技术要点:

    1. 安全清洗: 使用 DOMPurify库对输入的HTML字符串进行净化,彻底防止XSS攻击。

    2. HTML解析: 利用 html-react-parser(React) 或 DOMParser(Vue) 将净化后的HTML字符串解析为DOM节点树。

    3. 自定义组件替换与状态管理:

      • 支持将特定的HTML标签(如 <mermaid>)替换为自定义的React/Vue组件。
      • 关键机制:未闭合标签处理。在解析过程中,如果遇到尚未闭合的自定义标签(如代码块),渲染器会向该组件传递一个特殊状态(如 unclosedTags),告知其“渲染暂停”。只有当对应的闭合标签被匹配后,才更新状态(如 unclosedTags: []),触发组件的正式渲染。
      • 此机制的优势: 有效避免了因内容逐步加载而导致的UI“闪动”(Flash of Unstyled Code, FOUC),尤其在代码高亮场景中至关重要。
    4. 动画集成: 对于普通文本节点,会将其交由 animationText工具处理,以实现增量动画效果。

3. useStream (流式渲染Hook)
  • 职责: 处理从AI接口流式返回的、可能不完整的Markdown片段。它维护一个缓存,暂存未形成完整语法的片段,待其闭合后再一并渲染。

  • 工作流程:

    1. 监听输入的Markdown内容流。
    2. 若开启缓存模式 (hasNextChunk),则将新到来的内容与缓存中的未完成片段合并。
    3. 智能缓存判断: 遍历合并后的内容,优先检查是否处于代码块内部(通过匹配 ``` 或 ~~~ 围栏)。若在围栏内,则持续缓存直至检测到有效的结束围栏。
    4. 语法闭合分析: 若不在代码块内,则使用语法分析器判断其他Markdown结构(如粗体、斜体、表格)是否已正确闭合。对已闭合的结构,将其从缓存中“提交”并准备渲染;对未闭合的,则继续缓存并更新其内部状态标记。
    5. 最终输出已完成的、语法完整的Markdown内容。
4. animationText (动画文本工具)
  • 职责: 为文本块的增量更新添加优雅的淡入动画。

  • 工作原理:

    1. 增量提取: 通过比较新旧文本,仅提取出新增的部分(delta)。

    2. 分块与渲染: 将新增文本分割成多个小块(chunks),并为每个小块包裹一个带有CSS动画类的 <span>标签。

    3. 精准动画触发: 仅当小块首次被挂载到DOM时,CSS动画才会触发

      • 为何如此? ​ 这得益于React/Vue的 key机制和Diff算法。当组件更新时,Diff算法会识别出哪些 span元素是之前已存在的(复用旧节点),哪些是本次新增的(创建新节点)。CSS动画通常只在元素被首次插入DOM树时生效,因此只有新增的 span才会播放动画,从而实现了增量动画的效果。

三、Vue2 版本实现细节

1. Props 设计
  • streamConfig: 流式渲染配置

    • hasNextChunk: Boolean,是否开启流式缓存,默认为 false
    • enableAnimation: Boolean,是否为文本添加淡入动画,默认为 true
  • markdownConfig: Object,透传给 markdown-it的配置对象。

  • openLinksInNewTab: Boolean,控制所有链接是否在新的浏览器标签页中打开。

2. 核心流程

整个渲染流程始于外部以“打字机”形式不断输出内容,驱动内部组件联动更新。

  1. 数据流输入: AiMarkdown组件的 contentprop 随外部输入不断增加。

  2. 流式处理 (useStreaming) :

    • content的变化触发 useStreamingHook。
    • Hook根据 hasNextChunk决定是否启用缓存逻辑。若启用,则对新增文本进行复杂的多层级缓存判断(代码块 -> 其他Markdown语法)。
    • 处理完毕后,useStreaming返回一个语法完整的Markdown字符串。
  3. HTML转换 (Parser) :

    • 计算属性依赖于 useStreaming的输出,一旦有新的完整Markdown内容,便立即调用 parserrender方法。
    • parser将其转换为HTML字符串,在此过程中,所有自定义渲染规则(如链接重写、代码块处理)被应用。
  4. VNode渲染 (Render) :

    • HTML字符串更新后,触发 renderContent的计算属性。

    • renderContent函数中:

      • 使用 DOMPurify清洗HTML。

      • 使用 DOMParser将其解析为DOM文档对象,并获取 body节点。

      • 递归遍历DOM节点,对每个节点进行类型判断和转换:

        • 文本节点: 若非数学公式,则通过Vue的 h()函数创建一个 animationText组件的VNode,并将文本内容作为prop传入。animationText组件内部负责实现增量动画逻辑。
        • 元素节点: 若为 <mermaid>等自定义标签,则使用 h()函数创建对应的 MermaidChart组件的VNode。否则,递归处理其子节点,直至整棵DOM树被完全转换为VNode树。
    • 最终,renderContent返回一个完整的VNode树。

  5. 最终渲染: 在模板中,通过 <component :is="renderContent" />指令,将这个动态生成的VNode树渲染为真实的DOM。