MDX vs MDX 2.0:版本升级API全变?这份速查手册救急
刚把项目从 MDX 1.x 迁到 2.x,是不是觉得代码里的 import 和 export 突然就不好使了?或者文档里写着 mdx:format,结果编译器直接报错?版本升级后 API 全变了,这种断崖式的体验升级确实让人头大。别慌,这份 MDX 速查手册不是那种泛泛而谈的教程,而是专门针对那些被新版 API 卡住、急需恢复生产力的开发者准备的实战指南。
很多老手还停留在“MDX 就是带 JSX 的 Markdown”这个认知上,但 MDX 2.0 之后,它已经演变成了一个完整的、基于 AST(抽象语法树)的编译管线。如果你还在用旧版的 mdx-loader 配合 Webpack 4 的那套写法,现在直接照搬只会得到一堆解析错误。本文不聊虚的,直接拆解 MDX 1.x 与 MDX 2.x 的核心差异,通过代码对比帮你理清思路,让你明白为什么官方要这么改,以及如何在项目中平滑过渡。
定位重构:从“插件”到“编译器”
要理解 API 为什么变,得先搞清楚 MDX 这两个版本的底层定位差异。
在 MDX 1.x 时代,MDX 更像是一个 Markdown 的增强插件。它的核心逻辑是:先解析 Markdown,把代码块识别出来,然后交给 Babel 处理 JSX。这种架构简单直接,但在处理复杂的嵌套结构、自定义组件解析以及类型推导时,显得力不从心。它依赖宿主构建工具(如 Webpack)的能力,导致配置耦合度极高,一旦换构建工具,配置就要推倒重来。
MDX 2.x 则是一次彻底的架构重构。官方将其定义为“基于 AST 的超集”。这意味着 MDX 不再仅仅是一个“解析器”,而是一个独立的编译器。它拥有自己的解析器(Parser)、编译器(Compiler)和运行时(Runtime)。
- MDX 1.x:Markdown Parser -> Babel JSX Transform -> JavaScript。依赖 Babel 生态,配置分散。
- MDX 2.x:Markdown + JSX Parser -> MDX AST -> Compiler (Babel/TS) -> JavaScript/TypeScript。独立管线,支持 ESM/CJS,原生支持 TypeScript。
这种定位的变化,直接导致了 API 的断裂。1.x 版本中,你主要配置的是 Webpack 的 loader 选项;而在 2.x 版本中,你需要关注的是 @mdx-js/mdx 包的 compile 函数或 MDXRemote 组件的 props。对于前端开发者来说,这意味着你需要从“配置 Loader”思维转变为“配置 Compiler”思维。
核心差异:API 与配置项对比
这是最容易让人踩坑的地方。很多博客文章只告诉你“用这个”,却不告诉你“为什么不能用那个”。下面这张表格汇总了从 1.x 迁移到 2.x 时,最核心的 API 变更点。建议收藏,方便对照修改代码。
| 特性/维度 | MDX 1.x (旧版) | MDX 2.x (新版) | 变更影响与备注 |
|---|---|---|---|
| 核心包 | @mdx-js/loader |
@mdx-js/mdx, @mdx-js/react |
1.x 依赖 Webpack loader,2.x 解耦了构建工具 |
| 运行时 | 内置在 loader 中 | MDXRemote 或编译后的组件 |
2.x 需要显式引入运行时,支持 ESM 动态导入 |
| 配置方式 | module.rules (Webpack) |
compile() options 或 MDXRemote props |
2.x 配置更集中,支持 remarkPlugins 和 rehypePlugins |
| 组件注入 | import 语句在文件中 |
components prop 或 providerComponents |
2.x 通过 Context 传递组件,避免全局污染 |
| 类型支持 | 需额外配置 TypeScript | 原生支持 .tsx 转换 | 2.x 编译器内置 TS 支持,无需额外 babel preset |
| URL 导出 | 支持 export const url = ... |
支持,但需配合 export 语法 |
2.x 对顶层导出有更严格的 AST 处理 |
| 错误处理 | 报错位置模糊 | 精确到 AST 节点 | 2.x 提供了更好的 DevTools 支持,调试体验大幅提升 |
特别注意最后一行。在 1.x 中,如果 JSX 语法错误,Webpack 的报错往往指向编译后的 JS 文件,你需要反推源码位置。而在 2.x 中,由于 MDX 编译器保留了完整的源码映射和 AST 信息,报错会直接指向 MDX 文件的具体行数和列数,这对大型文档站点的维护是巨大的福音。
代码写法对比:从 Loader 到 Compiler
光看表格可能还是抽象,我们直接上代码。假设我们有一个简单的 Post.mdx 文件,里面包含一个自定义组件 <Highlight> 和一段 Markdown 文本。
MDX 1.x 写法 (Webpack 4/5)
在 1.x 中,我们的重心在 webpack.config.js 上。
// webpack.config.js (MDX 1.x 风格)
const path = require('path');module.exports = {entry: './src/index.js',output: {path: path.resolve(__dirname, 'dist'),filename: 'bundle.js'},module: {rules: [{test: /\.mdx?$/,use: ['babel-loader',{loader: '@mdx-js/loader',options: {// 1.x 的配置项,注意这里没有 components propproviderImportSource: '@mdx-js/react',}}]},{test: /\.jsx?$/,exclude: /node_modules/,use: 'babel-loader'}]}
};
// Post.mdx (MDX 1.x)
import Highlight from '../components/Highlight';# 标题这是一段文本,包含 <Highlight color="red">高亮</Highlight> 内容。export const meta = {title: 'MDX 1.x 示例'
};
注意:在 1.x 中,如果你想在 MDX 文件中动态使用外部组件,通常需要在文件顶部 import。但这种方式会导致组件被打包进每个 MDX 文件,造成包体积膨胀。且 import 是静态的,无法在运行时动态切换主题组件。
MDX 2.x 写法 (Next.js / Vite / 通用)
在 2.x 中,Webpack 配置变得极简,甚至不需要专门的 MDX loader(如果使用 Next.js,只需配置 next-mdx-remote)。核心逻辑转移到了运行时。
// pages/post.js (Next.js 13+ / React 18)
import { MDXRemote } from '@mdx-js/react';
import { getMDXComponent } from '@next/mdx';
import { loadMdx } from '../utils/loadMdx'; // 自定义加载器,通常调用 @mdx-js/mdx 的 compileexport default function Post({ components, mdxSource }) {return (<article>{/* components 是核心:通过 Context 注入,无需 import */}<MDXRemote components={components} {...mdxSource} /></article>);
}// 假设 mdxSource 是从文件读取并编译后的对象
// 如果动态获取,通常使用 next-mdx-remote 的 getMDXComponent
// Post.mdx (MDX 2.x)
# 标题这是一段文本,包含 <Highlight color="red">高亮</Highlight> 内容。export const meta = {title: 'MDX 2.x 示例'
};
// 注意:不再需要 import Highlight
// Highlight 是通过 props 传入的 components 对象解析的
// utils/loadMdx.js (可选,用于非 Next.js 环境)
import { compile } from '@mdx-js/mdx';
import fs from 'fs';export async function loadMdx(filePath) {const source = fs.readFileSync(filePath, 'utf-8');const compiled = await compile(source, {// 2.x 的编译选项,这里可以配置 remark/rehype 插件remarkPlugins: [require('remark-toc')],rehypePlugins: [require('rehype-slug')],});return new Function('components', 'mdxOptions', compiled)({}, // components 将在运行时传入{} // mdxOptions);
}
关键差异解析:
- 去除了
import:在 2.x 中,MDX 文件内部不再依赖import语句来引入 React 组件。这是通过MDXRemote的components属性实现的。编译器在编译时会将<Highlight>标记为需要从上下文中获取的组件。 - 编译与渲染分离:2.x 将“编译 MDX 字符串为代码”和“渲染代码为 DOM”彻底分离。你可以预先编译(SSG),也可以在客户端编译(CSR)。
- TypeScript 原生支持:如果文件扩展名是
.mdx,但内容涉及 TS 类型注解,2.x 编译器会自动处理,无需像 1.x 那样配置复杂的 Babel 预设。
进阶技巧与避坑指南
很多开发者在迁移过程中遇到的报错,往往不是 API 用法问题,而是思维惯性导致的坑。
坑一:ESM 与 CJS 的冲突
MDX 2.x 原生输出 ESM(ECMAScript Modules)。如果你的项目是 CommonJS(CJS)环境(比如某些 Node.js 服务端渲染场景),直接 require 编译后的 MDX 组件会报错 SyntaxError: Cannot use import statement outside a module。
解决方案:
使用 next-mdx-remote 或类似库,它们内部处理了 ESM 到 CJS 的转换。或者,在 Webpack/Vite 中配置 resolve 和 module 规则,确保能正确解析 ESM 格式的 MDX 组件。
坑二:components 的作用域陷阱
在 MDX 2.x 中,components 是通过 React Context 传递的。这意味着,如果你在 MDX 文件内部定义了一个本地组件,它不会覆盖外部的 components 传入值,除非你显式地解构并重新传入。
// 错误示例:试图在 MDX 内部覆盖全局组件
function LocalHeading() {return <h1 className="local-style">Local</h1>;
}# 标题
// 这里使用的 h1 仍然是外部传入的 components.h1,而不是 LocalHeading
正确做法:
如果需要在特定 MDX 文件中定制组件,应该在加载该 MDX 时,动态合并 components 对象。
// 在渲染层
const specificComponents = {h1: LocalHeading,...globalComponents
};
<MDXRemote components={specificComponents} />
坑三:图片与静态资源的相对路径
在 1.x 中,Markdown 里的图片路径通常是相对于文件位置的。但在 2.x 中,由于编译过程是解耦的,图片路径的处理取决于你的 remark 插件配置。
官方推荐在 remark 插件中处理图片路径,将其转换为绝对 URL 或打包后的资源路径。如果直接使用相对路径 ./images/pic.png,在客户端运行时可能会因为路径基准点变化(从文件系统变为浏览器 URL)而失效。
建议:
使用 remark-images 插件,并在插件配置中将 src 属性重写为经过打包工具处理后的路径。
适用场景与选型建议
了解了差异和坑,我们来看什么时候该用哪个,或者如何选型。
1. 纯静态文档站 (Gatsby, Astro, Docusaurus)
建议:直接使用 MDX 2.x 这些框架已经深度集成了 MDX 2.x 的编译器。你不需要关心底层配置,只需专注于内容编写。MDX 2.x 的性能优化(如部分预编译)能带来更好的首屏加载速度。
2. 大型博客系统 (Next.js)
建议:Next.js 13+ + MDX 2.x
这是目前最主流的组合。利用 next-mdx-remote 库,可以实现服务端预编译(SSG)和客户端动态渲染(CSR)的混合模式。
- SEO 友好:预编译确保 HTML 中包含完整内容。
- 动态主题:通过
componentsprop 轻松实现暗黑模式切换。 - 注意:确保你的
next.config.js中正确配置了transpilePackages,包括@mdx-js/mdx和@mdx-js/react。
3. 遗留 Webpack 4 项目
建议:谨慎迁移,或考虑封装 MDX 2.x 对 Webpack 4 的支持并不友好,因为它依赖较新的 ESM 特性。
- 如果项目允许,升级 Webpack 5。
- 如果无法升级 Webpack,继续使用 MDX 1.x。虽然它已停止维护,但在 Webpack 4 环境下依然稳定。不要强行迁移,API 的断裂在 Webpack 4 中很难平滑过渡。
4. 服务端渲染 (SSR) 非 Next.js 框架 (Nuxt, Remix)
建议:使用 @mdx-js/mdx 的 compile API
在 Nuxt 或 Remix 中,你可能需要手动集成。
- 在
server端使用@mdx-js/mdx的compile函数将 MDX 字符串编译为代码。 - 将编译后的代码通过 props 传递给客户端组件。
- 客户端使用
MDXRemote渲染。 这种模式更灵活,但需要你自己处理缓存和序列化问题。
总结与互动
MDX 2.x 的 API 变更看似复杂,实则是为了追求更解耦、更灵活、更类型安全的架构。从“Loader 配置”到“Compiler 控制”,这一转变要求开发者具备更清晰的模块边界意识。
对于中小团队而言,不要为了技术而技术。如果你的项目还在 Webpack 4,且没有强烈的 TS 或 ESM 需求,MDX 1.x 依然是可用的。但如果你正在启动新项目,或者使用 Next.js/Astro 等现代框架,MDX 2.x 是必经之路。
你更常用哪种写法?评论区交流
在实际项目中,你是倾向于在 MDX 文件中直接 import 组件(1.x 风格,虽然 2.x 不推荐但有时为了代码局部性会这么做),还是严格遵循 2.x 的 Context 注入模式?或者你遇到了什么奇怪的编译错误?欢迎在评论区分享你的踩坑经历和解决思路,我们一起交流。