Project Icon

next-mdx-remote

优化Next.js应用中MDX内容的加载与渲染

next-mdx-remote是一个轻量级工具集,用于在Next.js应用中高效处理MDX内容。它支持在getStaticProps或getServerSideProps中加载MDX,并在客户端进行水合渲染。通过提供serialize函数和MDXRemote组件,开发者可以灵活处理各种来源的MDX文本,支持自定义组件、作用域和前置元数据解析。该库优化了性能,简化了MDX的使用流程,适合构建灵活的内容驱动型Next.js网站。

next-mdx-remote

一组轻量级实用工具,允许在 getStaticPropsgetServerSideProps 中加载 MDX,并在客户端正确地进行水合。

next-mdx-remote


安装

npm install next-mdx-remote

如果与 Turbopack 一起使用,在解决此问题之前,你需要在 next.config.js 中添加以下内容:

const nextConfig = {
+  transpilePackages: ['next-mdx-remote'],
}

示例

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} />
    </div>
  )
}

export async function getStaticProps() {
  // MDX 文本 - 可以来自本地文件、数据库或任何地方
  const source = 'Some **mdx** text, with a component <Test />'
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}

虽然在同一个文件中看到这两个部分可能有些奇怪,但这正是 Next.js 的一个特点 —— getStaticPropsTestPage 虽然出现在同一个文件中,但它们在两个不同的地方运行。最终,你的浏览器包不会包含 getStaticProps,也不会包含它仅在服务器上使用的任何函数,因此 serialize 将完全从浏览器包中移除。

重要提示:请非常谨慎地将任何 next-mdx-remote 代码放入单独的"实用工具"文件中。这样做可能会导致 Next.js 的代码分割功能出现问题 - 它必须能够清晰地确定哪些内容仅在服务器端使用,哪些应该保留在客户端包中。如果你将 next-mdx-remote 代码放入外部实用工具文件中并出现问题,请先移除它并从上面的简单示例开始,然后再提交问题。

其他示例

解析前置元数据

Markdown 通常与前置元数据配对使用,这通常意味着需要对 markdown 的处理方式进行一些额外的自定义处理。为了解决这个问题,next-mdx-remote 提供了可选的前置元数据解析功能,可以通过向 serialize 传递 parseFrontmatter: true 来启用。

以下是使用示例:

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }

export default function TestPage({ mdxSource }) {
  return (
    <div className="wrapper">
      <h1>{mdxSource.frontmatter.title}</h1>
      <MDXRemote {...mdxSource} components={components} />
    </div>
  )
}

export async function getStaticProps() {
  // MDX 文本 - 可以来自本地文件、数据库或任何地方
  const source = `---
title: Test
---

Some **mdx** text, with a component <Test name={frontmatter.title}/>
  `

  const mdxSource = await serialize(source, { parseFrontmatter: true })
  return { props: { mdxSource } }
}

vfile-matter 用于解析前置元数据。

使用 `scope` 向组件传递自定义数据

<MDXRemote /> 接受一个 scope 属性,使所有值都可以在你的 MDX 中使用。

scope 参数中的每个键值对都将作为 JavaScript 变量公开。例如,如果你有一个 scope{ foo: 'bar' },它将被解释为 const foo = 'bar'

这特别意味着你需要确保 scope 参数中的键名是有效的 JavaScript 变量名。例如,传入 { 'my-variable-name': 'bar' } 会产生_错误_,因为键名不是有效的 JavaScript 变量名。

同样重要的是要注意,scope 变量必须作为_组件的参数_使用,不能在文本中间渲染。这在下面的示例中有所展示。

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }
const data = { product: 'next' }

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} scope={data} />
    </div>
  )
}

export async function getStaticProps() {
  // MDX 文本 - 可以来自本地文件、数据库或任何地方
  const source =
    'Some **mdx** text, with a component using a scope variable <Test product={product} />'
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}
将 `scope` 传入 `serialize` 函数

