Skip to content

MDX 及 React 组件

MDX 将 Markdown 的简单性与 React 组件的强大功能相结合,让您能够创建交互式、动态的文档。本章探讨如何在 Rspress 中利用 MDX 构建引人入胜的文档体验。

什么是 MDX?

MDX 概述

MDX 是一种格式,允许您直接在 Markdown 文件中编写 JSX。您可以在传统 Markdown 语法旁边导入和使用 React 组件。

mdx
# 普通 Markdown 标题

这是普通的 Markdown 文本。

<CustomButton onClick={() => alert('你好!')}>
  点击我
</CustomButton>

## 更多 Markdown

继续编写 Markdown...

为什么使用 MDX?

  • 交互示例: 嵌入实时代码演示
  • 自定义组件: 创建可重用的文档元素
  • 动态内容: 以编程方式生成内容
  • 丰富可视化: 添加图表、图表和交互式媒体
  • 增强用户体验: 构建更好的用户体验

基本 MDX 语法

导入组件

直接在 MDX 中导入 React 组件:

mdx
import { Alert } from '../components/Alert';
import { CodeBlock } from '../components/CodeBlock';

# 我的文档

<Alert type="info">
  这是一条信息消息!
</Alert>

<CodeBlock language="javascript">
  console.log('Hello World');
</CodeBlock>

内联 JSX

无需导入即可编写内联 JSX:

mdx
# 交互按钮

<button
  onClick={() => alert('已点击!')}
  style={{ padding: '10px 20px', background: '#007bff', color: 'white' }}
>
  点击我
</button>

JavaScript 表达式

在内容中使用 JavaScript 表达式:

mdx
export const name = 'Rspress';
export const version = '1.0.0';

# 欢迎使用 {name}

当前版本: **{version}**

发布年份: {new Date().getFullYear()}

创建自定义组件

简单组件

创建可重用组件:

tsx
// components/Highlight.tsx
import React from 'react';

interface HighlightProps {
  children: React.ReactNode;
  color?: string;
}

export function Highlight({ children, color = 'yellow' }: HighlightProps) {
  return (
    <span
      style={{
        backgroundColor: color,
        padding: '2px 6px',
        borderRadius: '3px',
      }}
    >
      {children}
    </span>
  );
}

在 MDX 中使用:

mdx
import { Highlight } from '../components/Highlight';

# 文档

这是 <Highlight>重要文本</Highlight> 需要记住。

这也是 <Highlight color="lightblue">重要的</Highlight>。

交互组件

创建交互演示组件:

tsx
// components/Counter.tsx
import React, { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
      <h3>计数器演示</h3>
      <p>计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <button onClick={() => setCount(count - 1)}>减少</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}
mdx
import { Counter } from '../components/Counter';

# 交互示例

试试这个计数器:

<Counter />

计数器维护状态并响应用户交互。

标签组件

构建用于多个示例的标签组件:

tsx
// components/Tabs.tsx
import React, { useState } from 'react';

interface TabsProps {
  children: React.ReactElement[];
}

interface TabProps {
  label: string;
  children: React.ReactNode;
}

export function Tabs({ children }: TabsProps) {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div className="tabs-container">
      <div className="tab-buttons">
        {React.Children.map(children, (child, index) => (
          <button
            key={index}
            onClick={() => setActiveTab(index)}
            className={activeTab === index ? 'active' : ''}
          >
            {child.props.label}
          </button>
        ))}
      </div>
      <div className="tab-content">
        {children[activeTab]}
      </div>
    </div>
  );
}

export function Tab({ children }: TabProps) {
  return <div>{children}</div>;
}
mdx
import { Tabs, Tab } from '../components/Tabs';

# 安装

<Tabs>
  <Tab label="npm">
    ```bash
    npm install rspress
    ```
  </Tab>
  <Tab label="yarn">
    ```bash
    yarn add rspress
    ```
  </Tab>
  <Tab label="pnpm">
    ```bash
    pnpm add rspress
    ```
  </Tab>
</Tabs>

内置组件

提示框

Rspress 提供内置的提示组件:

mdx
::: tip
这是给用户的有用提示。
:::

::: info
这提供了额外的信息。
:::

::: warning
这警告用户潜在的问题。
:::

::: danger
这向用户警示关键信息。
:::

代码块

带语法高亮的增强代码块:

mdx
```javascript title="app.js" {2,4-6}
function greet(name) {
  console.log(`你好, ${name}!`); // 高亮

  // 这些行被高亮
  if (!name) {
    return '你好, 世界!';
  }

  return `你好, ${name}!`;
}
```

高级模式

条件渲染

有条件地显示内容:

