本文适配前端开发场景,提供 React/Vue/Svelte/Angular 全框架实现方案,附带代码高亮与最佳实践。


1. Markdown 渲染原理

LLM 天然支持输出 Markdown 格式内容(标题、列表、代码块、表格等),直接渲染为纯文本会浪费结构化信息。LangChain 前端流式渲染流程分为 3 步:

  1. 接收流数据useStream 钩子实时累积流式文本,更新 msg.text 状态
  2. 解析 Markdown:将原始文本转换为 HTML 或 React 元素树(单次解析 < 5ms)
  3. DOM 渲染:React 用虚拟 DOM _diffing,Vue/Svelte/Angular 用安全 HTML 指令渲染

2. 环境搭建:useStream 配置

首先安装依赖(以 npm 为例):

# 核心依赖
npm install @langchain/core @langchain/react
# 类型支持(TypeScript 项目)
npm install -D @types/langchain__core

2.1 类型定义(TypeScript)

import type { BaseMessage } from "@langchain/core/messages";

// 匹配 Agent 状态结构的接口
interface AgentState {
  messages: BaseMessage[]; // 消息列表
}

2.2 基础流式组件(React 示例)

import { useStream } from "@langchain/react";
import { AIMessage, HumanMessage } from "@langchain/core/messages";
import { Markdown } from "./Markdown"; // 后续实现的自定义组件

// Agent 服务地址(替换为你的实际地址)
const AGENT_URL = "http://localhost:2024";

export function Chat() {
  // 初始化流式连接
  const stream = useStream<AgentState>({
    apiUrl: AGENT_URL,
    assistantId: "simple_agent", // 替换为你的 Agent ID
  });

  return (
    <div className="chat-container">
      {/* 渲染消息列表 */}
      {stream.messages.map((msg) => (
        <div key={msg.id} className="message-bubble">
          {AIMessage.isInstance(msg) ? (
            // AI 消息:用自定义 Markdown 组件渲染
            <Markdown>{msg.text}</Markdown>
          ) : HumanMessage.isInstance(msg) ? (
            // 人类消息:普通文本渲染
            <p>{msg.text}</p>
          ) : null}
        </div>
      ))}
    </div>
  );
}

3. 框架适配:Markdown 库选择

不同前端框架推荐对应的 Markdown 解析库,兼顾性能与安全性:

框架 推荐库组合 输出格式 核心优势
React react-markdown + remark-gfm React 元素 组件化渲染,支持虚拟 DOM diffing,无需 dangerouslySetInnerHTML
Vue marked + dompurify 安全 HTML 轻量快速,原生支持 GitHub Flavored Markdown(GFM)
Svelte marked + dompurify 安全 HTML 与 Vue 一致的 API,适配 Svelte 模板语法 {@html}
Angular marked + dompurify 安全 HTML 兼容 Angular [innerHTML] 指令,防护 XSS 攻击

安装对应依赖

# React
npm install react-markdown remark-gfm
# Vue/Svelte/Angular
npm install marked dompurify

4. 自定义 Markdown 组件实现

4.1 React 组件

import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";

interface MarkdownProps {
  children: string; // 接收 Markdown 文本
}

export function Markdown({ children }: MarkdownProps) {
  // 过滤空内容,避免渲染空容器
  if (!children.trim()) return null;

  return (
    <div className="markdown-content">
      <ReactMarkdown 
        remarkPlugins={[remarkGfm]} // 启用 GFM 特性(表格、任务列表等)
        options={{
          breaks: true, // 单换行转为 <br>
        }}
      >
        {children}
      </ReactMarkdown>
    </div>
  );
}

4.2 Vue 组件(Vue 3 + TypeScript)

<template>
  <div class="markdown-content" v-html="safeHtml"></div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import marked from "marked";
import DOMPurify from "dompurify";

const props = defineProps<{
  content: string;
}>();

// 解析 +  sanitize 处理
const safeHtml = computed(() => {
  if (!props.content.trim()) return "";
  // 启用 GFM 和换行转换
  const rawHtml = marked(props.content, { gfm: true, breaks: true });
  //  sanitize HTML 防止 XSS
  return DOMPurify.sanitize(rawHtml);
});
</script>

4.3 Svelte 组件

<script lang="ts">
  import marked from "marked";
  import DOMPurify from "dompurify";

  export let content: string;

  $: rawHtml = marked(content, { gfm: true, breaks: true });
  $: safeHtml = DOMPurify.sanitize(rawHtml);
</script>