你也可以将自定义数据传入 serialize,它会将值传递并使其在结果中可用。通过将 source 的结果展开到 <MDXRemote /> 中,数据将可用。

请注意,传入 serialize 的任何 scope 值都需要是可序列化的,这意味着无法传递函数或组件。此外,scope 参数中的任何键名都必须是有效的 JavaScript 变量名。如果你需要传递不可序列化的自定义 scope,可以直接将 scope 传递给渲染时的 <MDXRemote />。上一节有一个如何做到这一点的示例。

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }
const data = { product: 'next' }

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} />
    </div>
  )
}

export async function getStaticProps() {
  // MDX 文本 - 可以来自本地文件、数据库或任何地方
  const source =
    'Some **mdx** text, with a component <Test product={product} />'
  const mdxSource = await serialize(source, { scope: data })
  return { props: { source: mdxSource } }
}
来自 MDXProvider 的自定义组件

如果你想让组件在应用程序中的任何 <MDXRemote /> 中可用,可以使用 @mdx-js/react 中的 <MDXProvider />

// pages/_app.jsx
import { MDXProvider } from '@mdx-js/react'

import Test from '../components/test'

const components = { Test }

export default function MyApp({ Component, pageProps }) {
  return (
    <MDXProvider components={components}>
      <Component {...pageProps} />
    </MDXProvider>
  )
}
// pages/test.jsx
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} />
    </div>
  )
}

export async function getStaticProps() {
  // MDX 文本 - 可以来自本地文件、数据库或任何地方
  const source = 'Some **mdx** text, with a component <Test />'
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}
带点的组件名(例如 motion.div

包含点(.)的组件名,如 framer-motion 中的组件,可以像其他自定义组件一样渲染,只需在组件对象中传递 motion

import { motion } from 'framer-motion'

import { MDXProvider } from '@mdx-js/react'
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={{ motion }} />
    </div>
  )
}

export async function getStaticProps() {
  // MDX 文本 - 可以来自本地文件、数据库或任何地方
  const source = `Some **mdx** text, with a component:

<motion.div animate={{ x: 100 }} />`
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}
延迟水合

延迟水合会推迟客户端上组件的水合。这是一种优化技术,可以改善应用程序的初始加载,但可能会导致 MDX 内容中任何动态内容的交互性出现意外延迟。

注意:这将在你渲染的 MDX 周围添加一个额外的包装 div,这是为了避免渲染过程中的水合不匹配所必需的。

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} lazy />
    </div>
  )
}

export async function getStaticProps() {
  // MDX 文本 - 可以来自本地文件、数据库或任何地方
  const source = 'Some **mdx** text, with a component <Test />'
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}

API

该库暴露了一个函数和一个组件,分别是 serialize<MDXRemote />。这两者被有意隔离在各自的文件中 —— serialize 旨在在服务器端运行,因此在 getStaticProps 中运行,它在服务器/构建时运行。而 <MDXRemote /> 则旨在在客户端,即浏览器中运行。

  • serialize(source: string, { mdxOptions?: object, scope?: object, parseFrontmatter?: boolean })

    serialize 接收一个 MDX 字符串。它还可以选择传入选项,这些选项直接传递给 MDX,以及一个可以包含在 MDX 作用域中的 scope 对象。该函数返回一个对象,旨在直接传递给 <MDXRemote />

serialize(
  // 原始MDX内容字符串
  '# hello, world',
  // 可选参数
  {
    // 可用于任何自定义MDX组件参数
    scope: {},
    // MDX的可用选项,更多信息请参见MDX文档
    // https://mdxjs.com/packages/mdx/#compilefile-options
    mdxOptions: {
      remarkPlugins: [],
      rehypePlugins: [],
      format: 'mdx',
    },
    // 指示是否从MDX源解析frontmatter
    parseFrontmatter: false,
  }
)

