Skip to navigation

使用 MDX

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

内容

MDX 的工作原理

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

input.mdx
export function Thing() {
  return <>World</>
}

# Hello <Thing />

大致变成了下面的 JavaScript。 以下内容可能有助于形成心理模型:

output-outline.jsx
/* @jsxRuntime automatic @jsxImportSource react */

export function Thing() {
  return <>World</>
}

export default function MDXContent() {
  return <h1>Hello <Thing /></h1>
}

一些观察结果:

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

实际输出是:

output-actual.js
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'

export function Thing() {
  return _jsx(_Fragment, {children: 'World'})
}

function _createMdxContent(props) {
  const _components = {h1: 'h1', ...props.components}
  return _jsxs(_components.h1, {children: ['Hello ', _jsx(Thing, {})]})
}

export default function MDXContent(props = {}) {
  const {wrapper: MDXLayout} = props.components || {}
  return MDXLayout
    ? _jsx(MDXLayout, {...props, children: _jsx(_createMdxContent, {...props})})
    : _createMdxContent(props)
}

更多观察结果:

  • JSX is compiled away to function calls and an import of React†
  • 内容组件可以给 {components: {wrapper: MyLayout}} 来封装所有内容
  • 内容组件可以指定 {components: {h1: MyComponent}} 以使用其他内容作为标题

† MDX 未耦合到 React。 你还可以将其与 PreactVueEmotion主题界面 等一起使用。支持经典和自动 JSX 运行时。

MDX 内容

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

example.mdx
# Hi!

它可以在 React 应用中导入和使用,如下所示:

example.js
import React from 'react'
import ReactDom from 'react-dom'
import Example from './example.mdx' // Assumes an integration is used to compile MDX -> JS.

const root = ReactDom.createRoot(document.getElementById('root'))
root.render(<Example />)

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

example.mdx
export function Thing() {
  return <>World</>
}

# Hello <Thing />

可以通过以下方式导入:

example.js
// A namespace import to get everything:
import * as everything from './example.mdx' // Assumes an integration is used to compile MDX -> JS.
console.log(everything) // {Thing: [Function: Thing], default: [Function: MDXContent]}

// Default export shortcut and a named import specifier:
import Content, {Thing} from './example.mdx'
console.log(Content) // [Function: MDXContent]
console.log(Thing) // [Function: Thing]

// Import specifier with another local name:
import {Thing as AnotherName} from './example.mdx'
console.log(AnotherName) // [Function: Thing]

属性

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

example.mdx
import {year} from './data.js'
export const name = 'world'

# Hello {name.toUpperCase()}

The current year is {year}

数据也可以传递到 MDXContent,而不是在 MDX 中导入或定义数据。 传递的数据称为 props。 举个例子:

example.mdx
# Hello {props.name.toUpperCase()}

The current year is {props.year}

该文件可以用作:

example.jsx
import Example from './example.mdx' // Assumes an integration is used to compile MDX -> JS.

// Use a `createElement` call:
React.createElement(Example, {name: 'Venus', year: 2021})

// Use JSX:
<Example name="Mars" year={2022} />

组件列表

有一个特殊的属性: components。 它需要一个将组件名称映射到组件的对象。 举个例子:

example.mdx
# Hello *<Planet />*

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

example.jsx
import Example from './example.mdx' // Assumes an integration is used to compile MDX -> JS.

<Example
  components={{
    Planet() {
      return <span style={{color: 'tomato'}}>Pluto</span>
    }
  }}
/>

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

example.mdx
import {Box, Heading} from 'rebass'

MDX using imported components!

<Box>
  <Heading>Here’s a heading</Heading>
</Box>

由于 MDX 文件是组件,因此它们也可以相互导入:

example.mdx
import License from './license.md' // Assumes an integration is used to compile MDX -> JS.
import Contributing from './docs/contributing.mdx'

# Hello world

<License />

---

<Contributing />

以下是传递组件的其他一些示例:

