MDX 完整语法指南

全面介绍 MDX 语法,包括基础用法、组件导入、JSX 表达式以及在 Astro 中的最佳实践

MDX 完整语法指南

MDX 是 Markdown 的超集,它允许你在 Markdown 文档中直接使用 JSX 组件。这意味着你可以在写作时享受 Markdown 的简洁语法,同时又能利用 React/Preact/Vue 等框架组件的强大功能。本指南将全面介绍 MDX 的各种语法和在 Astro 中的使用方法。


什么是 MDX

MDX 简介

MDX(Markdown + JSX)是一种将 Markdown 和 JSX 结合的文件格式。它由 Unified 团队开发和维护,已经成为现代文档和博客系统的流行选择。

MDX 的核心理念是:

让内容创作者能够在熟悉的 Markdown 环境中使用组件化的交互式内容。

MDX 与 Markdown 的区别

特性MarkdownMDX
基础文本格式
代码块语法高亮
表格支持
导入组件
使用 JSX
JavaScript 表达式
自定义组件替换
交互式内容

MDX 的优势

  1. 组件复用:可以创建可复用的 UI 组件,在多篇文章中使用
  2. 交互性:支持创建交互式的演示、图表、代码示例等
  3. 类型安全:配合 TypeScript 可以获得完整的类型检查
  4. 灵活性:可以混合使用 Markdown 和 JSX,根据需要选择最合适的方式
  5. 生态系统:可以使用任何 React/Preact/Vue 组件库

基础语法

在 MDX 中使用 Markdown

MDX 完全兼容标准 Markdown 语法,你可以像往常一样编写:

# 这是标题
这是一个段落,包含 **粗体**_斜体_ 文本。
- 列表项 1
- 列表项 2
- 列表项 3
> 这是一段引用

所有标准的 Markdown 语法在 MDX 中都能正常工作。

在 MDX 中使用 JSX

MDX 的核心特性是可以直接在文档中使用 JSX 语法:

{
/* 这是 JSX 注释 */
}
;<div
style={{ padding: '1rem', backgroundColor: '#f0f0f0', borderRadius: '8px' }}
>
<h3>这是一个 JSX 块</h3>
<p>你可以在这里使用任何有效的 JSX 语法。</p>
</div>

注意事项:

  • JSX 中的 class 属性需要写成 className
  • 内联样式需要使用对象语法:style={{ color: 'red' }}
  • JSX 注释使用 {/* 注释内容 */} 格式

导入和使用组件

MDX 允许你在文件顶部导入组件,然后在文档中使用:

import MyButton from '../components/MyButton.astro';
import { Card, CardHeader, CardBody } from '../components/Card';
# 我的文章
这是一些普通的 Markdown 文本。
<MyButton>点击我</MyButton>
<Card>
<CardHeader>卡片标题</CardHeader>
<CardBody>
这是卡片的内容,可以包含任何 Markdown 或 JSX。
</CardBody>
</Card>

内联表达式

你可以在 MDX 中使用花括号 {} 来嵌入 JavaScript 表达式:

export const name = "MDX";
export const year = 2024;
# 欢迎使用 {name}
当前年份是 {year},{name} 已经发展了 {year - 2018} 年。
今天的日期是:{new Date().toLocaleDateString('zh-CN')}

支持的表达式类型:

  • 变量引用:{variableName}
  • 数学运算:{1 + 2 + 3}
  • 字符串操作:{name.toUpperCase()}
  • 三元表达式:{condition ? 'yes' : 'no'}
  • 函数调用:{formatDate(date)}
  • 数组方法:{items.map(item => item.name).join(', ')}

高级用法

导出变量和函数

你可以在 MDX 文件中导出变量、函数和组件:

export const metadata = {
author: "张三",
readTime: "5 分钟"
};
export function Highlight({ children, color = 'yellow' }) {
return (
<span style={{ backgroundColor: color, padding: '0.2em 0.4em', borderRadius: '4px' }}>
{children}
</span>
);
}
# 文章标题
作者:{metadata.author} | 阅读时间:{metadata.readTime}
这是一段包含 <Highlight>高亮文本</Highlight> 的内容。
你也可以使用不同的颜色:<Highlight color="#90EE90">绿色高亮</Highlight>

自定义组件映射

MDX 允许你用自定义组件替换默认的 HTML 元素。这在 Astro 中通过 components prop 实现:

// 在布局文件中
import { Content } from './my-post.mdx'
const components = {
// 替换默认的 h1 元素
h1: (props) => <h1 className="custom-heading" {...props} />,
// 替换默认的 a 元素
a: (props) => {
const isExternal = props.href?.startsWith('http')
return (
<a
{...props}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
/>
)
},
// 替换默认的 code 元素
code: (props) => <code className="inline-code" {...props} />,
// 替换默认的 pre 元素(代码块)
pre: (props) => <pre className="code-block" {...props} />,
}
;<Content components={components} />

