Markdown 渲染的工作原理
渲染管道有三个步骤:- 接收:
useStream将流式传输的文本累积到每个 AI 消息的msg.text中,在新令牌到达时响应式更新。 - 解析: Markdown 解析器将原始文本转换为 HTML(或 React 元素树)。这在每次更新时运行,但对于聊天长度的内容足够快(5 KB 消息 < 5ms)。
- 渲染: 解析后的输出渲染到 DOM。React 使用虚拟 DOM diffing;Vue 和 Svelte 使用
v-html/{@html}与清理后的 HTML。
设置 useStream
Markdown 模式使用简单的聊天代理,无需特殊配置。使用您的代理 URL 和助手 ID 连接useStream。
定义一个与代理状态模式匹配的 TypeScript 接口,并将其作为类型参数传递给 useStream,以便对状态值进行类型安全访问。在下面的示例中,将 typeof myAgent 替换为您的接口名称:
选择 Markdown 库
每个框架都有自然的 markdown 渲染选择:| 框架 | 库 | 输出 | 原因 |
|---|---|---|---|
| React | react-markdown + remark-gfm | React 元素 | 基于组件,虚拟 DOM diffing,无 dangerouslySetInnerHTML |
| Vue | marked + dompurify | 通过 v-html 清理后的 HTML | 轻量、快速,内置 GFM |
| Svelte | marked + dompurify | 通过 {@html} 清理后的 HTML | 与 Vue 相同,一致的 API |
| Angular | marked + dompurify | 通过 [innerHTML] 清理后的 HTML | 与 Vue/Svelte 相同 |
构建 Markdown 组件
清理 HTML 输出
当将解析后的 markdown 渲染为原始 HTML(v-html、{@html}、[innerHTML])时,必须清理输出以防止跨站脚本(XSS)。LLM 响应可能包含任意文本,包括 markdown 解析器可能转换为可执行 HTML 的标记。
使用 dompurify 剥离危险元素:
<script> 标签、onclick 属性、javascript: URL 和其他 XSS 向量,同时保留安全的 markdown 输出,如标题、列表、代码块、表格和链接。
React 的
react-markdown 不需要 dompurify,因为它直接生成 React 元素,不涉及原始 HTML 注入。流式传输注意事项
useStream 在每个令牌到达时响应式更新 msg.text。Markdown 组件在每次更新时重新解析。对于典型的聊天消息,这是高性能的:
marked以约 1 MB/s 的速度解析。5 KB 消息需要 < 5msreact-markdown+ remark 管道对于聊天长度的内容同样快速- 浏览器的布局引擎高效处理 DOM 更新
- 节流渲染: 使用
requestAnimationFrame以 60fps 的速度批量更新,而不是在每个令牌上重新渲染 - 增量解析: 仅解析新内容并追加到渲染缓冲区(高级,通常不需要用于聊天 UI)
对于大多数聊天应用程序,在每个令牌上重新解析完整消息的简单方法是足够的。只有在观察到非常长的消息出现滚动卡顿或掉帧时才进行优化。
为 markdown 内容添加样式
将样式应用于.markdown-content 类来控制渲染后的 markdown 的外观。这些是基本样式:
最佳实践
- 始终清理: 当使用
v-html、{@html}或[innerHTML]时,务必通过dompurify运行解析后的输出。永远不要信任来自 LLM 输出的 markdown 解析器的原始 HTML。 - 启用 GFM: GitHub 风格 Markdown 添加了表格、删除线、任务列表和自动链接。这些功能是 LLM 常用的。
- 处理空内容: 在解析前检查空字符串以避免渲染空容器。
- 使用
breaks: true: 启用换行符转换,以便 LLM 输出中的单个换行符渲染为<br>而不是被忽略。LLM 通常使用单个换行符进行视觉分隔。 - 为聊天上下文设置样式: 使用适合聊天气泡的紧凑边距和尺寸,而不是全宽文章布局。
- 使用丰富内容测试: 使用标题、嵌套列表、长行代码块、宽表格和引用块验证渲染,以发现溢出或布局问题。