example.jsx
<Example
  components={{
    // Map `h1` (`# heading`) to use `h2`s.
    h1: 'h2',
    // Rewrite `em`s (`*like so*`) to `i` with a goldenrod foreground color.
    em(props) {
      return <i style={{color: 'goldenrod'}} {...props} />
    },
    // Pass a layout (using the special `'wrapper'` key).
    wrapper({components, ...rest}) {
      return <main {...rest} />
    },
    // Pass a component.
    Planet() {
      return 'Neptune'
    },
    // This nested component can be used as `<theme.text>hi</theme.text>`
    theme: {
      text(props) {
        return <span style={{color: 'grey'}} {...props} />
      }
    }
  }}
/>

可以在 components 中传递以下键:

  • 使用 Markdown 编写的内容的 HTML 等效项,例如 h1 对应 # heading(有关示例,请参阅 § 组件表
  • wrapper,定义布局(但本地布局优先)
  • * 对于你使用 JSX 编写的内容来说,任何其他有效的 JavaScript 标识符(fooQuote_$xa1)(例如 <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
export default function Layout({children}) {
  return <main>{children}</main>;
}

All the things.

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

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

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

MDX 提供商

你可能不需要提供者。 传递组件通常没问题。 提供商通常只会增加额外的重量。 以这个文件为例:

post.mdx
# Hello world

像这样使用:

app.jsx
import React from 'react'
import ReactDom from 'react-dom'
import {Heading, /* … */ Table} from './components/index.js'
import Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.

const components = {
  h1: Heading.H1,
  // …
  table: Table
}

const root = ReactDom.createRoot(document.getElementById('root'))
root.render(<Post components={components} />)

有效,这些组件已被使用。

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

post.mdx
import License from './license.md' // Assumes an integration is used to compile MDX -> JS.
import Contributing from './docs/contributing.mdx'

# Hello world

<License components={props.components} />

---

<Contributing components={props.components} />

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

  1. 安装 @mdx-js/react@mdx-js/preact@mdx-js/vue,具体取决于你使用的框架
  2. 配置 MDX 集成,并将 providerImportSourceProcessorOptions 设置为该包,因此 '@mdx-js/react''@mdx-js/preact''@mdx-js/vue'
  3. 从该包导入 MDXProvider。 使用它来封装最顶层的 MDX 内容组件,并将其传递给你的 components
Diff
+import {MDXProvider} from '@mdx-js/react'
 import React from 'react'
 import ReactDom from 'react-dom'
 import {Heading, /* … */ Table} from './components/index.js'
 import Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.

 const components = {
   h1: Heading.H1,
   // …
   table: Table
 }

 const root = ReactDom.createRoot(document.getElementById('root'))
-root.render(<Post components={components} />)
+root.render(
+  <MDXProvider components={components}>
+    <Post />
+  </MDXProvider>
+)

现在你可以删除显式且详细的组件传递:

Diff
 import License from './license.md' // Assumes an integration is used to compile MDX -> JS.
 import Contributing from './docs/contributing.mdx'

 # Hello world

-<License components={props.components} />
+<License />

 ---

-<Contributing components={props.components} />
+<Contributing />

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

TypeScript
<>
  <MDXProvider components={{h1: Component1, h2: Component2}}>
    <MDXProvider components={{h2: Component3, h3: Component4}}>
      <Content />
    </MDXProvider>
  </MDXProvider>
</>

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

要以不同方式合并或根本不合并,请将函数传递给 components。 它给出了当前上下文 components,并且将使用它返回的内容。 在此示例中,当前上下文组件被丢弃:

TypeScript
<>
  <MDXProvider components={{h1: Component1, h2: Component2}}>
    <MDXProvider
      components={
        function () {
          return {h2: Component3, h3: Component4}
        }
      }
    >
      <Content />
    </MDXProvider>
  </MDXProvider>
</>

…结果是 h2 使用 Component3h3 使用 Component4h1 未使用任何组件。

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

MDX 中文网 - 粤ICP备13048890号