CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Next.js 实战入门/课时/MDX 与课时渲染
RawBlame
课时目录12 节

Next.js 实战入门

  1. 01Next.js 是什么
  2. 02项目创建与目录
  3. 03App Router 与文件路由
  4. 04Server 与 Client 组件
  5. 05数据获取与缓存
  6. 06渲染模式 SSR / SSG / ISR
  7. 07Route Handlers
  8. 08MDX 与课时渲染
  9. 09Tailwind 与样式
  10. 10认证与登录门控
  11. 11读 platform 源码
  12. 12综合练习与部署

MDX 与课时渲染

next-mdx-remote、GFM、本站课时页如何渲染 Markdown。

30 分钟

MDX 与课时渲染

这一课你将能做到

  1. 说清 Markdown 与 MDX 的关系:MDX = Markdown + 可嵌入的 React 组件
  2. 在 Next.js 里配置 @next/mdx,写出带自定义组件的 .mdx 页面
  3. 理解本站课时内容「服务端读源 → 渲染 → 消毒 → 输出」的完整管线

为什么要学

做内容型产品(文档站、课程平台、博客)绕不开一个问题:内容用什么格式写、怎么安全地渲染成页面?Markdown 是事实标准——写作友好、程序员都会;但它有个天花板:只能表达文字排版,表达不了交互。想在一篇文章里嵌一个可运行的代码沙盒、一个折叠面板、一道点击出答案的练习题,纯 Markdown 做不到。

MDX 补上这块:在 Markdown 里直接写 JSX,内容作者可以像用 HTML 标签一样用你封装好的 React 组件。本学习平台的课时内容就是按这条思路处理的——你此刻正在读的这篇课文的排版结构,最终都由 React 组件渲染出来;平台还支持课时里嵌练习与测验组件,这正是 MDX 的能力域。

面试场景虽不如 RSC 高频,但「Markdown 渲染的安全问题(XSS)」是个好的加分话题——任何用户可控内容进 HTML 都要消毒(sanitize),本站专门有 mdxSanitize 模块处理这件事,说明这不是纸上谈兵。

先建立图像

Markdown 与 MDX 的对比:

普通 Markdown(.md)                MDX(.mdx)
# 标题                              # 标题
- 列表                              - 列表
**加粗**                            **加粗**

❌ 不能写组件                        ✅ 可以写组件:
                                    <Callout type="warn">注意!</Callout>
                                    <Quiz question="1+1=?" answer="2" />

在 Next.js 生态里的位置:

写作源文件           Next.js 渲染管线              用户看到
.mdx 文件    ──→   @next/mdx 编译        ──→    页面(编译期,构建时确定)
─────────────
数据库/API 里的 markdown 字符串 ──→ 动态渲染库 + sanitize ──→ 页面(运行时)
(如本站后端下发的 contentMd)

两种来源决定两种技术路线,本课两条都讲:

内容来源技术路线特点
仓库里的 .mdx 文件@next/mdx 官方方案编译进构建,类型安全,改内容要重新部署
后端/API 下发的字符串运行时渲染(如 next-mdx-remote 或自研管线)内容可后台随时更新,必须做安全消毒

本站属于第二种:Spring Boot 后端把课时 contentMd 下发给前端,Next.js 服务端渲染成 React 元素。

跟着做

步骤 1:安装并配置 @next/mdx

npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

修改 next.config.ts(或新建 next.config.mjs):

import type { NextConfig } from "next";
import createMDX from "@next/mdx";

const nextConfig: NextConfig = {
  pageExtensions: ["ts", "tsx", "md", "mdx"], // 让 Next 把 .mdx 也当页面
};

const withMDX = createMDX({
  // 可加 remark/rehype 插件,如 remarkGfm(表格、删除线)
  // options: { remarkPlugins: [remarkGfm] },
});

export default withMDX(nextConfig);

关键点:pageExtensions 加上 mdx 后,app/about/page.mdx 就能直接当页面用——和 page.tsx 同一地位。

步骤 2:写第一个 MDX 页面

新建 src/app/mdx-demo/page.mdx:

# 我的第一个 MDX 页面

这是普通 **Markdown** 语法,一切照旧。

## 但是,可以插入组件

import Callout from "@/components/Callout";

<Callout type="warn">
  这是 React 组件渲染出来的警告框,不是引用块。
</Callout>

配套组件 src/components/Callout.tsx:

const styles = {
  info: "border-blue-500 bg-blue-50 text-blue-800",
  warn: "border-amber-500 bg-amber-50 text-amber-800",
} as const;