mdx
export const isProduction = process.env.NODE_ENV === 'production';

# 文档

{isProduction ? (
  <div>检测到生产环境。</div>
) : (
  <div>开发环境 - 某些功能可能表现不同。</div>
)}

循环数据

从数组生成内容:

mdx
export const features = [
  { name: '快速', icon: '⚡' },
  { name: '简单', icon: '✨' },
  { name: '强大', icon: '💪' },
];

# 特性

<div style={{ display: 'flex', gap: '20px' }}>
  {features.map(feature => (
    <div key={feature.name} style={{ textAlign: 'center' }}>
      <div style={{ fontSize: '3em' }}>{feature.icon}</div>
      <div>{feature.name}</div>
    </div>
  ))}
</div>

组件组合

组合多个组件:

mdx
import { Card } from '../components/Card';
import { Button } from '../components/Button';
import { Badge } from '../components/Badge';

# API 概览

<Card>
  <Card.Header>
    <h3>用户 API <Badge>v2.0</Badge></h3>
  </Card.Header>
  <Card.Body>
    使用我们的 REST API 创建和管理用户帐户。
  </Card.Body>
  <Card.Footer>
    <Button href="/api/users">查看文档</Button>
  </Card.Footer>
</Card>

代码演示场

实时代码编辑器

创建交互式代码演示场:

tsx
// components/CodePlayground.tsx
import React, { useState } from 'react';

export function CodePlayground({ initialCode }: { initialCode: string }) {
  const [code, setCode] = useState(initialCode);
  const [output, setOutput] = useState('');

  const runCode = () => {
    try {
      // 警告: 在生产中使用 eval 不安全
      // 这仅用于演示
      const result = eval(code);
      setOutput(String(result));
    } catch (error) {
      setOutput(`错误: ${error.message}`);
    }
  };

  return (
    <div className="code-playground">
      <textarea
        value={code}
        onChange={(e) => setCode(e.target.value)}
        rows={10}
        style={{ width: '100%', fontFamily: 'monospace' }}
      />
      <button onClick={runCode}>运行代码</button>
      <div className="output">
        <strong>输出:</strong>
        <pre>{output}</pre>
      </div>
    </div>
  );
}

TypeScript 支持

类型化组件

使用 TypeScript 实现类型安全:

tsx
// components/Card.tsx
import React from 'react';

interface CardProps {
  title: string;
  description: string;
  variant?: 'default' | 'highlighted';
  children?: React.ReactNode;
}

export function Card({
  title,
  description,
  variant = 'default',
  children
}: CardProps) {
  return (
    <div className={`card card-${variant}`}>
      <h3>{title}</h3>
      <p>{description}</p>
      {children}
    </div>
  );
}

样式组件

内联样式

mdx
<div style={{
  padding: '20px',
  background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
  color: 'white',
  borderRadius: '8px',
}}>
  样式化内容
</div>

CSS Modules

tsx
// components/Card.module.css
.card {
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  margin: 20px 0;
}

.cardHighlighted {
  border-color: #007bff;
  background: #f0f8ff;
}
tsx
// components/Card.tsx
import styles from './Card.module.css';

export function Card({ highlighted, children }) {
  return (
    <div className={highlighted ? styles.cardHighlighted : styles.card}>
      {children}
    </div>
  );
}

最佳实践

组件组织

components/
├── common/
│   ├── Button.tsx
│   ├── Card.tsx
│   └── Badge.tsx
├── interactive/
│   ├── CodePlayground.tsx
│   ├── ApiDemo.tsx
│   └── Counter.tsx
└── layout/
    ├── Tabs.tsx
    ├── Grid.tsx
    └── Columns.tsx

性能

  1. 懒加载: 按需加载重型组件
tsx
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

export function Demo() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}
  1. 记忆化: 防止不必要的重新渲染
tsx
import { memo } from 'react';

export const Card = memo(function Card({ title, content }) {
  return (
    <div>
      <h3>{title}</h3>
      <p>{content}</p>
    </div>
  );
});

故障排除

常见问题

  1. 导入错误
Error: Cannot find module '../components/Button'

解决方案: 检查文件路径和扩展名

  1. JSX 未渲染
mdx
<!-- 不起作用 - 需要是有效的 JSX -->
<Button>点击我<Button>

<!-- 正确 -->
<Button>点击我</Button>

下一步


最佳实践

  • 保持组件小而专注
  • 使用 TypeScript 以获得更好的开发体验
  • 彻底测试交互组件
  • 清楚地记录组件属性

安全性

  • 永远不要在生产中对用户输入使用 eval()
  • 清理任何动态内容
  • 小心使用 dangerouslySetInnerHTML
  • 验证所有外部数据