本文介绍了如何在您的项目中使用 MDX 文件。它展示了如何传递属性以及如何导入、定义或传递组件。有关如何将 MDX 集成到您的项目中,请参阅§ Getting started。要了解 MDX 格式的工作原理,我们建议您从§ 什么是 MDX 开始。

MDX 工作原理

一个集成系统将 MDX 语法编译成 JavaScript。假设我们有一个 MDX 文档,example.mdx

input.mdx

  1. export function Thing() {
  2. return <>World</>
  3. }
  4. # Hello <Thing />

这大致转换为以下 JavaScript。下面的内容可能有助于形成一个心理模型:

output-outline.jsx

  1. /* @jsxRuntime automatic */
  2. /* @jsxImportSource react */
  3. export function Thing() {
  4. return <>World</>
  5. }
  6. export default function MDXContent() {
  7. return <h1>Hello <Thing /></h1>
  8. }

一些观察结果:

  • 输出是序列化的 JavaScript,仍然需要被评估
  • 注入了一个注释来配置 JSX 的处理方式
  • 这是一个完整的文件,具有导入/导出
  • 导出了一个组件 (MDXContent)

实际输出如下:

output-actual.js

  1. import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
  2. export function Thing() {
  3. return _jsx(_Fragment, {children: 'World'})
  4. }
  5. function _createMdxContent(props) {
  6. const _components = {h1: 'h1', ...props.components}
  7. return _jsxs(_components.h1, {children: ['Hello ', _jsx(Thing, {})]})
  8. }
  9. export default function MDXContent(props = {}) {
  10. const {wrapper: MDXLayout} = props.components || {}
  11. return MDXLayout
  12. ? _jsx(MDXLayout, {...props, children: _jsx(_createMdxContent, {...props})})
  13. : _createMdxContent(props)
  14. }

一些额外的观察结果:

  • JSX 被编译成函数调用和对 React 的引入†
  • 内容组件可以通过 {components: {wrapper: MyLayout}} 来包装所有内容
  • 内容组件可以通过 {components: {h1: MyComponent}} 来使用其他组件作为标题

† MDX 与 React 没有耦合。您也可以将其与 Preact, Vue, Emotion, Theme UI 等一起使用。支持经典和自动 JSX 运行时。

MDX 内容

我们刚刚看到 MDX 文件被编译成了组件。您可以像在您选择的框架中使用任何其他组件一样使用这些组件。看这个文件:

example.mdx

  1. # Hi!

它可以像这样在 React 应用程序中导入和使用:

example.js

  1. import React from 'react'
  2. import ReactDom from 'react-dom'
  3. import Example from './example.mdx' // 假设使用集成编译 MDX -> JS。
  4. const root = ReactDom.createRoot(document.getElementById('root'))
  5. root.render(<Example />)

主要内容被导出为默认导出。所有其他值也被导出。看这个例子:

example.mdx

  1. export function Thing() {
  2. return <>World</>
  3. }
  4. # Hello <Thing />

它可以以以下方式导入:

example.js

  1. // 一个命名空间导入以获取所有内容:
  2. import * as everything from './example.mdx' // 假设使用集成编译 MDX -> JS。
  3. console.log(everything) // {Thing: [Function: Thing], default: [Function: MDXContent]}
  4. // 默认导出快捷方式和一个命名导入限定符:
  5. import Content, {Thing} from './example.mdx'
  6. console.log(Content) // [Function: MDXContent]
  7. console.log(Thing) // [Function: Thing]
  8. // 具有另一个本地名称的导入限定符:
  9. import {Thing as AnotherName} from './example.mdx'
  10. console.log(AnotherName) // [Function: Thing]

属性

§ 什么是 MDX 中,我们展示了在 MDX 中使用花括号内的 JavaScript 表达式:

example.mdx

  1. import {year} from './data.js'
  2. export const name = 'world'
  3. # Hello {name.toUpperCase()}
  4. The current year is {year}