可替换的元素列表:

元素Markdown 语法说明
h1 - h6#######标题
p普通段落段落
a[text](url)链接
img![alt](src)图片
blockquote>引用块
ul / ol-1.列表
li列表项列表项
code`code`行内代码
pre```代码块
table| 语法表格
hr---分割线

布局组件

你可以为 MDX 内容指定布局组件:

// my-post.mdx
export { default as Layout } from '../layouts/PostLayout.astro';
# 文章标题
文章内容...

或者在 Astro 中通过配置指定默认布局:

astro.config.mjs
import { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx'
export default defineConfig({
integrations: [
mdx({
// MDX 配置选项
}),
],
})

传递 Props

组件可以接收和使用 props:

export function Alert({ type = 'info', title, children }) {
const colors = {
info: { bg: '#e3f2fd', border: '#2196f3' },
warning: { bg: '#fff3e0', border: '#ff9800' },
error: { bg: '#ffebee', border: '#f44336' },
success: { bg: '#e8f5e9', border: '#4caf50' }
};
const style = {
padding: '1rem',
borderLeft: `4px solid ${colors[type].border}`,
backgroundColor: colors[type].bg,
borderRadius: '4px',
marginBottom: '1rem'
};
return (
<div style={style}>
{title && <strong>{title}</strong>}
<div>{children}</div>
</div>
);
}
<Alert type="info" title="提示">
这是一条信息提示。
</Alert>
<Alert type="warning" title="警告">
请注意这个重要事项!
</Alert>
<Alert type="error" title="错误">
操作失败,请重试。
</Alert>
<Alert type="success" title="成功">
操作已成功完成!
</Alert>

在 Astro 中使用 MDX

配置说明

要在 Astro 项目中使用 MDX,首先需要安装和配置 MDX 集成:

Terminal window
# 安装 MDX 集成
npm install @astrojs/mdx
# 或使用 pnpm
pnpm add @astrojs/mdx
# 或使用 yarn
yarn add @astrojs/mdx

然后在 astro.config.mjs 中添加配置:

astro.config.mjs
import { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx'
export default defineConfig({
integrations: [mdx()],
markdown: {
// Markdown 配置也会应用到 MDX
shikiConfig: {
theme: 'github-dark',
wrap: true,
},
},
})

MDX 配置选项

astro.config.mjs
import { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
export default defineConfig({
integrations: [
mdx({
// 语法扩展
syntaxHighlight: 'shiki', // 或 'prism'
// Remark 插件(处理 Markdown)
remarkPlugins: [remarkGfm],
// Rehype 插件(处理 HTML)
rehypePlugins: [rehypeSlug],
// 是否使用 Astro 的 Markdown 配置
extendMarkdownConfig: true,
// 优化选项
optimize: true,
}),
],
})

在 Astro 组件中使用 MDX

src/pages/post/[slug].astro
---
import { getCollection } from 'astro:content'
import PostLayout from '../../layouts/PostLayout.astro'
export async function getStaticPaths() {
const posts = await getCollection('posts')
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}))
}
const { post } = Astro.props
const { Content } = await post.render()
---
<PostLayout frontmatter={post.data}>
<Content />
</PostLayout>

导入 Astro 组件到 MDX

你可以在 MDX 文件中导入和使用 Astro 组件:

---
title: "使用 Astro 组件的文章"
---
import Button from '../../components/common/Button.astro';
import Container from '../../components/common/Container.astro';
# 我的文章
<Container>
这是容器内的内容。
<Button>点击这里</Button>
</Container>

注意:Astro 组件在 MDX 中是静态渲染的,不支持客户端交互。如果需要交互性,请使用带有 client:* 指令的框架组件。

使用客户端组件

对于需要客户端交互的组件,可以使用 React/Vue/Svelte 等框架组件:

import Counter from '../../components/Counter.jsx';
# 交互式计数器示例
下面是一个可交互的计数器组件:
<Counter client:load />
使用 `client:load` 指令使组件在页面加载时立即水合。

客户端指令说明:

指令说明
client:load页面加载时立即水合
client:idle页面空闲时水合
client:visible组件进入视口时水合
client:media满足媒体查询条件时水合
client:only仅在客户端渲染,跳过 SSR

最佳实践

1. 文件组织

推荐的文件组织结构:

src/
├── content/
│ └── posts/
│ ├── my-post.mdx
│ └── another-post.mdx
├── components/
│ ├── mdx/ # MDX 专用组件
│ │ ├── Alert.astro
│ │ ├── CodeDemo.astro
│ │ └── Tabs.astro
│ └── common/ # 通用组件
│ ├── Button.astro
│ └── Card.astro
└── layouts/
└── PostLayout.astro

2. 组件设计原则

  • 保持简单:MDX 组件应该专注于内容展示
  • 可复用性:设计通用的组件,避免过度定制
  • 无障碍性:确保组件符合 WCAG 标准
  • 性能优先:避免在 MDX 中使用过重的组件

3. 性能优化

// 使用动态导入减少初始包大小
const HeavyComponent = await import('../components/HeavyComponent')
// 使用 client:visible 延迟加载
;<HeavyComponent client:visible />

4. 类型安全

为 MDX 组件添加 TypeScript 类型:

components/Alert.tsx
interface AlertProps {
type?: 'info' | 'warning' | 'error' | 'success'
title?: string
children: React.ReactNode
}
export function Alert({ type = 'info', title, children }: AlertProps) {
// 组件实现
}

常见问题

Q: MDX 文件和 Markdown 文件有什么区别?

A: MDX 文件(.mdx)支持导入组件和使用 JSX 语法,而 Markdown 文件(.md)只支持标准 Markdown 语法。在 Astro 中,两者都可以用于内容集合,但 MDX 提供更多的灵活性。

Q: 为什么我的组件没有正确渲染?

A: 常见原因包括:

  1. 组件路径错误
  2. 忘记添加 client:* 指令(对于交互式组件)
  3. JSX 语法错误(如使用 class 而不是 className
  4. 组件没有正确导出

Q: 如何在 MDX 中使用条件渲染?

A: 使用 JSX 的条件渲染语法:

export const showAdvanced = true;
# 文档
基础内容...
{showAdvanced && (
<div>
## 高级内容
这部分只在 showAdvanced 为 true 时显示。
</div>
)}

Q: MDX 支持 frontmatter 吗?

A: 是的,MDX 完全支持 YAML frontmatter:

---
title: '我的文章'
description: '文章描述'
pubDate: 2024-01-15
---

Q: 如何处理 MDX 中的转义字符?

A: 在 MDX 中,某些字符需要转义:

  • {} 需要写成 \{\} 或使用 {'{'}
  • < 在某些情况下需要写成 &lt;
// 显示花括号
这是一个对象:\{key: value\}
// 或者
这是一个对象:{'{'}key: value{'}'}

Q: 如何在 MDX 中添加自定义样式?

A: 有几种方式:

// 1. 内联样式
<div style={{ color: 'red', fontSize: '1.2em' }}>
红色文本
</div>
// 2. CSS 类名
<div className="custom-class">
使用 CSS 类
</div>
// 3. 导入 CSS 模块
import styles from './styles.module.css';
<div className={styles.container}>
使用 CSS 模块
</div>

实用示例

创建可折叠内容

export function Collapsible({ title, children }) {
return (
<details style={{ marginBottom: '1rem' }}>
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>
{title}
</summary>
<div style={{ paddingTop: '0.5rem' }}>{children}</div>
</details>
)
}
;<Collapsible title="点击展开更多内容">
这里是隐藏的内容,点击标题可以展开或收起。 - 支持 Markdown 语法 - 支持嵌套组件
- 支持任意内容
</Collapsible>

创建标签页组件

export function Tabs({ children, labels }) {
return (
<div className="tabs-container">
<div className="tabs-header">
{labels.map((label, index) => (
<button key={index} className="tab-button">
{label}
</button>
))}
</div>
<div className="tabs-content">{children}</div>
</div>
)
}

使用示例:

<Tabs labels={['JavaScript', 'Python', 'Go']}>
{/* JavaScript 代码 */}
<pre>
<code>console.log('Hello, World!');</code>
</pre>
{/* Python 代码 */}
<pre>
<code>print('Hello, World!')</code>
</pre>
{/* Go 代码 */}
<pre>
<code>fmt.Println("Hello, World!")</code>
</pre>
</Tabs>

创建代码对比组件

export function CodeComparison({ before, after, language = 'javascript' }) {
return (
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}
>
<div>
<h4 style={{ color: '#ef4444' }}>❌ 之前</h4>
<pre>
<code className={`language-${language}`}>{before}</code>
</pre>
</div>
<div>
<h4 style={{ color: '#22c55e' }}>✅ 之后</h4>
<pre>
<code className={`language-${language}`}>{after}</code>
</pre>
</div>
</div>
)
}

总结

MDX 是一个强大的工具,它将 Markdown 的简洁性与组件化开发的灵活性完美结合。通过本指南,你已经了解了:

  1. MDX 基础:什么是 MDX,以及它与 Markdown 的区别
  2. 基础语法:如何在 MDX 中使用 JSX、导入组件和使用表达式
  3. 高级用法:自定义组件、布局组件和 props 传递
  4. Astro 集成:如何在 Astro 项目中配置和使用 MDX
  5. 最佳实践:文件组织、性能优化和类型安全
  6. 常见问题:解决使用 MDX 时可能遇到的问题

掌握 MDX 后,你可以创建更加丰富、交互性更强的内容,同时保持 Markdown 的写作体验。


参考资源


本文最后更新于 2024 年 1 月 15 日