@mdx-js/mdx、@mdx-js/loader 和 @mdx-js/react 共同构成了 MDX 在现代前端工程中的核心处理链路,但它们各自承担截然不同的职责。@mdx-js/mdx 是纯粹的编译器,负责将 .mdx 文本转换为 JavaScript 代码字符串,不依赖任何特定框架或构建工具。@mdx-js/loader 是 webpack 专用的集成桥梁,它调用编译器并将结果注入模块系统,使开发者能像导入普通 JS 文件一样导入 MDX。@mdx-js/react 则提供了运行时所需的 React 组件(如 MDXProvider)和上下文环境,确保编译后的内容能正确渲染并支持主题定制。理解这三者的边界是构建可维护文档系统或内容驱动应用的关键。
在构建基于内容的 React 应用时,开发者常面对 @mdx-js/mdx、@mdx-js/loader 和 @mdx-js/react 这三个包感到困惑。它们名字相似,却处于构建流水线的不同阶段。选错包会导致构建失败、运行时错误或架构耦合度过高。本文将从工程实践角度,拆解它们的真实职责与协作方式。
这三个包分别对应内容处理流水线的三个独立环节:编译源代码、集成到构建系统、在浏览器中渲染。
@mdx-js/mdx 是纯编译器。
它只关心一件事:把 MDX 文本变成 JavaScript 代码字符串。它不知道 webpack 是什么,也不关心 React 是否存在。你可以把它看作一个黑盒函数:输入字符串,输出字符串。
// 使用 @mdx-js/mdx 进行纯编译
import { compile } from '@mdx-js/mdx';
const mdxText = '# Hello\n\n<Counter />';
async function compileExample() {
const result = await compile(mdxText, {
outputFormat: 'function-body',
development: false
});
console.log(String(result));
// 输出:const {useMDXComponents: _provideComponents} = arguments[0];
// return <h1>{`Hello`}</h1>\n<_provideComponents().Counter />;
}
@mdx-js/loader 是 webpack 适配器。
它的作用是在 webpack 构建过程中拦截 .mdx 文件,调用编译器,并将结果转换为 webpack 能理解的模块。它让 import Page from './page.mdx' 成为可能。
// webpack.config.js 配置 @mdx-js/loader
module.exports = {
module: {
rules: [
{
test: /.mdx?$/,
use: [
{
loader: '@mdx-js/loader',
options: {
providerImportSource: '@mdx-js/react',
development: process.env.NODE_ENV === 'development'
}
}
]
}
]
}
};
// 业务代码中直接导入
import BlogPost from './blog-post.mdx';
// webpack 会自动调用 loader 处理该文件
@mdx-js/react 是运行时环境。
编译后的代码需要特定的 React 上下文才能工作。这个包提供了 MDXProvider 组件,用于注入自定义组件映射表,并处理组件查找逻辑。没有它,编译出的代码会因找不到 _provideComponents 而报错。
// 在 React 应用入口包裹 MDXProvider
import { MDXProvider } from '@mdx-js/react';
import { h1, a, pre } from './custom-components';
const components = { h1, a, pre };
function App({ children }) {
return (
<MDXProvider components={components}>
{children}
</MDXProvider>
);
}
// 编译后的 MDX 组件内部会调用 useMDXComponents()
// 从 MDXProvider 的上下文中获取自定义组件
选择哪个包,本质上取决于你希望如何控制编译过程。
手动控制流程(使用 @mdx-js/mdx)
适用于非标准构建环境。例如,你可能需要在服务端按需编译用户提交的内容,或者在 Vite 中编写自定义插件。
// 场景:在 Node.js 服务端动态编译
import { compile } from '@mdx-js/mdx';
import { renderToString } from 'react-dom/server';
import { createRoot } from 'react-dom/client'; // 假设有运行时环境
async function renderUserContent(content) {
const { default: MDXContent } = await compile(content, {
outputFormat: 'function-body'
});
// 注意:这里需要手动处理 React 环境注入
// 实际生产中需配合 @mdx-js/react 的上下文机制
return renderToString(<MDXContent />);
}
自动构建集成(使用 @mdx-js/loader)
适用于标准 webpack 项目。你无需在业务代码中调用编译函数,只需配置规则,构建工具会自动处理。
// 场景:Next.js 或 Create React App 项目
// 只需在 next.config.js 或 webpack 配置中添加 loader
// 业务代码保持简洁
import Article from './articles/intro.mdx';
function Page() {
return (
<main>
<Article /> {/* 直接作为组件使用 */}
</main>
);
}
虽然 @mdx-js/loader 内部调用了 @mdx-js/mdx,但它们的配置层级不同。加载器的选项会透传给编译器,但增加了构建特定的配置项。
@mdx-js/mdx 关注语言特性。
配置项主要涉及语法解析、输出格式和插件链。
// @mdx-js/mdx 配置示例
await compile(text, {
recmaPlugins: [myRecmaPlugin], // 操作 ESTree
rehypePlugins: [myRehypePlugin], // 操作 HTML AST
remarkPlugins: [myRemarkPlugin], // 操作 Markdown AST
outputFormat: 'function-body', // 输出函数体还是完整模块
development: true // 包含调试信息
});
@mdx-js/loader 关注构建集成。
除了透传上述选项外,还必须指定 React 组件的来源,以便编译后的代码知道从哪里导入 MDXProvider 相关逻辑。
// @mdx-js/loader 配置示例
{
loader: '@mdx-js/loader',
options: {
providerImportSource: '@mdx-js/react', // 必填:指定运行时包
remarkPlugins: [/* 透传给编译器 */],
format: 'detect', // 自动检测 .md 还是 .mdx
webpackLoaderOptions: { /* webpack 特定选项 */ }
}
}
误区一:以为安装了 loader 就不需要 react 包
很多开发者只安装了 @mdx-js/loader,却在运行时遇到 _provideComponents is not defined 错误。这是因为 loader 只负责编译,生成的代码仍然依赖 @mdx-js/react 提供的运行时环境。必须同时安装并配置 MDXProvider。
误区二:在非 webpack 项目中强行使用 loader
@mdx-js/loader 仅对 webpack 有效。如果在 Vite、Rollup 或 Esbuild 项目中尝试使用它,构建会直接失败。这些工具需要各自的插件(如 @mdx-js/rollup 或 @mdx-js/esbuild),但底层编译器依然是 @mdx-js/mdx。
误区三:混淆编译时与运行时错误
如果 MDX 语法有误,@mdx-js/mdx 或 @mdx-js/loader 会在构建阶段报错。但如果 MDXProvider 未正确包裹,错误会在浏览器运行时出现。调试时需区分错误发生的时间点。
// 错误示范:缺少 MDXProvider 包裹
function App() {
return <BlogPost />; // 运行时可能报错,找不到组件上下文
}
// 正确示范
function App() {
return (
<MDXProvider components={{ h1: CustomH1 }}>
<BlogPost />
</MDXProvider>
);
}
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 标准 webpack 项目 | @mdx-js/loader + @mdx-js/react | 集成最简单,支持 HMR 和源映射 |
| Vite / Rollup 项目 | @mdx-js/mdx (配合对应插件) + @mdx-js/react | loader 不兼容,需用底层编译器 |
| 服务端动态渲染 | @mdx-js/mdx + @mdx-js/react | 需在运行时编译字符串,无需构建工具 |
| 自定义 CLI 工具 | @mdx-js/mdx | 只需编译功能,无需构建集成 |
| 静态站点生成 (SSG) | @mdx-js/loader 或对应框架插件 | 利用构建时编译优化性能 |
在现代前端架构中,建议遵循"关注点分离"原则:
@mdx-js/mdx 作为核心引擎。即使使用 loader,也要明白它只是包装器。@mdx-js/loader,其他工具寻找社区插件或直接调用编译器 API。@mdx-js/react。它是连接编译产物与 React 树的桥梁。这种分层设计让你的系统更具弹性。如果未来从 webpack 迁移到 Vite,只需替换构建层适配器,核心编译逻辑和渲染组件无需改动。
@mdx-js/mdx 是引擎,@mdx-js/loader 是变速箱,@mdx-js/react 是车轮。引擎产生动力,变速箱将动力传递给车轮,车轮让车跑起来。缺一不可,但不能混用。
@mdx-js/mdx。@mdx-js/loader。@mdx-js/react。理解这三者的边界,能让你在面对复杂的文档系统或内容平台架构时,做出更清晰的技术决策。
如果你的项目基于 webpack(包括 Next.js、Gatsby 或自定义 webpack 配置),且希望通过 import 语句直接加载 .mdx 文件,必须使用 @mdx-js/loader。它简化了构建配置,自动处理依赖收集和源映射。但请注意,它仅限于 webpack 环境;若迁移到其他构建工具,此包将无法工作,需替换为对应的插件。
当你需要构建自定义构建插件、在非 webpack 环境(如 Vite、Rollup、Esbuild)中处理 MDX,或者需要在 Node.js 服务端动态编译内容时,应选择 @mdx-js/mdx。它是底层原语,不提供直接的模块导入功能,而是输出代码字符串供你自行处理。如果你只需要纯粹的文本转代码能力而不关心如何将其打包进应用,这是唯一的选择。
只要你的最终渲染目标是 React 应用,就必须安装 @mdx-js/react。无论使用何种构建工具,编译后的 MDX 内容都需要此包提供的 MDXProvider 来注入自定义组件(如将 <h1> 映射为特定样式组件)和处理上下文。如果不使用它,编译出的组件将缺少必要的运行时环境,导致渲染错误或无法进行主题化。
@mdx-js/loaderwebpack loader for MDX.
This package is a webpack loader to support MDX.
This integration is useful if you’re using webpack (or another tool that uses webpack, such as Next.js or Rspack).
This integration can be combined with the Babel loader to compile modern JavaScript features to ones your users support.
If you want to evaluate MDX code then the lower-level compiler (@mdx-js/mdx)
can be used manually.
This package is ESM only. In Node.js (version 16+), install with npm:
npm install @mdx-js/loader
Add something along these lines to your webpack.config.js:
/**
* @import {Options} from '@mdx-js/loader'
* @import {Configuration} from 'webpack'
*/
/** @type {Configuration} */
const webpackConfig = {
module: {
// …
rules: [
// …
{
test: /\.mdx?$/,
use: [
{
loader: '@mdx-js/loader',
/** @type {Options} */
options: {/* jsxImportSource: …, otherOptions… */}
}
]
}
]
}
}
export default webpackConfig
See also ¶ Next.js and ¶ Vue CLI, if you’re using webpack through them, for more info.
This package exports no identifiers.
The default export is mdx.
mdxThis package exports a webpack plugin as the default export.
Configuration (see Options) are passed separately through
webpack.
OptionsConfiguration (TypeScript type).
Options are the same as CompileOptions from @mdx-js/mdx
with the exception that the SourceMapGenerator and development options are
supported based on how you configure webpack.
You cannot pass them manually.
If you use modern JavaScript features you might want to use Babel through
babel-loader to compile to code that works in older browsers:
/**
* @import {Options} from '@mdx-js/loader'
* @import {Configuration} from 'webpack'
*/
/** @type {Configuration} */
const webpackConfig = {
module: {
// …
rules: [
// …
{
test: /\.mdx?$/,
use: [
// Note that Webpack runs right-to-left: `@mdx-js/loader` is used first, then
// `babel-loader`.
{loader: 'babel-loader', options: {}},
{
loader: '@mdx-js/loader',
/** @type {Options} */
options: {}
}
]
}
]
}
}
export default webpackConfig
This package is fully typed with TypeScript.
It exports the additional type Options.
See § Types on our website for information.
Projects maintained by the unified collective are compatible with maintained versions of Node.js.
When we cut a new major release, we drop support for unmaintained versions of
Node.
This means we try to keep the current release line, @mdx-js/loader@^3,
compatible with Node.js 16.
See § Security on our website for information.
See § Contribute on our website for ways to get started. See § Support for ways to get help.
This project has a code of conduct. By interacting with this repository, organization, or community you agree to abide by its terms.