除了在 MDX 中导入或定义数据,数据也可以传递给 MDXContent。传递的数据称为 props。例如:

example.mdx

  1. # Hello {props.name.toUpperCase()}
  2. The current year is {props.year}

这个文件可以这样使用:

example.jsx

  1. import Example from './example.mdx' // 假设使用集成编译 MDX -> JS。
  2. // 使用 `createElement` 调用:
  3. React.createElement(Example, {name: 'Venus', year: 2021})
  4. // 使用 JSX:
  5. <Example name="Mars" year={2022} />

组件

有一个特殊的属性:components。它接受一个将组件名称映射到组件的对象。看这个例子:

example.mdx

  1. # Hello *<Planet />*

它可以从 JavaScript 中导入并传递组件,如下所示:

example.jsx

  1. import Example from './example.mdx' // 假设使用集成编译 MDX -> JS。
  2. <Example
  3. components={{
  4. Planet() {
  5. return <span style={{color: 'tomato'}}>Pluto</span>
  6. }
  7. }}
  8. />

您不必传递组件。您也可以在 MDX 中定义或导入它们:

example.mdx

  1. import {Box, Heading} from 'rebass'
  2. 使用导入的组件的 MDX
  3. <Box>
  4. <Heading>这是一个标题</Heading>
  5. </Box>

因为 MDX 文件 就是 组件,它们也可以相互导入:

example.mdx

  1. import License from './license.md' // 假设使用集成编译 MDX -> JS。
  2. import Contributing from './docs/contributing.mdx'
  3. # 你好世界
  4. <License />
  5. ---
  6. <Contributing />

这里有一些传递组件的其他示例:

example

.jsx

  1. <Example
  2. components={{
  3. // 将 `h1` (`# heading`) 映射到使用 `h2`。
  4. h1: 'h2',
  5. // 将 `em` (`*like so*`) 重写为具有金色前景色的 `i`。
  6. em(props) {
  7. return <i style={{color: 'goldenrod'}} {...props} />
  8. },
  9. // 传递布局(使用特殊的 `'wrapper'` 键)。
  10. wrapper({components, ...rest}) {
  11. return <main {...rest} />
  12. },
  13. // 传递一个组件。
  14. Planet() {
  15. return 'Neptune'
  16. },
  17. // 此嵌套组件可以使用 `<theme.text>hi</theme.text>`
  18. theme: {
  19. text(props) {
  20. return <span style={{color: 'grey'}} {...props} />
  21. }
  22. }
  23. }}
  24. />

components 中可以传递以下键:

  • 您使用 markdown 编写的 HTML 等效项,例如 # headingh1(请参阅 § 组件表 以查看示例)
  • wrapper,它定义了布局(但本地布局优先)
  • * 其他任何有效的 JavaScript 标识符(fooQuote_$xa1),用于 JSX 中使用的东西(如 <So /><like.so />,请注意本地定义的组件优先级)

