Skip to navigation

使用 MDX

¥Using MDX

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

¥This article explains how to use MDX files in your project. It shows how you can pass props and how to import, define, or pass components. See § Getting started for how to integrate MDX into your project. To understand how the MDX format works, we recommend that you start with § What is MDX.

内容

¥Contents

MDX 的工作原理

¥How MDX works

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

¥An integration compiles MDX syntax to JavaScript. Say we have an MDX document, example.mdx:

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

# Hello <Thing />

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

¥That’s roughly turned into the following JavaScript. The below might help to form a mental model:

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

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

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

一些观察结果:

¥Some observations:

  • 输出是序列化的 JavaScript,仍需要评估

    ¥The output is serialized JavaScript that still needs to be evaluated

  • 注入注释来配置 JSX 的处理方式

    ¥A comment is injected to configure how JSX is handled

  • 这是一个包含导入/导出的完整文件

    ¥It’s a complete file with import/exports

  • 导出组件 (MDXContent)

    ¥A component (MDXContent) is exported

实际输出是:

¥The actual output is:

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)
}

更多观察结果:

¥Some more observations:

  • JSX 被编译为函数调用和 React 的导入†

    ¥JSX is compiled away to function calls and an import of React†

  • 内容组件可以给 {components: {wrapper: MyLayout}} 来封装所有内容

    ¥The content component can be given {components: {wrapper: MyLayout}} to wrap all content

  • 内容组件可以指定 {components: {h1: MyComponent}} 以使用其他内容作为标题

    ¥The content component can be given {components: {h1: MyComponent}} to use something else for the heading

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

¥† MDX is not coupled to React. You can also use it with Preact, Vue, Emotion, Theme UI, etc. Both the classic and automatic JSX runtimes are supported.

MDX 内容

¥MDX content

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

¥We just saw that MDX files are compiled to components. You can use those components like any other component in your framework of choice. Take this file:

example.mdx
# Hi!

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

¥It could be imported and used in a React app like so:

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 />)

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

¥The main content is exported as the default export. All other values are also exported. Take this example:

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

# Hello <Thing />

可以通过以下方式导入:

¥It could be imported in the following ways:

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]

属性

¥Props

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

¥In § What is MDX, we showed that JavaScript expressions, inside curly braces, can be used in MDX:

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

# Hello {name.toUpperCase()}

The current year is {year}

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

¥Instead of importing or defining data within MDX, data can also be passed to MDXContent. The passed data is called props. Take for example:

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

The current year is {props.year}

该文件可以用作:

¥This file could be used as:

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

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

¥There is one special prop: components. It takes an object mapping component names to components. Take this example:

example.mdx
# Hello *<Planet />*

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

¥It can be imported from JavaScript and passed components like so:

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 中定义或导入它们:

¥You don’t have to pass components. You can also define or import them within MDX:

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

MDX using imported components!

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

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

¥Because MDX files are components, they can also import each other:

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 />

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

¥Here are some other examples of passing components:

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 中传递以下键:

¥The following keys can be passed in components:

  • 使用 Markdown 编写的内容的 HTML 等效项,例如 h1 对应 # heading(有关示例,请参阅 § 组件表

    ¥HTML equivalents for the things you write with markdown such as h1 for # heading (see § Table of components for examples)

  • wrapper,定义布局(但本地布局优先)

    ¥wrapper, which defines the layout (but a local layout takes precedence)

  • * 对于你使用 JSX 编写的内容来说,任何其他有效的 JavaScript 标识符(fooQuote_$xa1)(例如 <So /><like.so />,请注意本地定义的组件优先)‡

    ¥* anything else that is a valid JavaScript identifier (foo, Quote, _, $x, a1) for the things you write with JSX (like <So /> or <like.so />, note that locally defined components take precedence)

如果你想知道 JSX 中的名称(例如 <x> 中的 x)是否是字面标签名称(例如 h1)或不是(例如 Component)的规则是什么,它们如下:

¥If you ever wondered what the rules are for whether a name in JSX (so x in <x>) is a literal tag name (like h1) or not (like Component), they are as follows:

  • 如果有点,则为成员表达式(<a.b> -> h(a.b)),这意味着 b 组件取自对象 a

    ¥If there’s a dot, it’s a member expression (<a.b> -> h(a.b)), which means that the b component is taken from object a

  • 否则,如果名称不是有效标识符,则它是字面量 (<a-b> -> h('a-b'))

    ¥Otherwise, if the name is not a valid identifier, it’s a literal (<a-b> -> h('a-b'))

  • 否则,如果它以小写字母开头,则它是一个字面量 (<a> -> h('a'))

    ¥Otherwise, if it starts with a lowercase, it’s a literal (<a> -> h('a'))

  • 否则,它是一个标识符(<A> -> h(A)),这意味着组件 A

    ¥Otherwise, it’s an identifier (<A> -> h(A)), which means a component A

布局

¥Layout

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

¥There is one special component: the layout. If it is defined, it’s used to wrap all content. A layout can be defined from within MDX using a default export:

MDX
export default function Layout({children}) {
  return <main>{children}</main>;
}

All the things.

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

¥The layout can also be imported and then exported with an export … from:

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

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

¥The layout can also be passed as components.wrapper (but a local one takes precedence).

MDX 提供商

¥MDX provider

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

¥You probably don’t need a provider. Passing components is typically fine. Providers often only add extra weight. Take for example this file:

post.mdx
# Hello world

像这样使用:

¥Used like so:

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} />)

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

¥That works, those components are used.

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

¥But when you’re nesting MDX files (importing them into each other) it can become cumbersome. Like so:

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。像这样设置:

¥To solve this, a context can be used in React, Preact, and Vue. Context provides a way to pass data through the component tree without having to pass props down manually at every level. Set it up like so:

  1. 安装 @mdx-js/react@mdx-js/preact@mdx-js/vue,具体取决于你使用的框架

    ¥Install either @mdx-js/react, @mdx-js/preact, or @mdx-js/vue, depending on what framework you’re using

  2. 配置 MDX 集成,并将 providerImportSourceProcessorOptions 设置为该包,因此 '@mdx-js/react''@mdx-js/preact''@mdx-js/vue'

    ¥Configure your MDX integration with providerImportSource in ProcessorOptions set to that package, so either '@mdx-js/react', '@mdx-js/preact', or '@mdx-js/vue'

  3. 从该包导入 MDXProvider。使用它来封装最顶层的 MDX 内容组件,并将其传递给你的 components

    ¥Import MDXProvider from that package. Use it to wrap your top-most MDX content component and pass it your components instead:

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>
+)

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

¥Now you can remove the explicit and verbose component passing:

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 嵌套时,它们的组件会合并。举个例子:

¥When MDXProviders are nested, their components are merged. Take this example:

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

…这导致 h1 使用 Component1h2 使用 Component3h3 使用 Component4

¥…which results in h1s using Component1, h2s using Component3, and h3s using Component4.

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

¥To merge differently or not at all, pass a function to components. It’s given the current context components and what it returns will be used instead. In this example the current context components are discarded:

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

…这导致 h2 使用 Component3h3 使用 Component4h1 未使用任何组件。

¥…which results in h2s using Component3 and h3s using Component4. No component is used for h1.

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

¥If you’re not nesting MDX files, or not nesting them often, don’t use providers: pass components explicitly.

MDX 中文网 - 粤ICP备13048890号