在 Next.js 中集成 ReactMD 主题库:with-react-md-typescript 示例深度解析
本指南以仓库中的 with-react-md-typescript 官方示例为骨架,讲解如何在 Next.js + TypeScript 应用中接入 ReactMD(React Material Design)组件库。你将学会:用 SCSS 变量定制品牌主题、依据系统「深色模式」偏好动态切换主题、借助自定义 _app.tsx 建立持久化布局,以及把 Next.js 路由无缝接入 ReactMD 的响应式导航布局,最终在纯组件与全路由导航间建立一套可复用的工程模板。
示例概述与目录结构
本示例构建了一个最小的 ReactMD + Next.js + TypeScript 应用,核心能力包括:
- _variables.scss 覆盖 ReactMD 默认主题与功能开关
- app.scss 依据用户操作系统偏好条件式应用暗色主题
- 自定义 _app.tsx 使用持久化布局
- 可复用的 Layout.tsx,它将全部图标替换为
SVGIcon,并用导航项初始化 ReactMD 的Layout组件
仓库中该示例的完整文件布局如下:
examples/with-react-md-typescript/
├── components/
│ ├── Layout/
│ │ ├── Layout.tsx # react-md Layout 封装 + 图标配置
│ │ ├── index.ts # 便捷导出
│ │ └── navItems.tsx # 导航树定义
│ └── LinkUnstyled.tsx # 无样式 next/link 封装
├── pages/
│ ├── _app.tsx # 根应用组件,引入全局样式与 Layout
│ ├── index.tsx # 首页
│ └── route-1.tsx # 第二路由页
├── styles/
│ ├── _variables.scss # react-md 主题变量覆写
│ └── app.scss # 全局样式入口
├── package.json
├── tsconfig.json
└── README.md
依赖配置:package.json 与 tsconfig
示例的 package.json 表明它需要三类依赖:next、react/react-dom、react-md,开发期还需要 typescript 与 sass(编译 SCSS)。react-md 依赖版本声明为 ^2.1.1,这是使用其主题系统与 Layout/Configuration 组件的前提。
"dependencies": {
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-md": "^2.1.1"
},
"devDependencies": {
"@types/node": "...",
"@types/react": "...",
"sass": "^1.32.4",
"typescript": "^3.9.7"
}
脚本保持 Next.js 惯例:next dev(开发)、next build(构建)、next start(生产启动)。值得注意的关键点是,ReactMD 主题需要 SCSS 预处理器,因此 sass 是必要的 devDependency;Next.js 内置对 Sass 的支持,无需额外配置 webpack 即可直接 @import ReactMD 的样式文件。
tsconfig.json 采用 jsx: "react-jsx"、esModuleInterop、isolatedModules 等标准设置,include 覆盖 **/*.ts 与 **/*.tsx,noEmit 开启——类型仅用于检查,实际输出由 Next.js/SWC 完成。
用 SCSS 变量定制 ReactMD 主题
ReactMD 的主题体系基于 SCSS 变量。示例将其所有覆写集中在 styles/_variables.scss 中。由于文件名以下划线开头,该文件是纯「部分文件」,只被导入而不直接生成 CSS。
@import "~@react-md/theme/dist/color-palette";
$rmd-theme-primary: $rmd-purple-500;
$rmd-theme-secondary: $rmd-pink-a-200;
$rmd-theme-light: true;
$rmd-theme-dark-elevation: "prefers-color-scheme";
$rmd-utils-auto-dense: false;
逐行解读:
- 首先
@import "~@react-md/theme/dist/color-palette"拉入 ReactMD 内置的调色板,之后才能使用$rmd-purple-500、$rmd-pink-a-200这样的预置色值变量。 $rmd-theme-primary与$rmd-theme-secondary设置品牌主色与次色。ReactMD 遵循 Material Design 色阶命名,如-500为主色调常用档位、-a-200为强调(accent)档位,可替换为你自己的品牌色。$rmd-theme-light: true声明主题基础为浅色。$rmd-theme-dark-elevation: "prefers-color-scheme"是关键字符串开关:它让 ReactMD 在暗色模式下使用「真实高度(elevation)」阴影体系,即通过媒体查询感知系统的prefers-color-scheme来决定暗色表面呈现。$rmd-utils-auto-dense: false关闭「紧凑密度」辅助样式,维持默认 Material 密度。
这里还预留了注释位置,可继续追加其他 react-md 覆写变量或应用内共享的全局 SCSS 变量。这种「变量部分文件 + 主样式文件」的分层方式,让主题定制与页面样式解耦,便于维护与多主题扩展。
全局样式与系统级暗色主题
主样式文件 app.scss 仅有三段逻辑,是整个主题生效的入口:
// import react-md variable overrides
@import "./variables";
// generate all react-md styles with the custom theme
@import "react-md/dist/styles";
@media (prefers-color-scheme: dark) {
:root {
@include rmd-theme-dark;
}
}
执行顺序很关键:
- 先
@import "./variables"加载上一步的主题覆写变量; - 再
@import "react-md/dist/styles"——此时 ReactMD 会基于已覆写的变量一次性生成全部组件样式,因此变量覆写必须发生在 styles 导入之前,否则不生效; - 最后用原生媒体查询监听操作系统深色偏好,命中时在
:root上@include rmd-theme-dark,将 CSS 变量切换为暗色主题值。
这一做法与示例文件中所述「依据用户 OS 偏好条件式应用暗色主题」完全一致。它无需在 JS 侧做任何运行时判断,属于纯 CSS 层面的主题切换方案。
自定义 _app.tsx:持久化布局入口
Next.js 的 Pages Router 中,_app.tsx 是所有页面共享的根组件,适合放置全局样式与布局。示例的 pages/_app.tsx 结构如下:
import React, { ReactElement } from "react";
import Head from "next/head";
import { AppProps } from "next/app";
import Layout from "../components/Layout";
import "../styles/app.scss";
export default function App({ Component, pageProps }: AppProps): ReactElement {
return (
<Layout>
<Head>
<title>react-md with next.js</title>
</Head>
<Component {...pageProps} />
</Layout>
);
}
要点:
- 在文件顶层
import "../styles/app.scss",确保全局主题样式在任何页面渲染前被引入; - 用
<Layout>包裹<Component {...pageProps} />,让侧边栏、顶部应用栏、导航抽屉在路由切换时保持挂载(持久化布局),而非每个页面各自重复渲染外壳; - 通过
next/head设置页面标题。注意AppProps由next/app提供,返回类型标注为ReactElement以保持类型安全。
Layout 组件:Configuration、SVG 图标与导航树
Layout.tsx 是本示例的核心组合点。它同时完成三件事:注入 SVG 图标集、把 Next.js 路由映射为 ReactMD 导航树、组合出响应式布局。
将 FontIcon 全部替换为 SVGIcon
ReactMD 默认使用字体图标(FontIcon),示例通过 ConfigurableIcons 类型一次性为 react-md 内部的各个「图标槽位」注入对应 SVG 组件:
const icons: ConfigurableIcons = {
back: <KeyboardArrowLeftSVGIcon />,
checkbox: <CheckBoxSVGIcon />,
dropdown: <ArrowDropDownSVGIcon />,
download: <FileDownloadSVGIcon />,
expander: <KeyboardArrowDownSVGIcon />,
forward: <KeyboardArrowRightSVGIcon />,
menu: <MenuSVGIcon />,
notification: <NotificationsSVGIcon />,
radio: <RadioButtonCheckedSVGIcon />,
password: <RemoveRedEyeSVGIcon />,
selected: <CheckSVGIcon />,
sort: <ArrowUpwardSVGIcon />,
};
这份映射覆盖了菜单按钮、下拉箭头、复选/单选标记、通知、密码可见性切换等常见语义位。选用 SVG 图标的收益是:矢量化缩放更清晰、颜色可随主题 CSS 变量变化、避免额外下载字体文件。
Configuration 与 Layout 组合
export default function Layout({ children }: LayoutProps): ReactElement {
const { pathname } = useRouter();
return (
<Configuration icons={icons}>
<RMDLayout
title="Main Title"
navHeaderTitle="Navigation Header Title"
tabletLayout="temporary"
landscapeTabletLayout="temporary"
desktopLayout="temporary"
largeDesktopLayout="temporary"
treeProps={useLayoutNavigation(navItems, pathname, LinkUnstyled)}
>
{children}
</RMDLayout>
</Configuration>
);
}
<Configuration icons={icons}>是 ReactMD 的应用级配置容器,将图标映射下发给整个组件树;<RMDLayout>接受title(应用栏标题)与navHeaderTitle(导航头部标题);tabletLayout、landscapeTabletLayout、desktopLayout、largeDesktopLayout分别对应平板、横屏平板、桌面、大桌面四个断点。全部设为"temporary"表示导航抽屉为「临时抽屉」:默认收起,需点击菜单按钮展开,点击遮罩或选中项后关闭——适合中小型导航;- 导入时对
Layout作了别名处理(Layout as RMDLayout),避免与本地导出的Layout组件同名冲突。
useLayoutNavigation:把 Next.js 路由变成导航树
useLayoutNavigation(navItems, pathname, LinkUnstyled) 返回一组供 RMDLayout 的 treeProps 消费的 props。它接收三个参数:导航树数据、当前路由路径名(来自 useRouter())、以及渲染链接时使用的自定义链接组件。ReactMD 据此自动完成「当前激活项高亮」「展开对应父级」「点击导航触发路由」等行为。
导航树在 navItems.tsx 中定义:
function createRoute(
pathname: string,
children: string,
leftAddon: ReactNode | undefined,
parentId: string | null = null,
): LayoutNavigationItem {
return {
itemId: pathname,
parentId,
href: pathname,
children,
leftAddon,
};
}
const navItems: LayoutNavigationTree = {
"/": createRoute("/", "Home", <HomeSVGIcon />),
"/route-1": createRoute("/route-1", "Route 1", <TvSVGIcon />),
};
源码注释强调了两个关键约定:
parentId必须默认为null,因为导航树底层基于@react-md/tree包实现;parentId为null的条目才出现在导航树根层;itemId即路由路径本身,保证「路径 ↔ 树节点」一一映射,当前路由对应节点会被识别为激活状态。
createRoute 是定义树条目的简写工厂:children 是显示文本,leftAddon 是行首图标,还可通过 parentId 构建多层嵌套的父子导航(本示例仅两层入口,未嵌套)。leftAddon 类型为 ReactNode,因此传 SVGIcon、FontIcon、Avatar 均可。
为什么需要 LinkUnstyled
LinkUnstyled.tsx 是将 next/link 包装成无样式链接的适配组件:
export default function LinkUnstyled({ as, href, scroll, shallow, replace, children, ...props }: LinkUnstyledProps): ReactElement {
if (typeof href === "string" && href.startsWith("http")) {
// external links
return (
<a {...props} href={href} rel="noopener noreferrer">
{children}
</a>
);
}
return (
<Link shallow={shallow} scroll={scroll} replace={replace} href={href} as={as} {...props}>
{children}
</Link>
);
}
由于 ReactMD 的 Layout 导航渲染 <a> 链接,若直接交给原生 <a> 会导致整页刷新、破坏 SPA 体验。该封装让导航树内部走 next/link 的客户端路由跳转;同时做了一层防御:当 href 以 http 开头(外部链接)时回退为原生 <a> 并附加 rel="noopener noreferrer"。其 props 类型通过 Omit<LinkProps, "children" | "onError"> 与 Omit<AnchorHTMLAttributes<HTMLAnchorElement>, ...> 交叉得到,兼顾 next/link 与原生锚点的合法属性。
页面示例与路由联动
两个页面都只使用 ReactMD 的排版组件,验证布局与主题确实作用于各路由:
// pages/index.tsx
export default function Home(): ReactElement {
return (
<TextContainer>
<Text type="headline-4">Hello, world!</Text>
</TextContainer>
);
}
pages/route-1.tsx 结构相同,仅文案为 "Route 1"。TextContainer 提供居中的内容宽度约束,Text type="headline-4" 选用 Material 排版层级。当你点击导航抽屉中的 "Route 1" 时,Next.js 在 Layout 内无缝切换页面组件,同时 ReactMD 依据 pathname 自动更新导航高亮——这即是持久化布局 + 路由感知导航的整体效果。Layout/index.ts 中的 export { default } from "./Layout" 属于目录导出习惯,让上层能以 ../components/Layout 简洁引用。
快速启动:从官方示例创建应用
ReactMD 官方文档与示例站源码提供了更多特性、样式、组件与 API 说明。本示例支持一键部署,也支持通过 create-next-app 引导:
npx create-next-app --example with-react-md-typescript with-react-md-typescript-app
使用 Yarn 时:
yarn create next-app --example with-react-md-typescript with-react-md-typescript-app
使用 pnpm 时:
pnpm create next-app --example with-react-md-typescript with-react-md-typescript-app
上述命令会克隆 examples/with-react-md-typescript 目录到一个名为 with-react-md-typescript-app 的新项目中。进入项目后依次执行 npm install、npm run dev,即可在开发服务器中查看带紫色主题与系统级暗色适配的示例页面。也可以直接把它部署到云端(如 Vercel)。
工程要点小结
对照本示例可沉淀出几条可直接复用的集成经验:
- 变量覆写必须先于样式生成:
_variables.scss中所有$rmd-*覆写必须在@import "react-md/dist/styles"之前,否则主题不生效;保持二者分离便于多主题场景切换。 - 暗色模式可纯 CSS 实现:
$rmd-theme-dark-elevation: "prefers-color-scheme"配合@media (prefers-color-scheme: dark)中的rmd-theme-darkmixin,即可在不写任何运行时 JS 的情况下响应系统深色偏好。 - 持久化布局提升路由体验:把
Configuration与RMDLayout放到_app.tsx的外层,让应用栏与导航抽屉跨路由保持,避免重复挂载开销。 - 路由感知导航需要桥接:
useLayoutNavigation+LinkUnstyled是「ReactMD 导航树 ↔ Next.js 客户端路由」之间的桥梁,务必用next/link渲染树内链接以免整页刷新,并注意树条目的parentId默认值必须为null。 - 以 SVG 图标替代字体图标:通过
ConfigurableIcons全局注入,能获得更好的缩放质量与主题一致性。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00