如果您曾经想知道 JSX 中的名称(例如 <x> 中的 x)是字面标记名称(例如 h1)还是非字面标记名称(例如 Component)的规则是什么,它们如下:

  • 如果有一个点,它是一个成员表达式(<a.b> -> h(a.b)),这意味着组件 b 是从对象 a 中获取的
  • 否则,如果名称不是有效的标识符,则它是一个字面量(<a-b> -> h('a-b')
  • 否则,如果名称以小写字母开头,则它是一个字面量(<a> -> h('a')
  • 否则,它是一个标识符(<A> -> h(A)),这意味着组件 A

布局

有一个特殊的组件:布局。如果定义了布局,它将用于包装所有内容。可以使用默认导出从 MDX 中定义布局:

MDX

  1. export default function Layout({children}) {
  2. return <main>{children}</main>;
  3. }
  4. 所有的东西。

布局也可以被导入,然后使用 export … from 导出:

MDX

  1. export {Layout as default} from './components.js'

布局也可以作为 components.wrapper 传递(但本地布局优先)。

MDX 提供者

您可能不需要提供者。通常情况下,传递组件就足够了。提供者通常只会增加额外的负担。比如这个文件:

post.mdx

  1. # Hello world

如下使用:

app.jsx

  1. import React from 'react'
  2. import ReactDom from 'react-dom'
  3. import {Heading, /* … */ Table} from './components/index.js'
  4. import Post from './post.mdx' // 假设使用集成编译 MDX -> JS。
  5. const components = {
  6. h1: Heading.H1,
  7. // …
  8. table: Table
  9. }
  10. const root = ReactDom.createRoot(document.getElementById('root'))
  11. root.render(<Post components={components} />)

这样可以工作,这些组件被使用了。

但是当您嵌套 MDX 文件(相互导入它们)时,可能会变得麻烦。就像这样:

post.mdx

  1. import License from './license.md' // 假设使用集成编译 MDX -> JS。
  2. import Contributing from './docs/contributing.mdx'
  3. # Hello world
  4. <License components={props.components} />
  5. ---
  6. <Contributing components={props.components} />

为了解决这个问题,在 React、Preact 和 Vue 中可以使用一个 上下文。上下文提供了一种通过组件树传递数据的方法,而不必在每个级别手动传递 props。设置方法如下:

  1. 根据您使用的框架,安装 @mdx-js/react@mdx-js/preact@mdx-js/vue 中的一个
  2. 使用 ProcessorOptions 中的 providerImportSource 配置您的 MDX 集成,设置为该包,因此为 '@mdx-js/react''@mdx-js/preact''@mdx-js/vue'
  3. 从该包中导入 MDXProvider。使用它来包装您的最顶层 MDX 内容组件,并将其传递给您的 components

Diff

  1. +import {MDXProvider} from '@mdx-js/react'
  2. import React from 'react'
  3. import ReactDom from 'react-dom'
  4. import {Heading, /* … */ Table} from './components/index.js'
  5. import Post from './post.mdx' // 假设使用集成编译 MDX -> JS。
  6. const components = {
  7. h1: Heading.H1,
  8. // …
  9. table: Table
  10. }
  11. const root = ReactDom.createRoot(document.getElementById('root'))
  12. -root.render(<Post components={components} />)
  13. +root.render(
  14. + <MDXProvider components={components}>
  15. + <Post />
  16. + </MDXProvider>
  17. +)

现在,您可以删除显式和冗长的组件传递:

Diff

  1. import License from './license.md' // 假设使用集成编译 MDX -> JS。
  2. import Contributing from './docs/contributing.mdx'
  3. # Hello world
  4. -<License components={props.components} />
  5. +<License />
  6. ---
  7. -<Contributing components={props.components} />
  8. +<
  9. Contributing />

当 MDXProvider 嵌套时,它们的组件会合并。看这个例子:

TypeScript

  1. <>
  2. <MDXProvider components={{h1: Component1, h2: Component2}}>
  3. <MDXProvider components={{h2: Component3, h3: Component4}}>
  4. <Content />
  5. </MDXProvider>
  6. </MDXProvider>
  7. </>

…结果是 h1 使用 Component1h2 使用 Component3h3 使用 Component4

要以不同的方式合并或不合并,将函数传递给 components。它会给出当前上下文 components,并将其返回的内容用于替代。在此示例中,当前上下文组件被丢弃:

TypeScript

  1. <>
  2. <MDXProvider components={{h1: Component1, h2: Component2}}>
  3. <MDXProvider
  4. components={
  5. function () {
  6. return {h2: Component3, h3: Component4}
  7. }
  8. }
  9. >
  10. <Content />
  11. </MDXProvider>
  12. </MDXProvider>
  13. </>

…结果是 h2 使用 Component3h3 使用 Component4。没有组件用于 h1

如果您不是经常嵌套 MDX 文件,或者根本不嵌套它们,请不要使用提供者:显式传递组件。