logo
  • 指南
  • 配置
  • 插件
  • API
  • 示例
  • 社区
  • Modern.js 2.x 文档
  • 简体中文
    • 简体中文
    • English
    • 开始
      介绍
      快速上手
      版本升级
      名词解释
      技术栈
      核心概念
      页面入口
      构建工具
      Web 服务器
      基础功能
      路由
      路由基础
      配置式路由
      数据管理
      数据获取
      数据写入
      数据缓存
      渲染
      服务端渲染(SSR)
      服务端流式渲染(Streaming SSR)
      渲染缓存
      静态站点生成(SSG)
      渲染预处理 (Render Preprocessing)
      样式开发
      引入 CSS
      使用 CSS Modules
      使用 CSS-in-JS
      使用 Tailwind CSS
      HTML 模板
      引用静态资源
      引用 JSON 文件
      引用 SVG 资源
      引用 Wasm 资源
      调试
      数据模拟(Mock)
      网络代理
      使用 Rsdoctor
      使用 Storybook
      测试
      Playwright
      Vitest
      Jest
      Cypress
      路径别名
      环境变量
      构建产物目录
      部署应用
      进阶功能
      使用 Rspack
      使用 BFF
      基础用法
      运行时框架
      扩展 BFF Server
      扩展一体化调用 SDK
      文件上传
      跨项目调用
      优化页面性能
      代码分割
      静态资源内联
      产物体积优化
      React Compiler
      提升构建性能
      浏览器兼容性
      配置底层工具
      源码构建模式
      服务端监控
      Monitors
      日志事件
      指标事件
      国际化
      基础概念
      快速开始
      配置说明
      语言检测
      资源加载
      路由集成
      API 参考
      高级用法
      最佳实践
      自定义 Web Server
      专题详解
      模块联邦
      简介
      开始使用
      应用级别模块
      服务端渲染
      部署
      集成国际化能力
      常见问题
      依赖安装问题
      命令行问题
      构建相关问题
      热更新问题
      已下线功能
      📝 编辑此页面
      上一页Web 服务器下一页配置式路由

      #路由基础

      Modern.js 的路由基于 React Router 7,提供了基于文件约定的路由能力,并支持了业界流行的约定式路由模式:嵌套路由。当入口被识别为 约定式路由 时,Modern.js 会自动基于文件系统,生成对应的路由结构。

      Note

      本小节提到的路由,都是约定式路由。

      #什么是嵌套路由

      嵌套路由是一种将 URL 分段与组件层次结构和数据耦合起来的路由模式。通常,URL 段会决定:

      • 页面上要渲染的布局
      • 这些布局的数据依赖

      因此,使用嵌套路由时,页面的路由与 UI 结构是相呼应的,我们将会详细介绍这种路由模式。

      /user/johnny/profile                  /user/johnny/posts
      +------------------+                  +-----------------+
      | User             |                  | User            |
      | +--------------+ |                  | +-------------+ |
      | | Profile      | |  +------------>  | | Posts       | |
      | |              | |                  | |             | |
      | +--------------+ |                  | +-------------+ |
      +------------------+                  +-----------------+

      #路由文件约定

      在 routes/ 目录下,子目录名会作为路由 URL 的映射,Modern.js 有两个文件约定 layout.tsx 和 page.tsx。这两个文件决定了应用的布局层次,其中:

      • page.tsx 为内容组件,所在目录下存在该文件时,对应的路由 URL 可访问。
      • layout.tsx 为布局组件,控制所在目录下所有子路由的布局,使用 <Outlet> 表示子组件。
      Tip

      .ts、.js、.jsx 或 .tsx 文件扩展名可用于上述约定文件。

      #Page

      <Page> 组件是指 routes/ 目录下所有 page.tsx 文件,也是所有路由的叶子组件。除了通配路由外,任何路由都应该由 <Page> 组件结束。

      routes/page.tsx
      export default () => {
        return <div>Hello world</div>;
      };

      当应用中存在以下目录结构时:

      .
      └── routes
          ├── page.tsx
          └── user
              └── page.tsx

      会产出下面两条路由:

      • /
      • /user

      #Layout

      <Layout> 组件是指 routes/ 目录下所有 layout.tsx 文件,它们表示对应路由片段的布局,使用 <Outlet> 表示子组件。

      routes/layout.tsx
      import { Link, Outlet, useLoaderData } from '@modern-js/runtime/router';
      
      export default () => {
        return (
          <>
            <Outlet></Outlet>
          </>
        );
      };
      Note

      <Outlet> 是 React Router v7 中提供的 API,详情可以查看 Outlet。

      不同目录结构下,<Outlet> 所代表的组件也不同。为了方便介绍 <Layout> 与 <Outlet> 的关系,以下面的文件目录举例:

      .
      └── routes
          ├── blog
          │   └── page.tsx
          ├── layout.tsx
          ├── page.tsx
          └── user
              ├── layout.tsx
              └── page.tsx
      1. 当路由为 / 时,routes/layout.tsx 中的 <Outlet> 代表的是 routes/page.tsx 中导出的组件,路由的 UI 结构为:
      <Layout>
        <Page />
      </Layout>
      1. 当路由为 /blog 时,routes/layout.tsx 中的 <Outlet> 代表的是 routes/blog/page.tsx 中导出的组件,路由的 UI 结构为:
      <Layout>
        <BlogPage />
      </Layout>
      1. 当路由为 /user 时,routes/layout.tsx 中的 <Outlet> 代表的是 routes/user/layout.tsx 中导出的组件。routes/user/layout.tsx 中的 <Outlet> 代表的是 routes/user/page.tsx 中导出的组件,路由的 UI 结构为:
      <Layout>
        <UserLayout>
          <UserPage />
        </UserLayout>
      </Layout>

      总结而言,如果子路由的文件目录下存在 layout.tsx,上一级 layout.tsx 中的 <Outlet> 即为子路由文件目录下的 layout.tsx ,否则为子路由文件目录下的 page.tsx。

      #动态路由

      通过 [] 命名的文件目录,生成的路由会作为动态路由。例如以下文件目录:

      .
      └── routes
          ├── [id]
          │   └── page.tsx
          ├── blog
          │   └── page.tsx
          └── page.tsx

      routes/[id]/page.tsx 文件会转为 /:id 路由。除了可以确切匹配的 /blog 路由,其他所有 /xxx 都会匹配到该路由。

      在组件中,可以通过 useParams 获取对应命名的参数。

      import { useParams } from '@modern-js/runtime/router';
      
      function Blog() {
        const { id } = useParams();
        return <div>current blog ID is: {id}</div>;
      }
      export default Blog;

      #动态可选路由

      通过 [$] 命名的文件目录,生成的路由会作为动态可选路由。例如以下文件目录:

      .
      └── routes
          ├── blog
          │   └── [id$]
          │       └── page.tsx
          └── page.tsx

      routes/blog/[id$]/page.tsx 文件会转为 /blog/:id? 路由。/blog 下的所有路由都会匹配到该路由,并且 id 参数可选存在。通常在区分创建与编辑时,可以使用该路由。

      import { useParams } from '@modern-js/runtime/router';
      
      function Blog() {
        const { id } = useParams();
        if (id) {
          return <div>current blog ID is: {id}</div>;
        }
      
        return <div>create new blog</div>;
      }
      export default Blog;

      #通配路由

      如果在某个子目录下存在 $.tsx 文件,该文件会作为通配路由组件,当没有匹配的路由时,会渲染该路由组件。

      Note

      $.tsx 可以认为是一种特殊的 <Page> 组件,如果路由无法匹配,则 $.tsx 会作为 <Layout> 的子组件渲染。

      Warning

      如果当前目录下不存在 <Layout> 组件时,则 $.tsx 不会生效。

      例如以下目录结构:

      .
      └── routes
          ├── blog
          │   ├── $.tsx
          │   └── layout.tsx
          ├── layout.tsx
          └── page.tsx

      当你访问 /blog/a 时,无法匹配到任意路由,则页面会渲染 routes/blog/$.tsx 组件,路由的 UI 结构为:

      <RootLayout>
        <BlogLayout>
          <$></$>
        </BlogLayout>
      </RootLayout>

      如果希望访问 /blog 时,也匹配到 blog/$.tsx 文件,需要删除同目录下的 blog/layout.tsx 文件,同时保证 blog 下面没有其他子路由。

      同样,$.tsx 中可以使用 useParams 捕获 url 的剩余部分。

      $.tsx
      import { useParams } from '@modern-js/runtime/router';
      
      function Blog() {
        // 当 path 是 `/blog/aaa/bbb` 时
        const params = useParams();
        console.log(params); // ---> { '*': 'aaa/bbb' }
      
        return <div>current blog URL is {params['*']}</div>;
      }
      export default Blog;

      #定制 404 页面

      通配路由可以被添加到 routes/ 目录下的任意子目录中,一种常见的使用场景是通过 $.tsx 文件去定制任意层级的 404 内容。

      例如你需要对所有未匹配到的路由,都展示一个 404 页面,可以添加 routes/$.tsx 文件:

      .
      └── routes
          ├── $.tsx
          ├── blog
          │   └── [id$]
          │       └── page.tsx
          ├── layout.tsx
          └── page.tsx
      function Page404() {
        return <div>404 Not Found</div>;
      }
      export default Page404;

      此时,当访问除了 / 或 /blog/* 以外的路由时,都会匹配到 routes/$.tsx 组件,展示 404 页面。

      #路由句柄配置

      某些场景下,每个路由会拥有属于自己的数据,应用需要在其他组件中获取匹配到的路由的这些数据。一个常见的例子是在布局中获取到匹配路由的面包屑信息。

      Modern.js 提供了独立的约定,每个 Layout, $ 或 Page 文件都可以定义一个自己的 config 文件,如 page.config.ts,该文件中我们约定了一个具名导出 handle,这个字段中你可以定义任意属性:

      routes/page.config.ts
      export const handle = {
        breadcrumbName: 'profile',
      };

      定义的这些属性可以通过 useMatches hook 获取。

      routes/layout.ts
      export default () => {
        const matches = useMatches();
        const breadcrumbs = matches.map(
          matchedRoute => matchedRoute?.handle?.breadcrumbName,
        );
        return <Breadcrumb names={breadcrumbs}></Breadcrumb>;
      };

      #无路径布局

      当目录名以 __ 开头时,对应的目录名不会转换为实际的路由路径,例如以下文件目录:

      .
      └── routes
          ├── __auth
          │   ├── layout.tsx
          │   ├── login
          │   │   └── page.tsx
          │   └── sign
          │       └── page.tsx
          ├── layout.tsx
          └── page.tsx

      Modern.js 会生成 /login 和 /sign 两条路由,__auth/layout.tsx 组件会作为 login/page.tsx 和 sign/page.tsx 的布局组件,但__auth 不会作为路由路径片段出现在用户访问的 URL 中。

      当需要为某些类型的路由,做独立的布局,或是想要将路由做归类时,这一功能非常有用。

      #无布局路径

      有些情况下,项目需要较为复杂的路由,但这些路由又不存在独立的 UI 布局,如果像普通文件目录那样创建路由会导致目录层级较深。

      因此 Modern.js 支持了通过 . 来分割路由片段,代替文件目录。例如,当需要 /user/profile/2022/edit 时,可以直接创建如下文件:

      └── routes
          ├── user.profile.[id].edit
          │      └── page.tsx
          ├── layout.tsx
          └── page.tsx

      访问路由时,将得到如下 UI 布局:

      <RootLayout>
        {/* routes/user.profile.[id].edit/page.tsx */}
        <UserProfileEdit />
      </RootLayout>

      #路由重定向

      某些应用中,可能需要根据用户的身份信息,或是其他数据条件,选择重定向到其他路由。在 Modern.js 中,你可以使用 Data Loader 文件来获取数据,或是和传统 React 组件那样,在 useEffect 中请求数据。

      #在 Data Loader 中重定向

      在任意的 page.tsx 同级目录中创建,page.data.ts 文件,这个文件就是该路由的 Data Loader。在 Data Loader 中,你可以通过调用 redirect API 来完成路由的重定向。

      routes/user/page.data.ts
      import { redirect } from '@modern-js/runtime/router';
      
      export const loader = () => {
        const user = await getUser();
        if (!user) {
          return redirect('/login');
        }
        return null;
      };

      #在组件中重定向

      在组件内做重定向,则可以通过 useNavigate hook,示例如下:

      routes/user/page.ts
      import { useNavigate } from '@modern-js/runtime/router';
      import { useEffect } from 'react';
      
      export default () => {
        const navigate = useNavigate();
        useEffect(() => {
          getUser().then(user => {
            if (!user) {
              navigate('/login');
            }
          });
        });
      
        return <div>Hello World</div>;
      };

      #错误处理

      routes/ 下每一层目录中,开发者同样可以定义一个 error.tsx 文件,默认导出一个 <ErrorBoundary> 组件。当路由目录下存在该组件时,组件渲染出错会被 ErrorBoundary 组件捕获。

      <ErrorBoundary> 可以返回出错时的 UI 视图,当前层级未声明 <ErrorBoundary> 组件时,错误会向上冒泡到更上层的组件,直到被捕获或抛出错误。同时,当组件出错时,只会影响捕获到该错误的路由组件及子组件,其他组件的状态和视图不受影响,可以继续交互。

      在 <ErrorBoundary> 组件内,可以使用 useRouteError 获取的错误的具体信息:

      import { useRouteError } from '@modern-js/runtime/router';
      
      const ErrorBoundary = () => {
        const error = useRouteError();
        return (
          <div>
            <h1>{error.status}</h1>
            <h2>{error.message}</h2>
          </div>
        );
      };
      export default ErrorBoundary;

      #Loading (Experimental)

      Experimental

      此功能当前是实验性功能,后续 API 可能有调整。

      在约定式路由下, Modern.js 会根据路由,自动地对路由进行分片(每一个路由都作为单独的 JS Chunk 进行加载),当用户访问具体的路由时,再自动加载对应的分片。这样可以有效地减少首屏加载的时间。但这也带来了一个问题,当用户访问一个路由时,如果该路由对应的分片还未加载完成,就会出现白屏的情况。

      Modern.js 支持通过 loading.tsx 文件来解决这个问题,routes/ 下每一层目录中,都可以创建 loading.tsx 文件,默认导出一个 <Loading> 组件。

      Warning

      如果当前目录下不存在 <Layout> 组件时,则 loading.tsx 不会生效。为了保证用户体验,Modern.js 推荐每个应用添加根 Loading 组件。

      当路由目录下同时存在该组件和 layout 组件时,这一级路由下所有的子路由切换时,会先展示 loading.tsx 中导出的组件 UI,直到对应的 JS Chunk 加载完成。例如以下文件目录:

      .
      └── routes
          ├── blog
          │   ├── [id]
          │   │   └── page.tsx
          │   └── page.tsx
          ├── layout.tsx
          ├── loading.tsx
          └── page.tsx

      当定义 loading.tsx 时,当路由从 / 跳转到 /blog,或从 /blog 跳转到 /blog/123 时,如果路由对应的 JS Chunk 还未加载,都会先展示 loading.tsx 中导出的组件 UI。相当于最终的 UI 结构如下:

      当路由为
      <Layout>
        <Suspense fallback={<Loading />}>
          <Page />
        </Suspense>
      </Layout>
      当路由为
      <Layout>
        <Suspense fallback={<Loading />}>
          <BlogPage />
        </Suspense>
      </Layout>
      当路由为
      <Layout>
        <Suspense fallback={<Loading />}>
          <BlogIdPage />
        </Suspense>
      </Layout>

      #预加载

      大多数切换路由时白屏的情况,都可以通过定义 <Loading> 组件来优化体验。Modern.js 也支持在 <Link> 组件上定义 prefetch 属性,提前对静态资源和数据进行加载。

      对于有更高性能要求的应用,预加载可以进一步提升用户体验,减少展示 <Loading> 组件的时间:

      <Link prefetch="intent" to="page">
      Tip

      对数据的预加载目前只会预加载 SSR 项目中 Data Loader 中返回的数据。

      prefetch 属性有三个可选值:

      • none, 默认值,不会做 prefetch,没有任何额外的行为。
      • intent,这是我们推荐大多数场景下使用的值,当你把鼠标放在 Link 上时,会自动开始加载对应的分片和 Data Loader 中定义的数据,当鼠标移开时,会自动取消加载。在我们的测试中,即使是快速点击,也能减少大约 200ms 的加载时间。
      • render,当 <Link> 组件渲染时,就会加载对应的分片和 Data Loader 中定义的数据。
      值为 render 和不做路由分片的区别
      • render 加载路由分片的时机是可控的,只有在 <Link> 组件进入视窗时才触发,可以通过控制 <Link> 组件的渲染位置来控制分片加载时机。
      • render 仅在空闲时对静态资源进行加载,不占用重要模块的加载时间。
      • 除了预加载路由分片,render 在 SSR 项目中还会发起数据预取。

      #常见问题

      1. 为什么要提供 @modern-js/runtime/router 来导出 React Router API ?

      可以发现,在文档中所有的代码用例都是使用 @modern-js/runtime/router 包导出的 API,而不是直接使用 React Router 包导出的 API。那两者有什么区别呢?

      首先,在 @modern-js/runtime/router 中导出的 API 和 React Router 包的 API 是完全一致的,如果某个 API 使用出现问题,请先检查 React Router 的文档和 Issues。

      在使用约定式路由的情况下,务必使用 @modern-js/runtime/router 中的 API,不直接使用 React Router 的 API。因为 Modern.js 内部会安装 React Router,如果应用中使用了 React Router 的 API,可能会导致两个版本的 React Router 同时存在,出现不符合预期的行为。

      Note

      如果应用中必须直接使用 React Router 包的 API,例如部分路由行为被封装在统一的 npm 包中,那应用可以通过设置 source.alias,将 react-router 和 react-router-dom 统一指向项目的依赖,避免两个版本的 React Router 同时存在的问题。