首页
/ 在 React Router 与 TypeScript 工程中集成 Material UI:examples/material-ui-react-router-ts 示例全解析

在 React Router 与 TypeScript 工程中集成 Material UI:examples/material-ui-react-router-ts 示例全解析

2026-09-07 12:52:04作者:苗圣禹Peter

本文以 Material UI 官方仓库中的 examples/material-ui-react-router-ts 示例为骨架,系统讲解如何在 Vite + React Router(SSR 模式)+ TypeScript 的现代工程中集成 Material UI,并正确处理 Emotion 样式引擎在服务端渲染、客户端水合与暗色/亮色主题下的配置。读完本文,你将掌握一套可复制的脚手架工程结构、关键配置文件的底层原理,以及遇到 MUI 与 React Router 组合开发时的常见取舍。

示例定位与工程概览

原 README 明确说明了该示例的用途:演示如何在 TypeScript 工程中把 Material UI 与 React Router 一起使用,并打包了 @mui/material 及其 peer 依赖,包括 Emotion——Material UI 的默认样式引擎。

与仓库中其他示例(如 examples/material-ui-nextjsexamples/material-ui-vite-ts)相比,这个示例的独特之处在于它基于 React Router 框架模式(framework mode),即由 Vite 插件驱动、自带路由配置与 SSR/水合能力的新一代架构,而不是把 React Router 当普通库手写 <BrowserRouter>。其文件布局如下:

examples/material-ui-react-router-ts/
├── app/                          # 应用源码
│   ├── components/               # 可复用页面组件
│   │   ├── Copyright.tsx
│   │   └── ProTip.tsx
│   ├── routes/                   # 路由级页面组件
│   │   ├── about.tsx
│   │   └── home.tsx
│   ├── createCache.ts            # Emotion 缓存工厂(带 @layer mui 包装)
│   ├── entry.client.tsx          # 浏览器端入口(水合)
│   ├── entry.server.tsx          # 服务端渲染入口(含 Emotion SSR 提取)
│   ├── root.tsx                  # 根布局、全局 Provider 与错误边界
│   ├── routes.ts                 # 路由表声明
│   └── theme.tsx                 # Material UI 主题(亮/暗色)
├── public/favicon.ico
├── package.json
├── react-router.config.ts        # React Router 框架配置
├── tsconfig.json
└── vite.config.ts                # Vite 构建配置(reactRouter 插件)

快速上手:安装与运行

原 README 提供的核心流程是下载/拷贝示例后安装依赖并启动开发服务器。对应命令如下:

npm install
npm run dev

在原 README 中还提供了两种免手工拷贝的方式:一种是直接从上游仓库拉取该示例目录的压缩包解压,另一种是借助在线沙箱(CodeSandbox / StackBlitz)直接体验,二者对本地 Node 环境无要求。若在本地克隆仓库,则进入 examples/material-ui-react-router-ts 目录执行上述命令即可。

值得补充的是,该示例的 package.json 共声明了四个脚本,分别覆盖开发、构建、产物运行与类型检查:

脚本 命令 作用
dev react-router dev 启动开发服务器(含 HMR)
build react-router build 构建生产产物
start react-router-serve ./build/server/index.js 以 Node 服务方式运行构建产物(SSR)
typecheck react-router typegen && tsc 先为路由生成类型,再执行 TypeScript 严格检查

其中 typecheck 里的 react-router typegen 会生成 .react-router/types/**/* 类型文件,这也是 tsconfig.jsoninclude 中包含 .react-router/types/**/*、并在 rootDirs 里把 ./.react-router/types 与工程根目录并列的原因——让路由 +types 的导入能在编译期通过。

依赖组成

package.json 可以看出依赖分三组:

  • UI 层@mui/material,以及 Emotion 三件套 @emotion/react@emotion/styled@emotion/cache(客户端缓存)与 @emotion/server(服务端样式提取);
  • 框架层react-router@react-router/node(Node 适配,提供 createReadableStreamFromReadable 等)、@react-router/serve(生产环境静态服务)、isbot(爬虫 UA 识别,供 SSR 全量渲染判断用);
  • 工程层vite@react-router/devvite-tsconfig-pathstypescript 及 React 类型声明。

所有依赖均取 latest,未锁定具体版本,实际行为以安装时的版本为准;示例同时设置了 "type": "module",整个工程按 ESM 方式运行。

构建配置:Vite、SSR 与路径别名

vite.config.ts

vite.config.ts 是整个工程最值得注意的配置文件:

import { reactRouter } from '@react-router/dev/vite';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  plugins: [reactRouter(), tsconfigPaths()],
  ssr: {
    // Workaround for resolving dependencies in the server bundle
    // Without this, the React context will be different between direct import and transitive imports in development environment
    optimizeDeps: {
      include: ['@emotion/*', '@mui/*'],
    },
    noExternal: ['@emotion/*', '@mui/*'],
  },
});