export default function Callout({
  type = "info",
  children,
}: {
  type?: "info" | "warn";
  children: React.ReactNode;
}) {
  return (
    <div className={`my-4 rounded border-l-4 p-3 text-sm ${styles[type]}`}>
      {children}
    </div>
  );
}

访问 /mdx-demo:标题、加粗是 Markdown 渲染的,警告框是 React 组件渲染的——两种内容在一页和谐共处。

步骤 3:覆盖默认元素映射(mdx-components)

MDX 里所有 Markdown 元素(h1、p、code…)默认渲染成原生标签。想全局换成自己的组件(比如统一代码块样式),在项目根目录建 mdx-components.tsx:

import type { MDXComponents } from "mdx/types";

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    h2: (props) => (
      <h2 className="mt-8 mb-3 text-xl font-bold border-b pb-1" {...props} />
    ),
    code: (props) => (
      <code className="rounded bg-gray-100 px-1 py-0.5 text-sm" {...props} />
    ),
    ...components,
  };
}

从此所有 .mdx 页面的 ## 标题 都自动带上统一样式——内容作者完全无感,样式收敛到组件层。这就是「内容与样式分离」的 MDX 版本。

步骤 4:运行时渲染字符串(本站路线)

后端下发的 markdown 是字符串,没法走编译路线,需要运行时渲染。以社区方案 next-mdx-remote-client 思路演示核心(重点看消毒环节):

npm install next-mdx-remote-client
// src/components/MdxRenderer.tsx(Server 组件)
import { MDXRemote } from "next-mdx-remote-client/rsc";
import rehypeSanitize from "rehype-sanitize";
import Callout from "@/components/Callout";

const components = { Callout }; // 开放给内容的组件白名单

export default async function MdxRenderer({ source }: { source: string }) {
  return (
    <MDXRemote
      source={source}
      components={components}
      rehypePlugins={[[rehypeSanitize, { tagNames: ["p", "h2", "h3", "code", "pre"] }]]}
    />
  );
}

页面中使用:

// src/app/lesson/[slug]/page.tsx
import MdxRenderer from "@/components/MdxRenderer";

export default async function LessonPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  // 真实项目:fetch 后端 /api/lessons/{slug} 拿 contentMd
  const contentMd = `# 课时标题\n\n正文 **加粗** 与 <Callout>提示</Callout>`;
  return (
    <article className="prose max-w-none p-6">
      <MdxRenderer source={contentMd} />
    </article>
  );
}

消毒是生死线:如果内容里有 <script>alert(1)</script> 或 <img onerror=...>,不消毒直接渲染就是存储型 XSS。方案二选一:渲染前用 rehype-sanitize 这类白名单过滤;或像本站一样先对源做 sanitize(mdxSanitize 模块)再进渲染管线。用户可控内容(评论、社区发帖)100% 需要这条防线。

步骤 5:给内容配「元数据」(frontmatter)

内容通常需要标题、作者、标签这类结构化信息,写在文件头:

---
title: Next.js 数据获取
order: 5
tags: [fetch, cache]
---

正文从这里开始……

@next/mdx 本身不解析 frontmatter,社区常用 @next/mdx + remark-frontmatter 导出,或最简单的方式:内容走 CMS/后端,元数据由后端字段提供(本站的做法:title、orderIndex、contentMd 都是 API 返回的独立字段,不做 frontmatter)。两条路线取舍:文件驱动选 frontmatter,数据库驱动选 API 字段。

讲清楚要点

  1. MDX 的价值 = 内容里长出交互:练习题组件、折叠答案、在线运行沙盒,这些「课程产品的差异化体验」全靠它。纯文档可以只用 Markdown,内容型平台几乎都会走到 MDX。
  2. 两条渲染路线按内容来源选:仓库内文件走 @next/mdx(编译期,快而稳);API/数据库字符串走运行时渲染库(灵活,可后台更新)。混用的判据就一条:内容改了要不要重新部署。
  3. 组件白名单思维:永远不把「任意组件」开放给内容。components 映射表里放什么,内容里才能用什么——这是能力边界,也是安全边界。本站渲染课时内容时对可嵌组件有严格白名单。
  4. 消毒(sanitize)必须先于渲染:rehype-sanitize 或等价白名单过滤,过滤对象是标签与属性(script、iframe、onerror 全拦)。记住判断口诀:内容来源是用户可控的(后端数据、UGC)→ 必消毒;来源是仓库里可信作者写的 .mdx → 可免。
  5. 样式注入点有三层:mdx-components.tsx 全局覆盖默认元素;components 传参提供业务组件;外层容器(如 Tailwind 的 prose 类)控制排版基调。本站课时页的排版就由容器 + 映射共同完成。

