基于 React 实现 Vditor 的可复用 Markdown 渲染组件

实现效果

什么是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
$$

## 图片示例

![示例图片](https://picsum.photos/900/600)
![示例图片](https://picsum.photos/900/600)

## 视频示例

使用 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 渲染方案。

本文只是示例,可自行按需添加修改!!!

相关推荐
EndingCoder2 小时前
React 与 TypeScript:组件类型化
前端·javascript·react.js·typescript·前端框架
沛沛老爹2 小时前
Web开发者实战:多模态Agent技能开发——语音交互与合成技能集成指南
java·开发语言·前端·人工智能·交互·skills
皮卡穆2 小时前
Vue3 + Swiper.js 实现无缝轮播图组件
前端·javascript·vue
民乐团扒谱机2 小时前
【数模美赛=美术大赛?】O奖论文图片复刻——高级绘图matlab代码集锦,让你摆脱画图“一眼MATLAB”的痛苦!
前端·人工智能·matlab
shehuiyuelaiyuehao2 小时前
图书管理系统
java·服务器·前端
打小就很皮...2 小时前
Vditor 实现混合模式评论,使用 Zustand 本地存储
前端·vditor·enablecomment
Xxtaoaooo2 小时前
React Native 跨平台鸿蒙开发实战:与鸿蒙原子化服务(Atomic Service)融合
react native·react.js·harmonyos
小二·10 小时前
Python Web 开发进阶实战 :AI 原生数字孪生 —— 在 Flask + Three.js 中构建物理世界实时仿真与优化平台
前端·人工智能·python
Amumu1213811 小时前
Vue组件化编程
前端·javascript·vue.js