Markdown Renderer
Markdown Renderer 是前端将 LLM 输出的 Markdown 文本渲染为 React 组件的模块。它需要处理 GFM 语法、代码高亮、安全性和自定义组件。
#type / concept
#status / evergreen
#tech / frontend
#tech / ai
[!info] related notes
- 所属 MOC: AI Agent Application MOC
- 相关: Streaming Renderer, Chat UI
- 依赖: react-markdown, remark-gfm, rehype-highlight (npm 包)
Markdown Renderer
一句话定义
Markdown Renderer 是前端将 LLM 输出的 Markdown 文本渲染为 React 组件的模块。LLM 的输出天然包含 Markdown(标题、列表、代码块、表格),需要专门的渲染器来正确显示。
它解决什么问题
LLM 的输出是 Markdown 格式的文本。直接用 dangerouslySetInnerHTML 渲染有 XSS 风险。直接用 <pre> 显示又丢失了格式。需要一个安全、功能完整、可定制的渲染器。
核心原理
渲染管线
Markdown 字符串
│
▼
remark 解析 → mdast (Markdown 语法树)
│
▼
remark 插件处理 (remark-gfm: 表格、任务列表、删除线)
│
▼
remark-rehype 转换 → hast (HTML 语法树)
│
▼
rehype 插件处理 (rehype-highlight: 代码高亮)
│
▼
React 组件映射 → React Elements
核心组件
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
function MarkdownRenderer({ content }: { content: string }) {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// 自定义代码块渲染
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={oneDark}
language={match[1]}
PreTag="div"
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
// 自定义表格渲染
table({ children }) {
return <div className="table-wrapper"><table>{children}</table></div>;
},
// 自定义链接渲染
a({ href, children }) {
return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>;
},
}}
>
{content}
</ReactMarkdown>
);
}
remark-gfm 支持的语法
| 语法 | 示例 | 说明 |
|---|---|---|
| 表格 | | A | B | | GFM 表格 |
| 任务列表 | - [x] done | 复选框 |
| 删除线 | ~~strikethrough~~ | 删除线 |
| 自动链接 | https://example.com | 自动识别 URL |
典型工程实现
安全性
react-markdown 默认安全,不使用 dangerouslySetInnerHTML。但需要注意:
// URL 安全:默认只允许 http/https/mailto 等协议
// 阻止 javascript: 等危险协议
<ReactMarkdown
urlTransform={(url) => {
if (url.startsWith('javascript:')) return '';
return url;
}}
>
{content}
</ReactMarkdown>
自定义组件
const customComponents = {
// 代码块带复制按钮
code({ node, inline, className, children, ...props }) {
const [copied, setCopied] = useState(false);
const text = String(children).replace(/\n$/, '');
if (!inline) {
return (
<div className="code-block">
<button onClick={() => {
navigator.clipboard.writeText(text);
setCopied(true);
}}>
{copied ? '✅' : '📋'}
</button>
<SyntaxHighlighter language="javascript" style={oneDark}>
{text}
</SyntaxHighlighter>
</div>
);
}
return <code>{children}</code>;
},
};
常见坑
- 用 dangerouslySetInnerHTML: XSS 风险
- 不做代码高亮: 代码块没有语法高亮,可读性差
- 表格不滚动: 宽表格溢出容器
- 图片不限制大小: 大图片撑破布局
- 链接不加 target=“_blank”: 点击链接离开当前页面