实现效果





什么是Vditor?
Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora)和分屏预览模式。它使用 TypeScript 实现,支持原生 JavaScript 以及 Vue、React、Angular 和 Svelte 等框架。
参考官网可自行修改:https://b3log.org/vditor/
本文主要实现?
使用 Vditor 的 Markdown 渲染机制进行共用组件的封装,也就是使用这个库来做成一个可复用的组件,因为在实际的开发中,接口返回的Markdown的内容可能涵盖图片、视频等更复杂的内容,所以使用这个库,主要是用于数据的渲染
依赖安装及项目结构
1、首先安装 vditor 依赖:
npm install vditor
2、项目结构
css
src/view/pages/markdown/
├── Markdown.jsx # 示例页面(实时预览)
├── Markdown.module.scss # 页面样式
├── MarkdownRenderer.jsx # 核心可复用组件
├── MarkdownRenderer.module.scss # 组件样式
核心组件如何实现?
1. 组件参数定义
javascript
const MarkdownRenderer = ({
content = '', // Markdown 内容
theme = 'light', // 主题:light/dark/classic
lang = 'zh_CN', // 语言
className = '', // 自定义类名
onAfterRender, // 渲染完成回调
enableImagePreview = true, // 启用图片预览
enableOutline = true // 启用目录大纲
}) => {
2. 核心状态管理
javascript
// 使用 useRef 存储 DOM 引用和回调函数,避免不必要的重渲染
const containerRef = useRef(null); // Markdown 内容容器
const onAfterRenderRef = useRef(onAfterRender); // 回调函数引用
const [outlineItems, setOutlineItems] = useState([]); // 大纲数据
3. 大纲生成
javascript
// 从渲染后的 DOM 中提取所有标题(h1-h6)
const generateOutline = () => {
if (!containerRef.current) {
setOutlineItems([]);
return;
}
// 查询所有标题元素
const headings = containerRef.current.querySelectorAll('h1, h2, h3, h4, h5, h6');
const items = [];
headings.forEach((heading, index) => {
const level = parseInt(heading.tagName.charAt(1)); // 提取标题级别 1-6
const text = heading.textContent || ''; // 标题文本
const id = `heading-${index}`; // 唯一 ID
// 为标题添加 ID,用于锚点跳转
heading.id = id;
items.push({ id, text, level });
});
setOutlineItems(items);
};
4. 平滑滚动实现
javascript
const scrollToHeading = (id) => {
const element = document.getElementById(id);
if (element) {
// 使用原生 API 实现平滑滚动
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
5. Vditor 渲染核心逻辑
javascript
useEffect(() => {
if (!containerRef.current) return;
// 空内容处理
if (!content || content.trim() === '') {
containerRef.current.innerHTML = '';
setOutlineItems([]);
return;
}
// 清空容器,避免重复渲染
containerRef.current.innerHTML = '';
try {
// 调用 Vditor.preview API 进行渲染
Vditor.preview(containerRef.current, content, {
mode: theme === 'dark' ? 'dark' : 'light',
lang: lang,
cdn: 'https://unpkg.com/vditor@3.11.2', // CDN 配置
hljs: {
enable: true,
style: theme === 'dark' ? 'github-dark' : 'github'
},
math: {
engine: 'KaTeX', // 数学公式引擎
inlineDigit: true,
macros: {}
},
speech: {
enable: true // 语音朗读
}
})
.then(() => {
// 渲染后的增强处理
enhanceRenderedContent();
})
.catch((error) => {
console.error('Error rendering Markdown:', error);
});
} catch (error) {
console.error('Error calling Vditor.preview:', error);
}
// 清理函数:组件卸载时清空内容
return () => {
if (containerRef.current) {
containerRef.current.innerHTML = '';
}
};
}, [content, theme, lang, enableImagePreview, enableOutline]);
6. 图片预览功能实现
javascript
// 在渲染完成后,将 img 标签替换为 Antd Image 组件
if (enableImagePreview && containerRef.current) {
const images = containerRef.current.querySelectorAll('img');
images.forEach((img, index) => {
const src = img.src;
const alt = img.alt || `Image ${index + 1}`;
const parent = img.parentNode;
// 创建包装容器
const wrapper = document.createElement('div');
wrapper.className = 'vditor-image-wrapper';
// 创建 React 根节点
const imageRoot = document.createElement('div');
wrapper.appendChild(imageRoot);
parent?.replaceChild(wrapper, img);
// 动态导入 react-dom/client 并渲染 Image 组件
import('react-dom/client').then(({ createRoot }) => {
const root = createRoot(imageRoot);
root.render(
<Image
src={src}
alt={alt}
preview={enableImagePreview}
style={{
maxWidth: '100%',
height: 'auto',
borderRadius: '8px',
cursor: 'pointer'
}}
/>
);
});
});
}
技术要点:
- 使用动态 React 渲染将原生 `<img>` 替换为 Antd `<Image>`
- 自动处理跨域图片预览
- 支持缩放、旋转等高级功能
7. 视频播放功能
javascript
// 确保 video 标签有完整的播放控件
const videos = containerRef.current.querySelectorAll('video');
videos.forEach(video => {
video.controls = true; // 添加播放控件
});
8. 渲染结构
javascript
return (
<div className={styles.markdownWrapper}>
{/* 大纲侧边栏 */}
{enableOutline && outlineItems.length > 0 && (
<div className={styles.outline}>
<div className={styles.outlineHeader}>目录</div>
<ul className={styles.outlineList}>
{outlineItems.map((item) => (
<li
key={item.id}
className={styles.outlineItem}
// 根据标题级别动态设置缩进
style={{ paddingLeft: `${(item.level - 1) * 16}px` }}
>
<a
href={`#${item.id}`}
onClick={(e) => {
e.preventDefault();
scrollToHeading(item.id);
}}
className={styles.outlineLink}
>
{item.text}
</a>
</li>
))}
</ul>
</div>
)}
{/* Markdown 内容区域 */}
<div
ref={containerRef}
className={`${styles.markdownRenderer} ${className}`}
/>
</div>
);
9. 核心组件样式实现
css
.markdownWrapper {
display: flex;
gap: 24px;
position: relative;
}
// 大纲侧边栏样式
.outline {
position: sticky;
top: 24px;
width: 240px;
flex-shrink: 0;
max-height: calc(100vh - 48px);
overflow-y: auto;
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
align-self: flex-start;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
&:hover {
background: #bfbfbf;
}
}
}
.outlineHeader {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e8e8e8;
color: #262626;
}
.outlineList {
list-style: none;
padding: 0;
margin: 0;
}
.outlineItem {
margin: 4px 0;
line-height: 1.8;
}
.outlineLink {
display: block;
color: #595959;
text-decoration: none;
font-size: 14px;
transition: all 0.2s;
border-radius: 4px;
padding: 4px 8px;
&:hover {
color: #1890ff;
background: #f0f9ff;
}
}
.markdownRenderer {
flex: 1;
width: 100%;
min-height: 200px;
// Vditor 预览区域的样式调整
:global(.vditor) {
border: none !important;
.vditor-content {
background: transparent;
}
.vditor-ir {
background: transparent;
}
.vditor-ir__node--pre {
background: #f6f8fa;
border-radius: 6px;
}
.vditor-ir__node--block {
pre {
background: #f6f8fa;
}
}
}
// 暗色主题适配
:global(.vditor--dark) {
.vditor-ir__node--pre {
background: #1e1e1e;
}
.vditor-ir__node--block {
pre {
background: #1e1e1e;
}
}
}
// 图片样式和交互效果
:global(.vditor) {
img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
cursor: pointer;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
transform: scale(1.02);
}
}
}
// 视频容器样式
:global(.vditor) {
video {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #000;
margin: 16px 0;
}
}
// 代码块样式
:global(.vditor-ir__node--pre) {
code {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
}
}
// 引用块样式
:global(.vditor-ir__node--blockquote) {
border-left: 4px solid #dfe2e5;
padding-left: 16px;
color: #6a737d;
margin: 16px 0;
}
// 表格样式
:global(.vditor-ir__node--table) {
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
th,
td {
border: 1px solid #dfe2e5;
padding: 8px 12px;
}
th {
background: #f6f8fa;
font-weight: 600;
}
}
}
// 链接样式
:global(.vditor-ir__marker--link) {
a {
color: #0366d6;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
// 列表样式
:global(.vditor-ir__node--list) {
ul,
ol {
padding-left: 24px;
margin: 8px 0;
}
li {
margin: 4px 0;
}
}
// 标题样式
:global(.vditor-ir__node--heading) {
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 24px 0 16px;
font-weight: 600;
line-height: 1.25;
scroll-margin-top: 24px;
&:first-child {
margin-top: 0;
}
}
h1 {
font-size: 2em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
h2 {
font-size: 1.5em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
h3 {
font-size: 1.25em;
}
h4 {
font-size: 1em;
}
h5 {
font-size: 0.875em;
}
h6 {
font-size: 0.85em;
color: #6a737d;
}
}
}
实时预览组件
该组件主要是根据获取到的 Markdown 数据通过 props 传递给核心组件用于渲染。
其主要包含:
- header 内容显示区域
- 主题选择器和操作按钮
- 输入框和预览区域
核心代码及 Mock 数据
javascript
import { useState } from 'react';
import { Card, Input, Button, Space, Radio } from 'antd';
import MarkdownRenderer from './MarkdownRenderer';
import styles from './Markdown.module.scss';
const { TextArea } = Input;
// 示例 Markdown 内容
const DEMO_CONTENT = `# Vditor Markdown 渲染示例
这是一个基于 Vditor 的可复用 Markdown 渲染组件,支持丰富的语法和媒体内容。
## 功能特性
- ✅ 支持标准 Markdown 语法
- ✅ 支持代码高亮
- ✅ 支持数学公式
- ✅ 支持图片、视频等媒体内容
- ✅ 支持表格、引用等复杂结构
- ✅ 支持亮色/暗色主题切换
## 代码块示例
\`\`\`javascript
// JavaScript 代码示例
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(10)); // 输出: 55
\`\`\`
\`\`\`python
# Python 代码示例
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
\`\`\`
## 图表示例(Mermaid)
### 1. 流程图 (Flowchart)
\`\`\`mermaid
flowchart TD
A[开始] --> B{是否满足条件?}
B -->|是| C[执行操作 A]
B -->|否| D[执行操作 B]
C --> E[结束]
D --> E
E --> F{需要重试?}
F -->|是| B
F -->|否| G[完成]
style A fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#bbf,stroke:#333,stroke-width:2px
style G fill:#bfb,stroke:#333,stroke-width:2px
\`\`\`
### 2. 时序图 (Sequence Diagram)
\`\`\`mermaid
sequenceDiagram
participant 用户
participant 浏览器
participant 服务器
participant 数据库
用户->>浏览器: 输入 URL
浏览器->>服务器: 发送 HTTP 请求
服务器->>数据库: 查询数据
数据库-->>服务器: 返回查询结果
服务器-->>浏览器: 返回 HTML 响应
浏览器-->>用户: 渲染页面
Note over 浏览器,数据库: 完整的请求-响应流程
\`\`\`
### 3. 甘特图 (Gantt Chart)
\`\`\`mermaid
gantt
title 项目开发进度
dateFormat YYYY-MM-DD
section 需求分析
需求收集 :done, des1, 2024-01-01, 2024-01-05
需求文档编写 :active, des2, 2024-01-06, 3d
需求评审 : des3, after des2, 2d
section 设计阶段
系统设计 : des4, 2024-01-10, 5d
UI 设计 : des5, 2024-01-12, 4d
section 开发阶段
前端开发 : des6, 2024-01-15, 10d
后端开发 : des7, 2024-01-15, 12d
接口联调 : des8, after des6, 5d
section 测试阶段
单元测试 : des9, after des8, 3d
集成测试 : des10, after des9, 5d
上线部署 :crit, des11, 2024-02-05, 2d
\`\`\`
### 4. 思维导图 (Mindmap)
\`\`\`mermaid
mindmap
root((前端开发))
HTML
语义化标签
表单元素
多媒体
CSS
Flexbox
Grid
动画
响应式设计
JavaScript
ES6+
DOM 操作
异步编程
模块化
框架
React
Vue
Angular
工程化
Webpack
Vite
Git
\`\`\`
### 5. 状态图 (State Diagram)
\`\`\`mermaid
stateDiagram-v2
[*] --> 待支付
待支付 --> 已支付: 支付成功
待支付 --> 已取消: 取消订单
待支付 --> 已过期: 超时
已支付 --> 配送中: 开始配送
配送中 --> 已完成: 签收确认
配送中 --> 退款中: 申请退款
退款中 --> 已退款: 退款成功
退款中 --> 已完成: 退款失败
已完成 --> [*]
已取消 --> [*]
已过期 --> [*]
已退款 --> [*]
note right of 待支付: 用户下单后的初始状态
note left of 已完成: 订单完成状态
\`\`\`
### 6. 类图 (Class Diagram)
\`\`\`mermaid
classDiagram
class Animal {
+String name
+int age
+eat() void
+sleep() void
}
class Dog {
+String breed
+bark() void
+fetch() void
}
class Cat {
+String color
+meow() void
+scratch() void
}
Animal <|-- Dog: 继承
Animal <|-- Cat: 继承
class Owner {
+String name
+Animal[] pets
+addPet(Animal) void
+feedPet(Animal) void
}
Owner "1" --> "*" Animal: 拥有
\`\`\`
### 7. ER 图 (Entity Relationship)
\`\`\`mermaid
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE_ITEM : contains
PRODUCT ||--o{ LINE_ITEM : "ordered in"
CUSTOMER }|..|{ DELIVERY_ADDRESS : uses
CUSTOMER {
int id PK
string name
string email
string phone
}
ORDER {
int id PK
int customer_id FK
date order_date
float total_amount
string status
}
PRODUCT {
int id PK
string name
float price
int stock
}
LINE_ITEM {
int id PK
int order_id FK
int product_id FK
int quantity
float unit_price
}
\`\`\`
### 8. 饼图 (Pie Chart)
\`\`\`mermaid
pie title 技术栈使用占比
"React" : 35
"Vue" : 25
"Angular" : 15
"jQuery" : 10
"其他" : 15
\`\`\`
### 9. 用户旅程图 (User Journey)
\`\`\`mermaid
journey
title 用户购物体验旅程
section 浏览商品
浏览首页: 5: 用户
搜索商品: 4: 用户
查看详情: 5: 用户
section 下单流程
加入购物车: 5: 用户
填写地址: 3: 用户
选择支付: 4: 用户
section 售后服务
收到商品: 5: 用户
申请退款: 2: 用户
完成评价: 4: 用户
\`\`\`
### 10. Git 分支图 (Git Graph)
\`\`\`mermaid
gitGraph
commit
commit
branch develop
checkout develop
commit
commit
checkout main
merge develop
commit
branch feature
checkout feature
commit
commit
checkout develop
merge feature
checkout main
merge develop
\`\`\`
## 表格示例
| 功能 | 状态 | 说明 |
|------|------|------|
| Markdown 渲染 | ✅ | 完整支持 |
| 代码高亮 | ✅ | 多语言支持 |
| 数学公式 | ✅ | LaTeX 语法 |
| 图片渲染 | ✅ | 自动适配 |
| 视频支持 | ✅ | HTML5 视频 |
## 引用块示例
> 这是一个引用块示例。
>
> Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染和分屏预览模式。
## 列表示例
### 无序列表
- 第一项
- 第二项
- 子项 1
- 子项 2
- 第三项
### 有序列表
1. 第一步
2. 第二步
3. 第三步
## 数学公式示例
行内公式:$E = mc^2$
块级公式:
$$
\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}
$$
$$
f(x) = \\int_{-\\infty}^{\\infty} \\hat{f}(\\xi)\\,e^{2\\pi i \\xi x} \\,d\\xi
$$
## 图片示例


## 视频示例
使用 HTML5 video 标签嵌入视频:
<video controls src="https://media.w3.org/2010/05/sintel/trailer.mp4" width="600"></video>
或者使用示例视频:
<video controls poster="https://peach.blender.org/wp-content/uploads/title_anouncement.jpg?x11217" width="600">
<source src="https://peach.blender.org/wp-content/uploads/big_buck_bunny_720p_30mb.mp4" type="video/mp4">
您的浏览器不支持视频标签。
</video>
## 任务列表
- [x] 完成 Vditor 集成
- [x] 创建可复用组件
- [x] 添加样式支持
- [ ] 添加更多自定义选项
- [ ] 编写单元测试
---
**提示**:在上方文本框中输入 Markdown 内容,下方会实时预览渲染效果。
`;
const Markdown = () => {
const [content, setContent] = useState(DEMO_CONTENT);
const [theme, setTheme] = useState('light');
const [loading, setLoading] = useState(false);
const handleContentChange = (e) => {
setContent(e.target.value);
};
const handleThemeChange = (e) => {
setTheme(e.target.value);
};
const handleLoadFromAPI = () => {
setLoading(true);
// 模拟从 API 加载 Markdown 内容
setTimeout(() => {
setContent(`# 从 API 加载的内容
这是模拟从后端接口获取的 Markdown 内容。
## 接口返回数据示例
\`\`\`json
{
"title": "文章标题",
"content": "Markdown 格式的内容",
"author": "作者名",
"timestamp": "2024-01-01"
}
\`\`\`
## 使用说明
在实际项目中,你可以通过以下方式使用这个组件:
\`\`\`jsx
import MarkdownRenderer from './MarkdownRenderer';
function MyComponent() {
const [markdown, setMarkdown] = useState('');
useEffect(() => {
// 从 API 获取 Markdown 内容
fetch('/api/article/1')
.then(res => res.json())
.then(data => setMarkdown(data.content));
}, []);
return <MarkdownRenderer content={markdown} />;
}
\`\`\`
`);
setLoading(false);
}, 1000);
};
const handleClear = () => {
setContent('');
};
return (
<div className={styles.markdown}>
<div className={styles.header}>
<h1>Markdown 渲染组件示例</h1>
<p>基于 Vditor 的可复用 Markdown 渲染组件</p>
</div>
<div className={styles.controls}>
<Space>
<span>主题:</span>
<Radio.Group value={theme} onChange={handleThemeChange}>
<Radio.Button value="light">亮色</Radio.Button>
<Radio.Button value="dark">暗色</Radio.Button>
<Radio.Button value="classic">经典</Radio.Button>
</Radio.Group>
</Space>
<Space className={styles.buttons}>
<Button type="primary" onClick={handleLoadFromAPI} loading={loading}>
模拟 API 加载
</Button>
<Button onClick={handleClear}>清空</Button>
<Button onClick={() => setContent(DEMO_CONTENT)}>重置示例</Button>
</Space>
</div>
<div className={styles.content}>
<Card title="输入 Markdown 内容" className={styles.inputCard}>
<TextArea
value={content}
onChange={handleContentChange}
placeholder="在这里输入 Markdown 内容..."
autoSize={{ minRows: 10, maxRows: 20 }}
style={{ fontFamily: 'monospace' }}
/>
</Card>
<Card title="渲染预览" className={styles.previewCard}>
<MarkdownRenderer
content={content}
theme={theme}
/>
</Card>
</div>
</div>
);
};
export default Markdown;
组件样式
css
.markdown {
padding: 24px;
max-width: 1600px;
margin: 0 auto;
}
.header {
margin-bottom: 24px;
h1 {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 600;
}
p {
margin: 0;
color: #666;
font-size: 14px;
}
}
.controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px;
background: #f5f5f5;
border-radius: 8px;
flex-wrap: wrap;
gap: 16px;
}
.buttons {
display: flex;
gap: 8px;
}
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
align-items: start;
@media (max-width: 1200px) {
grid-template-columns: 1fr;
}
}
// .inputCard,
.previewCard {
width: 1000px;
height: fit-content;
:global(.ant-card-head) {
position: sticky;
top: 0;
z-index: 10;
background: #fff;
}
:global(.ant-card-body) {
max-height: calc(100vh - 200px);
overflow: auto;
}
}
.inputCard {
:global(.ant-card-body) {
padding: 16px;
}
}
.previewCard {
:global(.ant-card-body) {
padding: 0;
}
}
数据流转过程

