首页
/ 在 Next.js 中集成 ReactMD 主题库:with-react-md-typescript 示例深度解析

在 Next.js 中集成 ReactMD 主题库:with-react-md-typescript 示例深度解析

2026-09-07 09:00:39作者:牧宁李

本指南以仓库中的 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 表明它需要三类依赖:nextreact/react-domreact-md,开发期还需要 typescriptsass(编译 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"esModuleInteropisolatedModules 等标准设置,include 覆盖 **/*.ts**/*.tsxnoEmit 开启——类型仅用于检查,实际输出由 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;
  }
}

执行顺序很关键:

  1. @import "./variables" 加载上一步的主题覆写变量;
  2. @import "react-md/dist/styles" ——此时 ReactMD 会基于已覆写的变量一次性生成全部组件样式,因此变量覆写必须发生在 styles 导入之前,否则不生效;
  3. 最后用原生媒体查询监听操作系统深色偏好,命中时在 :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 设置页面标题。注意 AppPropsnext/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(导航头部标题);
  • tabletLayoutlandscapeTabletLayoutdesktopLayoutlargeDesktopLayout 分别对应平板、横屏平板、桌面、大桌面四个断点。全部设为 "temporary" 表示导航抽屉为「临时抽屉」:默认收起,需点击菜单按钮展开,点击遮罩或选中项后关闭——适合中小型导航;
  • 导入时对 Layout 作了别名处理(Layout as RMDLayout),避免与本地导出的 Layout 组件同名冲突。

useLayoutNavigation:把 Next.js 路由变成导航树

useLayoutNavigation(navItems, pathname, LinkUnstyled) 返回一组供 RMDLayouttreeProps 消费的 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 包实现;parentIdnull 的条目才出现在导航树根层;
  • itemId 即路由路径本身,保证「路径 ↔ 树节点」一一映射,当前路由对应节点会被识别为激活状态。

createRoute 是定义树条目的简写工厂:children 是显示文本,leftAddon 是行首图标,还可通过 parentId 构建多层嵌套的父子导航(本示例仅两层入口,未嵌套)。leftAddon 类型为 ReactNode,因此传 SVGIconFontIconAvatar 均可。

为什么需要 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 的客户端路由跳转;同时做了一层防御:当 hrefhttp 开头(外部链接)时回退为原生 <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 installnpm run dev,即可在开发服务器中查看带紫色主题与系统级暗色适配的示例页面。也可以直接把它部署到云端(如 Vercel)。

工程要点小结

对照本示例可沉淀出几条可直接复用的集成经验:

  1. 变量覆写必须先于样式生成_variables.scss 中所有 $rmd-* 覆写必须在 @import "react-md/dist/styles" 之前,否则主题不生效;保持二者分离便于多主题场景切换。
  2. 暗色模式可纯 CSS 实现$rmd-theme-dark-elevation: "prefers-color-scheme" 配合 @media (prefers-color-scheme: dark) 中的 rmd-theme-dark mixin,即可在不写任何运行时 JS 的情况下响应系统深色偏好。
  3. 持久化布局提升路由体验:把 ConfigurationRMDLayout 放到 _app.tsx 的外层,让应用栏与导航抽屉跨路由保持,避免重复挂载开销。
  4. 路由感知导航需要桥接useLayoutNavigation + LinkUnstyled 是「ReactMD 导航树 ↔ Next.js 客户端路由」之间的桥梁,务必用 next/link 渲染树内链接以免整页刷新,并注意树条目的 parentId 默认值必须为 null
  5. 以 SVG 图标替代字体图标:通过 ConfigurableIcons 全局注入,能获得更好的缩放质量与主题一致性。
登录后查看全文
热门项目推荐
相关项目推荐