MDX:嵌入组件与表达式
本教程共 56 篇 · 第 18 篇 · 更新于 2026-08-07 · 约 9 分钟阅读
本节目标:学会用 MDX 让 Markdown 文章里能写变量、写表达式、直接放组件,让内容页不再只是纯文字。
上一章讲的 Markdown,擅长写文字,但有个局限:它只能写内容,不能写逻辑,也不能直接塞组件进去。MDX 就是来补这个短板的。
一句话概括:MDX 让你能在 Markdown 里写 JSX。JSX 是给界面写结构的一种语法,你可以把它理解成「能在文档里直接写组件、写变量」的能力。于是你的文章既能有 Markdown 的清爽,又能有组件的灵活。
先装上 MDX 集成
MDX 不是 Astro 默认开启的,得装一个官方「集成」(integration,就是给 Astro 加功能的插件包)。装它用一行命令:
npx astro add mdx
这个命令会自动把 @astrojs/mdx 装好,并写进配置文件。如果你更喜欢手动装,也行:
npm install @astrojs/mdx
装完再去 astro.config.mjs 里登记一下:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
export default defineConfig({
integrations: [mdx()],
});
记牢一件事:.mdx 文件用的是 MDX 语法,不是 Astro 那种像 HTML 的写法。在 .mdx 里写标签、写表达式,遵循的是 JSX 规矩。
Tip用 VS Code 写 MDX,建议装官方的 MDX 扩展,写起来有高亮和提示,不容易写错。
在 MDX 里导出变量
MDX 支持用 export 语句定义变量,这个变量可以在文件里直接用,也能被导入它的组件拿到。
export const title = '我的第一篇 MDX 帖子'
# {title}
上面 {title} 就是 JSX 表达式的写法,花括号里放变量名,渲染时换成它的值。于是标题会显示成「我的第一篇 MDX 帖子」。
别的组件导入这个文件时,也能用这些导出的变量:
---
const matches = import.meta.glob('../posts/*.mdx', { eager: true });
const posts = Object.values(matches);
---
{posts.map(post => <p>{post.title}</p>)}
这里 post.title 拿到的,就是 MDX 里 export const title 的值。
用 frontmatter 当变量
MDX 集成默认也支持 frontmatter,写法和普通 Markdown 一样。这些属性在模板里通过 frontmatter.xxx 访问。
---
title: '我的第一篇 MDX 帖子'
author: 'Houston'
---
# {frontmatter.title}
作者:{frontmatter.author}
这个能力和前面「导出变量」不冲突,你按习惯选一种就行。很多人的习惯是:文章元信息(标题、作者)放 frontmatter,需要动态计算的才用 export。
在 MDX 里用组件
装了 MDX 集成后,你可以把 Astro 组件,甚至 React、Vue 这类 UI 框架组件,直接 import 进 .mdx 使用,跟在 .astro 文件里用组件没两样。
---
title: 我的第一篇帖子
---
import ReactCounter from '../components/ReactCounter.jsx';
我刚开了个 Astro 博客!
下面这个计数器组件,在 MDX 里也能跑:
<ReactCounter client:load />
注意那行 client:load。它是「客户端指令」(client directive),告诉 Astro:这个 UI 框架组件需要在浏览器里「水合」(hydration,让静态组件变成可交互)后用。client:load 表示页面一加载就水合。忘了写指令,组件只会渲染个静态样子,点了没反应。
把 HTML 元素换成自定义组件
MDX 有个很妙的能力:你可以让某个 HTML 标签,自动用上你自己的组件。比如你想给所有引用块 <blockquote> 加特殊样式,不用每处手写组件,定义一次映射就好。
先写一个 Astro 组件:
---
const props = Astro.props;
---
<blockquote {...props} class="bg-blue-50 p-4">
<span class="text-4xl text-blue-600 mb-2">“</span>
<slot /> <!-- 引用内容从这里进来 -->
</blockquote>
再在 MDX 里导入它,并导出一个 components 映射:
import Blockquote from '../components/Blockquote.astro';
export const components = { blockquote: Blockquote }
> 这句话会自动用上自定义的 Blockquote 样式
这样写普通 Markdown 的 > 引用语法,实际渲染的却是你的 Blockquote 组件。MDX 支持把 h1、h2、blockquote 等一大堆 HTML 元素都替换掉,想知道全清单可以去 MDX 官网查。
给导入的 MDX 传组件
当你用 <Content /> 渲染一篇导入的 MDX 时,可以通过 components 这个属性,把自定义组件传进去。
---
import { Content, components } from '../content.mdx';
import Heading from '../Heading.astro';
---
<!-- 给 # 语法换上自定义 h1,同时带上 MDX 文件里自己定义的组件 -->
<Content components={{ ...components, h1: Heading }} />
如果 MDX 是「内容集合」里的一项,渲染方式类似,只是要先从 astro:content 取 <Content />:
---
import { getEntry, render } from 'astro:content';
import CustomHeading from '../../components/CustomHeading.astro';
const entry = await getEntry('blog', 'post-1');
const { Content } = await render(entry);
---
<Content components={{ h1: CustomHeading }} />
components 对象把 HTML 元素名(如 h1)映射到你的组件。展开运算符 ...components 是把 MDX 文件自身导出的组件也一并带进来,省得漏掉。
MDX 和 Markdown 配置的关系
MDX 集成默认会继承你 markdown 里的配置,比如语法高亮、处理器设置。也就是说你给 Markdown 配的主题,MDX 一般也跟着用。
想单独给 .mdx 文件配不同的选项,在 mdx() 里写就行:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
export default defineConfig({
integrations: [
mdx({
syntaxHighlight: 'shiki',
shikiConfig: { theme: 'dracula' },
}),
],
});
如果哪天你想让 MDX 完全不继承 Markdown 的配置,把 extendMarkdownConfig 设成 false 即可。
Note关于语法高亮(Shiki、Prism)的细节,下一章专门讲。这里只要知道 MDX 能沿用同一套高亮配置就好。
MDX 文件也能直接当页面
和 Markdown 一样,把 .mdx 文件放进 src/pages/ 目录,Astro 会自动把它变成一个网页。文件 src/pages/posts/hello.mdx 对应的网址就是 /posts/hello/。
---
title: 我的 MDX 页面
---
import { Counter } from '../components/Counter.jsx';
# 欢迎
下面这个组件直接长在页面里:
<Counter client:load />
这种方式适合「内容里偶尔要嵌点交互」的单个页面。它和「放进内容集合统一管理」是两条路,按你内容规模选。
把 MDX 放进内容集合
如果你有一批 MDX 文章想集中管理,在内容集合的加载器里把 .mdx 也纳进来就好。glob 的匹配规则同时写 md 和 mdx:
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
}),
});
export const collections = { blog };
这样 Markdown 和 MDX 混在同一个集合里也没问题。渲染时一律用 render() 拿 <Content />,上一章(第 19 章)讲的高亮配置对 MDX 同样生效。
NoteMDX 里如果用了客户端指令(如
client:load),那个组件在集合页面里依然会正常水合。内容集合不改变 MDX 的交互能力。
什么时候用 MDX,什么时候用纯 Markdown
一句话:纯文字内容用 Markdown,要嵌交互或动态逻辑才上 MDX。
Markdown 更轻、渲染更稳、写起来没心智负担。一篇普通博客、一份文档,Markdown 足够。只有当你确实需要在文章里放组件、写表达式、用变量时,MDX 的价值才显出来。
别为了「看起来高级」就全站用 MDX。文件越多、越杂,构建越慢,排查问题也越麻烦。按需使用最踏实。
Tip拿不准时,默认写
.md。真碰到「这段内容想变成组件」的需求,再改成.mdx,成本很低。
小结
MDX 的本质,是给 Markdown 加了「写变量、写表达式、放组件」的能力。装上 @astrojs/mdx 集成就能用;export 和 frontmatter 都能当变量;组件直接 import 进来用;用 components 映射还能把普通 HTML 标签换成你的组件。
MDX 文件既能直接放 pages/ 当页面,也能进内容集合统一管。它特别适合写「半文章半应用」的内容页,比如带可交互 Demo 的教程。下一章先聊语法高亮,第 20、21 章再回到内容集合。