常见问题解决方案
问题 1:图片跨域
使用 Antd Image 组件自动处理
问题 2:大纲同步更新
在 Vditor 渲染完成后提取标题
问题 3:重复渲染
每次渲染前清空容器 `innerHTML = ''`
问题 4:内存泄漏
useEffect 返回清理函数
Vditor 对比 marked、markdown-it、react-markdown
| 工具 | 点评 | 适用场景 | 核心缺陷/特点 |
|---|---|---|---|
| Marked | "快但简陋的直男。" | 简单 Markdown → HTML 转换,不依赖复杂功能(如公式、图表),追求速度和轻量包体积。 | 1. 扩展性差 2. 在 React 中必须用 dangerouslySetInnerHTML,违反 React 推荐实践 |
| Markdown-it | "灵活的瑞士军刀。" | 需要高度自定义解析规则(如造编辑器、定制语法),或是希望使用业界通用解析器(VS Code、VuePress 同款)。 | 1. 在 React 中同样要处理 HTML 字符串注入 2. 配齐插件(数学公式、流程图、任务列表等)比较繁琐 |
| React-Markdown | "React 亲儿子。" | 希望完全符合 React 哲学,将 Markdown 各部分替换为 React 组件(如用 Next.js Image 替换 img、自定义代码块)。 | 配置复杂内容(视频、数学公式)需理解 remark/rehype AST 转换逻辑,学习门槛较高 |
| Vditor | "开箱即用的重型装甲车。" | 后端返回复杂内容(视频、图表、数学公式等)混杂,希望一次性渲染、自带样式与交互(图片点击放大、多媒体解析)。 | 优势:唯一自带完整样式、交互、多媒体支持的方案,适合"丢进去就能漂亮显示"的场景 |
总述
本文通过 Vditor 的 preview API 实现了高效的 Markdown 渲染,结合 Antd Image 实现了图片预览功能,自动提取标题生成大纲,是一个功能完整、性能优良、可复用的 Markdown 渲染方案。
本文只是示例,可自行按需添加修改!!!