访问 https://mdxjs.com/packages/mdx/#compilefile-options 获取可用的 mdxOptions

  • <MDXRemote compiledSource={string} components?={object} scope?={object} lazy?={boolean} />

    <MDXRemote /> 使用 serialize 的输出以及可选的组件参数。其结果可以直接渲染到您的组件中。要延迟内容的水合并立即提供静态标记,请传递 lazy 属性。

    <MDXRemote {...source} components={components} />
    

替换默认组件

渲染将在底层使用 MDXProvider。这意味着您可以用自定义组件替换HTML标签。这些组件列在MDXJS的组件表中。

一个使用案例是使用您偏好的样式库渲染内容。

import { Typography } from "@material-ui/core";

const components = { Test, h2: (props) => <Typography variant="h2" {...props} /> }
...

如果您愿意,也可以用 <MDXProvider /> 包裹整个应用,而不是直接将组件传递给 <MDXRemote />。参见上面的示例

注意:由于组件名称中的 "/",th/td 将无法工作。

背景与理论

在Next.js应用中加载MDX文件没有一个很好的默认方式。之前,我们编写了 next-mdx-enhanced 以便能够将MDX文件渲染到布局中并导入其frontmatter来创建索引页。

next-mdx-enhanced 的这个工作流程还可以,但引入了一些限制,我们已在 next-mdx-remote 中移除了这些限制:

  • 文件内容必须是本地的。 您不能将MDX文件存储在另一个仓库、数据库等中。对于足够大的操作,最终会在创作内容的人和处理内容呈现的人之间产生分歧。在同一个仓库中重叠这两个关注点会使每个人的工作流程更加困难。
  • 您受限于基于文件系统的路由。 您的页面是根据其位置生成URL的。或者您可能使用 exportPathMap 重新映射它们,这会给作者带来困惑。无论如何,以任何方式移动页面都会破坏东西 -- 要么是页面的URL,要么是您的 exportPathMap 配置。
  • 您最终会遇到性能问题。 Webpack是一个JavaScript打包工具,强制它加载数百/数千页的文本内容会导致内存需求激增。Webpack将每个页面存储为具有大量元数据的独特对象。我们的一个实现中,仅有几百个页面就需要超过8GB的内存来编译网站。构建时间超过25分钟。
  • 您在构建关系数据的方式上会受到限制。 当您的整个数据结构是解析成JavaScript对象并保存在内存中的frontmatter时,将内容组织成动态的、相关的类别是困难的。

因此,next-mdx-remote 改变了整个模式,使您不是通过导入来加载MDX内容,而是通过 getStaticPropsgetServerProps 来加载 -- 您知道的,就像加载任何其他数据一样。该库提供了以性能良好的方式序列化和水合MDX内容的工具。这消除了上面列出的所有限制,而且成本显著降低 -- next-mdx-enhanced 是一个非常重的库,有大量自定义逻辑和一些令人烦恼的限制。我们的非正式测试显示构建时间减少了50%或更多。

自本项目最初创建以来,Kent C. Dodds制作了一个类似的项目,mdx-bundler。该库支持MDX文件中的导入和导出(只要您手动读取每个导入的文件并传递其内容),并自动处理frontmatter。如果您有很多文件都导入和使用不同的组件,您可能会受益于使用 mdx-bundler,因为 next-mdx-remote 目前只允许在所有页面中导入和使用组件。重要的是要注意,这个功能是有代价的 -- 对于基本的markdown内容,mdx-bundler 的输出至少比 next-mdx-remote 的输出大400%。

我如何用这个构建博客?

数据显示,所有开发者工具用例的99%都是构建不必要的复杂个人博客。开玩笑的。但说真的,如果您试图为个人或小型企业使用构建博客,请考虑只使用普通的HTML和CSS。您绝对不需要使用重量级的全栈JavaScript框架来制作一个简单的博客。当您几年后回来进行更新时,您会感谢自己的,因为届时您所有的依赖项不会有10个破坏性的发布。

如果您真的坚持要这样做,请查看我们官方的Next.js示例实现。💖

注意事项

环境目标