{#if content.trim()}
  <div class="markdown-content" {@html} safeHtml />
{/if}

4.4 Angular 组件

// markdown.component.ts
import { Component, Input } from "@angular/core";
import marked from "marked";
import DOMPurify from "dompurify";

@Component({
  selector: "app-markdown",
  template: `<div class="markdown-content" [innerHTML]="safeHtml"></div>`,
})
export class MarkdownComponent {
  @Input() content = "";

  get safeHtml(): string {
    if (!this.content.trim()) return "";
    const rawHtml = marked(this.content, { gfm: true, breaks: true });
    return DOMPurify.sanitize(rawHtml);
  }
}

5. HTML 输出安全:XSS 防护

LLM 输出可能包含恶意 HTML 片段(如 <script> 标签、onclick 事件),必须进行安全净化:

核心原则

  • React 例外react-markdown 直接生成 React 元素,无需额外 sanitize
  • 其他框架:使用 dompurify 过滤危险内容(官方仓库

净化示例(通用)

import DOMPurify from "dompurify";

// 原始 HTML(可能含恶意代码)
const rawHtml = marked(llmOutput);

// 安全净化后输出
const safeHtml = DOMPurify.sanitize(rawHtml, {
  // 可选:自定义允许的标签/属性
  ADD_TAGS: ["iframe"],
  ADD_ATTR: ["allowfullscreen"],
});

Dompurify 会自动过滤

  • <script><iframe>(默认)等危险标签
  • onclickonload 等事件属性
  • javascript: 伪协议链接
  • 其他 XSS 攻击向量

6. 流式渲染性能优化

useStream 会在每个 Token 到达时更新状态,触发 Markdown 重解析。默认方案适用于 99% 场景(单条消息 < 50KB),以下是极端场景优化:

6.1 节流渲染(避免频繁更新)

// React 示例:使用 requestAnimationFrame 节流
import { useRef, useState, useEffect } from "react";

export function ThrottledMarkdown({ children }: { children: string }) {
  const [throttledText, setThrottledText] = useState(children);
  const textRef = useRef(children);
  const frameRef = useRef<number | null>(null);

  useEffect(() => {
    textRef.current = children;
    if (!frameRef.current) {
      frameRef.current = requestAnimationFrame(() => {
        setThrottledText(textRef.current);
        frameRef.current = null;
      });
    }
    return () => {
      if (frameRef.current) cancelAnimationFrame(frameRef.current);
    };
  }, [children]);

  return (
    <div className="markdown-content">
      <ReactMarkdown remarkPlugins={[remarkGfm]}>
        {throttledText}
      </ReactMarkdown>
    </div>
  );
}

6.2 增量解析(超大文本优化)

// 核心思路:只解析新增内容,避免全量重解析
const parsedBuffer = useRef<string[]>([]); // 存储已解析的 HTML 片段
const lastTextRef = useRef(""); // 存储上一次的完整文本

// 对比新旧文本,只解析新增部分
const parseIncremental = (newText: string) => {
  const diff = newText.slice(lastTextRef.current.length);
  if (diff) {
    const parsedDiff = marked(diff);
    parsedBuffer.current.push(parsedDiff);
  }
  lastTextRef.current = newText;
  return DOMPurify.sanitize(parsedBuffer.current.join(""));
};

7. Markdown 样式定制

为聊天场景优化的紧凑样式(适配气泡布局),直接复制到全局 CSS:

/* Markdown 渲染容器基础样式 */
.markdown-content {
  font-size: 0.9rem;
  line-height: 1.5;
  color: #333;
}

/* 段落间距 */
.markdown-content p {
  margin: 0.4em 0;
}

/* 列表样式 */
.markdown-content ul,
.markdown-content ol {
  margin: 0.4em 0;
  padding-left: 1.4em;
}

/* 代码块样式 */
.markdown-content pre {
  overflow-x: auto;
  border-radius: 0.375rem;
  background: rgba(0, 0, 0, 0.05);
  padding: 0.8rem;
  margin: 0.8em 0;
  font-size: 0.85rem;
}

/* 行内代码样式 */
.markdown-content code {
  border-radius: 0.25rem;
  background: rgba(0, 0, 0, 0.08);
  padding: 0.125rem 0.25rem;
  font-size: 0.85rem;
}

/* 引用样式 */
.markdown-content blockquote {
  margin: 0.8em 0;
  padding-left: 0.75em;
  border-left: 3px solid #ccc;
  opacity: 0.8;
}

/* 表格样式 */
.markdown-content table {
  border-collapse: collapse;
  margin: 0.8em 0;
  width: 100%;
}

.markdown-content th,
.markdown-content td {
  border: 1px solid #e5e7eb;
  padding: 0.4em 0.6em;
  text-align: left;
}

.markdown-content th {
  background: rgba(0, 0, 0, 0.03);
}

/* 链接样式 */
.markdown-content a {
  color: #2563eb;
  text-decoration: underline;
}

.markdown-content a:hover {
  color: #1d4ed8;
}

8. 生产环境最佳实践

  1. 强制启用 GFM:LLM 普遍使用 GitHub 风格 Markdown,必须开启 gfm: true
  2. 严格 sanitize:Vue/Svelte/Angular 务必通过 dompurify 处理 HTML
  3. 处理空内容:避免渲染空的 Markdown 容器,提升 UI 整洁度
  4. 启用换行转换:设置 breaks: true,让 LLM 的单换行正常显示
  5. 适配聊天布局:使用紧凑样式,避免大间距影响气泡布局
  6. 测试边界场景:验证以下内容渲染效果:
    • 超长代码块(横向滚动)
    • 宽表格(响应式适配)
    • 嵌套列表(缩进正确)
    • 特殊字符(如 $、`、*)
  7. 监控性能:通过 Chrome DevTools Performance 面板,排查长消息(>50KB)的渲染卡顿

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