关键点有三:

  1. reactRouter() 插件接管构建,应用本身不再手写 Vite 入口,入口/路由由框架模式自动装配;
  2. tsconfigPaths() 让 TypeScript 的 ~/* 别名在 Vite 侧生效(与 tsconfig.json"~/*": ["./app/*"] 对应),源码里的 import ProTip from '~/components/ProTip' 因此可正常解析;
  3. ssr.optimizeDeps.includessr.noExternal 会把 @emotion/*@mui/* 统一打进服务端 bundle。配置注释说明了动机:如果不这样做,开发环境下直接导入与传递导入可能产生两份 React 上下文,导致 @emotion/react 的 Provider 判断失效(这也是 MUI + SSR 常见的一类坑)。

react-router.config.ts 与 tsconfig.json

react-router.config.ts 极为精简:

import type { Config } from '@react-router/dev/config';

export default {
  // Server-side render by default, to enable SPA mode set this to `false`
  ssr: true,
} satisfies Config;

ssr: true 表示默认启用服务端渲染。若想退化为纯 SPA 模式,把这里改为 false 即可;这一开关同时会影响 entry.server.tsx 中渲染就绪策略的选择。

tsconfig.json 采用 strict 全开、moduleResolution: "bundler"jsx: "react-jsx"verbatimModuleSyntax 等现代配置,并开启 resolveJsonModuleskipLibChecktypes 仅包含 nodevite/client,避免引入无关全局类型。

应用外壳:根布局、全局 Provider 与错误边界

app/root.tsx 承担了三个职责:

1. 全局 <html> 布局与字体预连接。 links 导出函数声明了 Google Fonts 的 preconnect 以及 Roboto 字体表(Material UI 的推荐默认字体),并在 Layout 组件中放入 <Meta /><Links /><ScrollRestoration /><Scripts /> 等框架必需标签,同时在 <body> 上设置 suppressHydrationWarning 以兼容水合差异。

2. 注入 Emotion 缓存与主题。 根组件 App 中根据是否处于浏览器环境做差异化渲染(见 app/root.tsx):

const cache = createEmotionCache();

export default function App() {
  if (typeof window !== 'undefined') {
    return (
      <CacheProvider value={cache}>
        <AppTheme>
          <Outlet />
        </AppTheme>
      </CacheProvider>
    );
  }
  return (
    <AppTheme>
      <Outlet />
    </AppTheme>
  );
}

注意:只有客户端才包 CacheProvider。这是因为服务端渲染时样式提取走的是 entry.server.tsx 里各自新建的缓存实例,而客户端水合需要一个稳定的全局单例缓存,否则可能出现重复注入样式。

3. 根级错误边界。 ErrorBoundary 使用 isRouteErrorResponse(error) 区分框架路由错误(404/5xx)与普通异常:404 展示专属文案;开发模式下(import.meta.env.DEV)才会泄露错误堆栈。边界内直接使用 @mui/material/Box 布局,体现 MUI 组件在整个应用(含错误页)中的覆盖。

主题与暗色模式:theme.tsx + CssBaseline

app/theme.tsx 展示了 Material UI 当前推荐的 createTheme 用法:

import * as React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';

const theme = createTheme({
  cssVariables: true,
  colorSchemes: {
    light: true,
    dark: true,
  },
});

export default function AppTheme({ children }: AppThemeProps) {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      {children}
    </ThemeProvider>
  );
}

两个配置项的含义:

  • cssVariables: true:让主题令牌以 CSS 变量形式输出,便于运行时切换主题、按颜色方案缓存样式,是 MUI 新版主题能力的开关;
  • colorSchemes: { light: true, dark: true }:同时启用亮/暗两种配色方案,CssBaseline 会按系统偏好与方案声明自动处理背景与前景色归一化(@media (prefers-color-scheme) 逻辑见 @mui/materialCssBaseline 实现),因此示例页面在亮色与暗色之间可无缝切换。

AppTheme 这个包裹组件对路由 Outlet 之外的所有 MUI 组件生效,是“一处定义、全局复用”主题的标准姿势。

关键拼图:带 @layer mui 的 Emotion 缓存

app/createCache.ts 是很多读者容易忽略却至关重要的文件:

import createCache from '@emotion/cache';

export default function createEmotionCache(options?: Parameters<typeof createCache>[0]) {
  const emotionCache = createCache({ key: 'mui', ...options });
  const prevInsert = emotionCache.insert;
  emotionCache.insert = (...args) => {
    // ignore styles that contain layer order (`@layer ...` without `{`)
    if (!args[1].styles.match(/^@layer\s+[^{]*$/)) {
      args[1].styles = `@layer mui {${args[1].styles}}`;
    }
    return prevInsert(...args);
  };

  return emotionCache;
}

它做了两件事:

  1. key: 'mui' 创建 Emotion 缓存,使生成的样式类带有 mui 前缀标识,避免与第三方 Emotion 实例冲突;
  2. 重写 insert 钩子:对每条即将插入的样式追加 @layer mui { ... } 级联层包装,唯一的例外是当样式本身已是裸的 @layer xxx 声明(即不含 { 的层顺序语句)时跳过包装。这样可以把 MUI 生成的样式整体约束在自定义级联层内,降低与 Tailwind CSS 等使用 @layer 体系的框架共存时的优先级冲突风险。

这个缓存工厂同时被客户端水合路径(root.tsx)与服务端入口复用,确保两端规则一致。

SSR 与水合:entry.server.tsx 与 entry.client.tsx

框架模式的 SSR 由自定义入口接管。app/entry.server.tsx 展示了 MUI + Emotion 服务端渲染的规范做法

const cache = createEmotionCache();
const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache);

它通过 ReactDOMServer.renderToPipeableStream<CacheProvider value={cache}><ServerRouter .../></CacheProvider> 渲染为可中断流,随后用 createEmotionServer(cache) 从产出的 HTML 中提取 Emotion 关键样式,拼成 <style> 标签注入到 </head> 之前。这解决了 SSR 场景下“首屏 HTML 不含样式导致闪烁/错版”的问题。

关于渲染就绪策略,代码做了精细区分:

const readyOption =
  (userAgent && isbot(userAgent)) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady';
  • 普通浏览器:用 onShellReady 先吐壳,配合 streamTimeout(5000ms 后 abort)实现流式响应;
  • 爬虫/机器人或 SPA 模式下:改用 onAllReady,等全部内容加载完再响应,保证 SEO 抓取到完整 DOM(注释还引用了 React 官方对渲染策略的说明)。

浏览器侧 app/entry.client.tsx 则用 React.startTransition 包裹 ReactDOM.hydrateRoot,并在 <StrictMode> 下挂载 HydratedRouter 完成水合。这解释了前文 App 为何要区分客户端与服务端分支——服务端不建持久缓存、客户端必须建,两者配套才能保证样式幂等。

路由定义与页面组件

路由表集中在 app/routes.ts

import { type RouteConfig, index, route } from '@react-router/dev/routes';

export default [
  index('routes/home.tsx'),
  route('/about', 'routes/about.tsx'),
] satisfies RouteConfig;
  • / 映射 routes/home.tsx(首页);
  • /about 映射 routes/about.tsx(关于页)。

app/routes/about.tsx 为例,可以看出 MUI 组件与 React Router 的衔接写法

import { Link as ReactRouterLink } from 'react-router';
// ...
<Button variant="contained" component={ReactRouterLink} to="/">
  Go to the home page
</Button>

这里复用了 MUI Buttoncomponent 多态机制:把按钮的底层元素替换为 React Router 的 Link,从而既保留按钮外观又获得框架路由跳转能力(客户端导航、无整页刷新)。路由页面通过导出的 meta() 返回标题与描述,供框架生成 <head>。页面主体使用 ContainerBoxTypographyButton 等 MUI 组件按 sx 布局;首页 home.tsx 的结构与之类似,可通过仓库源码继续对照阅读。

页面底部的 ProTipCopyright 来自 app/components 目录,作为页脚提示与版权占位的可复用组件,也示范了把公共 UI 片段抽到 app/components 的组织习惯。

小结:可复制到自身项目的关键经验

围绕原 README 的“The idea behind the example”,可以从源码提炼出六条可直接迁移的工程经验:

  1. 用框架模式集成vite.config.ts 挂载 reactRouter() 插件并声明 ~/* 别名(vite-tsconfig-paths),无需手写路由入口;
  2. 默认 SSRreact-router.config.tsssr: true,需要纯 SPA 时可改回 false
  3. 两端共享主题:在 root.tsxAppThemeThemeProvider + CssBaseline)包住 Outlettheme.tsx 中开启 cssVariablescolorSchemes 即可获得原生亮/暗双模式;
  4. Emotion 缓存策略分端:客户端 CacheProvider 使用单例缓存;服务端入口每次请求新建缓存并通过 @emotion/serverextractCriticalToChunks/constructStyleTagsFromChunks 注入 <head>
  5. 样式分层隔离:通过 createCache.ts 把全部 MUI 样式包进 @layer mui,为将来与其他样式体系共存预留空间;
  6. MUI × Router 的组合套路:跨页导航用 component={ReactRouterLink} 让 MUI 组件直接具备框架路由能力,并在 routes.ts 集中维护路由表。

按原 README 的建议,拿到这套可用工程后,下一步可以在仓库文档的模板/入门章节以及 examples 目录下的其他示例(如 Next.js、Vite 版本)中挑选更完整的模板继续扩展——本次示例所承载的 MUI + React Router + Emotion SSR 体系,足以作为多数生产级应用的起点骨架。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388