next-mdx-remote 生成的用于实际渲染MDX的代码针对支持模块的浏览器。如果您需要支持较旧的浏览器,请考虑转译 serializecompiledSource 输出。

import / export

importexport 语句不能在MDX文件内部使用。如果您需要在MDX文件中使用组件,应该将它们作为prop提供给 <MDXRemote />

希望这是有意义的,因为为了工作,导入必须相对于文件路径,而这个库允许从任何地方加载内容,而不是只从设定的文件路径加载本地内容。至于导出,MDX内容被视为数据,而不是模块,所以我们无法访问可能从传递给 next-mdx-remote 的MDX中导出的任何值。

安全性

这个库在客户端评估JavaScript字符串,这是它MDXRemotes MDX内容的方式。如果不小心处理,将字符串评估为javascript可能是一种危险的做法,因为它可能启用XSS攻击。重要的是要确保您只将 serialize 函数生成的 mdxSource 输入传递给 <MDXRemote />,如文档中所指示。不要将用户输入传递给 <MDXRemote />

如果您的网站有禁止通过 evalnew Function() 进行代码评估的CSP,您需要放宽该限制以使用 next-mdx-remote,这可以通过使用 unsafe-eval 来完成。

TypeScript

这个项目确实包含TypeScript使用的原生类型。serialize<MDXRemote /> 都有正常预期的类型,该库还导出了一个可用于为 getStaticProps 的结果添加类型的类型。

  • MDXRemoteSerializeResult<TScope = Record<string, unknown>>:表示 serialize 的返回值。可以传递 TScope 泛型类型来表示您传入的作用域数据的类型。

以下是TypeScript中简单实现的示例。对于TypeScript的每种配置,您可能不需要完全按照这种方式实现类型 - 这个示例只是演示在需要时可以应用类型的位置。

import type { GetStaticProps } from 'next'
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote, type MDXRemoteSerializeResult } from 'next-mdx-remote'
import ExampleComponent from './example'

const components = { ExampleComponent }

interface Props {
  mdxSource: MDXRemoteSerializeResult
}

export default function ExamplePage({ mdxSource }: Props) {
  return (
    <div>
      <MDXRemote {...mdxSource} components={components} />
    </div>
  )
}

export const getStaticProps: GetStaticProps<{
  mdxSource: MDXRemoteSerializeResult
}> = async () => {
  const mdxSource = await serialize('some *mdx* content: <ExampleComponent />')
  return { props: { mdxSource } }
}

React Server Components (RSC) 和 Next.js app 目录支持

在服务器组件中使用 next-mdx-remote,特别是在Next.js的 app 目录中,可以通过从 next-mdx-remote/rsc 导入来支持。之前,序列化和渲染步骤是分开的,但是从现在开始,RSC使这种分离变得不必要。

一些值得注意的区别:

  • <MDXRemote /> 现在接受一个 source 属性,而不是接受来自 next-mdx-remote/serialize 的序列化输出
  • 不能再通过使用 @mdx-js/reactMDXProvider 上下文来提供自定义组件,因为RSC不支持React上下文
  • 要在传递 parseFrontmatter: true 时在MDX外部访问frontmatter,请使用 next-mdx-remote/rsc 导出的 compileMdx 方法
  • 不再支持 lazy 属性,因为渲染发生在服务器上
  • <MDXRemote /> 必须在服务器上渲染,因为它现在是一个异步组件。客户端组件可以作为MDX标记的一部分渲染

有关RSC的更多信息,请查看 Next.js文档

示例

假设在使用 app 目录的Next.js 13+应用程序中使用。

基本

import { MDXRemote } from 'next-mdx-remote/rsc'

// app/page.js
export default function Home() {
  return (
    <MDXRemote
      source={`# Hello World

      This is from Server Components!
      `}
    />
  )
}

加载状态

import { MDXRemote } from 'next-mdx-remote/rsc'