你可能会懵的地方

  • MDX 和 Markdown-It / Marked 这类库什么关系? 那些是纯 Markdown→HTML 转换器,遇到 JSX 只会当文本或转义。MDX 用的是 MDX 编译器(@mdx-js),能把 JSX 编译成 React 组件调用。功能上是超集关系,选型看要不要组件。

  • 课时内容放数据库还是放文件? 内容频繁更新、多角色管理(编辑/审核)→ 数据库 + 后台(本站模式);内容随代码走、版本控制天然合并 → 仓库 .mdx 文件(开源文档常见模式)。技术上渲染管线几乎一致。

  • 为什么 MDX 组件不用再写 'use client'? @next/mdx 页面默认按 Server 组件渲染;只有引入了状态组件(如带点击计数的练习)时,那一个组件文件自己标 'use client' 即可。内容主体保持服务端渲染,SEO 与首屏不受影响。

  • rehype 和 remark 插件有什么区别? 处理管线的两个阶段:remark 操作 Markdown 语法树( remarkGfm 加表格支持、remark-frontmatter 解析元数据),rehype 操作 HTML 语法树(rehypeSanitize 消毒、rehype-highlight 代码高亮)。记忆:remark 管「写什么」,rehype 管「出什么 HTML」。

  • 消毒会把我的自定义组件也过滤掉吗? 顺序和层位是关键:sanitize 过滤的是「原始 HTML 标签与属性」,你的自定义组件在 MDX 编译层就已经变成组件调用了。但要小心内容里裸写的 HTML——这正是需要白名单的地方。把「组件白名单」和「HTML 白名单」分开管理。

错法对照

错法问题正解
忘记配 pageExtensions: ["mdx"].mdx 文件不被当页面,404next.config 里加上扩展名
mdx-components.tsx 放进 src/ 或 app/ 里官方约定必须项目根目录根目录(与 package.json 同级)
直接 dangerouslySetInnerHTML 渲染后端 markdown存储型 XSS 高危入口运行时 MDX 渲染 + rehype-sanitize
对用户 UGC 内容免消毒渲染脚本注入无条件消毒,白名单收紧
在内容里放任意导入(import 外部包)编译期执行不可信代码,供应链风险编译路线仅限可信仓库;运行时路线用组件白名单

卡点急救

  1. /mdx-demo 404:检查 next.config 里 pageExtensions 是否含 mdx;改完配置要重启 dev;文件确实叫 page.mdx。
  2. <Callout> 渲染成了普通文字:组件没进「映射」——编译路线检查页面里的 import 是否写在 MDX 内;运行时路线检查 components 传参。名字大小写也要完全一致。
  3. 改了 mdx-components.tsx 样式没生效:该文件只在编译路线(@next/mdx 页面)生效;运行时渲染的字符串要用渲染库自己的 components 参数传。两条路线的映射互不相通。

面试常问

  • Markdown 内容在 Next.js 里怎么渲染成页面?安全上注意什么? 按来源分两条路:仓库内 .mdx 用 @next/mdx 编译成页面,元素样式通过 mdx-components 映射统一接管;API 下发的字符串用运行时 MDX 渲染组件(如 next-mdx-remote)。安全核心是消毒:用户可控内容必须过 rehype-sanitize 白名单(拦 script/iframe/事件属性),同时只开放受信组件白名单,禁止内容侧任意 import。

  • MDX 相比纯 Markdown 的优势与代价? 优势:内容中可嵌入 React 组件实现交互(练习、演示、图表),样式可全局映射收敛。代价:内容耦合了组件 API(作者要懂可用组件清单)、编译链更重、运行时渲染需要更严的安全控制。内容型产品(课程、文档站)通常值得,纯博客未必。

  • 你们的课时内容是怎么从数据库到页面的? 参考回答:Spring Boot 返回课时对象(title/orderIndex/contentMd 等字段),Next.js 课时页(Server Component)拿到 contentMd 后走「消毒 → MDX 运行时渲染 → 组件映射」管线输出 React 元素,渲染在服务端完成保证首屏与 SEO;练习、测验等交互块以白名单组件形式嵌入,客户端水合后可交互。

自测三问

  1. 仓库 .mdx 文件和后端下发的 markdown 字符串,分别走哪条渲染路线?
  2. 为什么运行时渲染必须消毒?白名单要管住哪两类东西?
  3. mdx-components.tsx 和渲染时的 components 参数各管什么?

小结一句话

MDX 让内容长出交互:编译路线管仓库文件、运行时路线管接口字符串,共同的底线是组件白名单与 HTML 消毒。

延伸

  • Next.js 官方文档:MDX
  • 下一课:Tailwind 与样式体系——让这些组件好看起来。
加载本节练习…
← 上一课Route Handlers下一课 →Tailwind 与样式