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 presetURL 导出 支持 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-styleLocal/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 中包含完整内容。 动态主题:通过 components prop 轻松实现暗黑模式切换。 注意:确保你的 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 注入模式?或者你遇到了什么奇怪的编译错误?欢迎在评论区分享你的踩坑经历和解决思路,我们一起交流。