// app/page.js
export default function Home() {
  return (
    // 理想情况下,这个加载旋转器应确保没有布局偏移,
    // 这是如何提供这样一个加载旋转器的示例。
    // 在Next.js中,您也可以使用 `loading.js` 来实现这一点。
    <Suspense fallback={<>Loading...</>}>
      <MDXRemote
        source={`# Hello World

        This is from Server Components!
        `}
      />
    </Suspense>
  )
}

自定义组件

// components/mdx-remote.js
import { MDXRemote } from 'next-mdx-remote/rsc'

const components = {
  h1: (props) => (
    <h1 {...props} className="large-text">
      {props.children}
    </h1>
  ),
}

export function CustomMDX(props) {
  return (
    <MDXRemote
      {...props}
      components={{ ...components, ...(props.components || {}) }}
    />
  )
}
// app/page.js
import { CustomMDX } from '../components/mdx-remote'

export default function Home() {
  return (
    <CustomMDX
      // h1 现在使用 `large-text` 类名渲染
      source={`# 你好世界
      这是来自服务器组件的内容!
    `}
    />
  )
}

在 MDX 外部访问前置元数据

// app/page.js
import { compileMDX } from 'next-mdx-remote/rsc'

export default async function Home() {
  // 可选择为前置元数据对象提供类型
  const { content, frontmatter } = await compileMDX<{ title: string }>({
    source: `---
title: RSC 前置元数据示例
---
# 你好世界
这是来自服务器组件的内容!
`,
    options: { parseFrontmatter: true },
  })
  return (
    <>
      <h1>{frontmatter.title}</h1>
      {content}
    </>
  )
}

替代方案

next-mdx-remote 对其支持的功能有明确的观点。如果你需要 next-mdx-remote 未提供的额外功能,以下是一些可以考虑的替代方案:

你可能不需要 next-mdx-remote

如果你正在使用 React 服务器组件,并且只是想使用带有自定义组件的基本 MDX,你只需要核心的 MDX 库,不需要其他东西。

import { compile, run } from '@mdx-js/mdx'
import * as runtime from 'react/jsx-runtime'
import ClientComponent from './components/client'

// MDX 可以从任何地方获取,比如文件或数据库。
const mdxSource = `# 你好,世界!
<ClientComponent />
`

export default async function Page() {
  // 将 MDX 源代码编译为函数体
  const code = String(
    await compile(mdxSource, { outputFormat: 'function-body' })
  )
  // 然后你可以在服务器上运行代码,生成一个服务器
  // 组件,或者你可以将字符串传递给客户端组件进行
  // 最终渲染。

  // 使用运行时运行编译后的代码并获取默认导出
  const { default: MDXContent } = await run(code, {
    ...runtime,
    baseUrl: import.meta.url,
  })

  // 渲染 MDX 内容,提供 ClientComponent 作为组件
  return <MDXContent components={{ ClientComponent }} />
}

如果你不打算将编译后的字符串传递给数据库或客户端组件,你也可以使用 evaluate 来简化这个过程,它可以在一次调用中编译和运行代码。

import { evaluate } from '@mdx-js/mdx'
import * as runtime from 'react/jsx-runtime'
import ClientComponent from './components/client'

// MDX 可以从任何地方获取,比如文件或数据库。
const mdxSource = `
export const title = "MDX 导出演示";

# 你好,世界!
<ClientComponent />

export function MDXDefinedComponent() {
  return <p>MDX 定义的组件</p>;
}
`

export default async function Page() {
  // 运行编译后的代码
  const {
    default: MDXContent,
    MDXDefinedComponent,
    ...rest
  } = await evaluate(mdxSource, runtime)

  console.log(rest) // 输出 { title: 'MDX 导出演示' }

  // 渲染 MDX 内容,提供 ClientComponent 作为组件,以及
  // 导出的 MDXDefinedComponent。
  return (
    <>
      <MDXContent components={{ ClientComponent }} />
      <MDXDefinedComponent />
    </>
  )
}

许可证

Mozilla 公共许可证 2.0 版

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号