Back to blog
May 20, 2024
7 min read

梳理当前项目的路由系统(二),基础案例演示

梳理一个完整项目的整体路由系统是相对复杂的,我们将逐步深入了解,进行基础案例演示

路由是前端应用架构中最重要的基础框架

在整个架构设计初期,路由设计是第一优先级。因为路由相对独立且基础,它通常不依赖于具体业务和数据模型,但是却为整个应用的页面和导航提供了框架 在设计路由时,应遵循以下几个原则:

  • 明确性:URL路径应该清晰、简洁,能够准确反映页面内容。例如,/products应该对应产品列表,/products/:id对应具体产品详情。
  • 层次性:路径应该有层次感,能够反映页面之间的层级关系。例如,/dashboard/settings显然是一个设置页面,并且位于仪表盘下。
  • 可扩展性:路由设计应考虑未来的扩展需求,留有足够的灵活性。例如,可以为将来的新功能预留路径。
  • 一致性:路由的命名和结构应该保持一致,避免混淆。比如所有资源的详情页都使用/:resource/:id这种形式。

路由定义了页面的访问路径,负责组织和协调各个页面组件的展示逻辑,为其他内容(页面、组件、样式、接口等)通过了基础架构和明确的内容指引

  1. 导航控制:路由控制用户能够访问的页面和路径,提供导航的逻辑。
  2. 组件加载:通过路由决定哪些组件在何时加载,包括懒加载组件以优化性能。
  3. 权限管理:结合路由守卫实现权限管理,决定不同用户可以访问的内容。
  4. 数据流管理:在路由级别进行数据加载和管理,可以决定页面加载前后需要处理的数据。

最基础的路由参数

核心就2个: RouterProvider(管理路由上下文) 和 createBrowserRouter(createHashRouter) - 创建不同类型的路由器

RouterProvider

创建的路由器对象传递给整个应用,使得路由配置生效。

import { RouterProvider, createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
    // ... 路由配置
])
const App = () => {
    return (
        <RouterProvider router={router} />
    )
}

createBrowserRouter和createHashRouter 是什么?有什么区别?

创建路由的两个函数,主要基于不同的历史管理机制

createBrowserRouter: 创建基于HTML5 history API的路由器。它使用浏览器的pushState和popState事件来管理URL。可以实现无刷新地改变URL路径,非常适合现代浏览器。由于路径不包含哈希符号,URL看起来更干净,且对SEO更友好(特别是在服务器端渲染的情况下)。需要服务器配置来处理不同的URL路径,通常需要配置URL重写规则,让所有请求都指向应用的入口文件(例如index.html)。 1. 优势1:URL美观:不带#符号的URL更美观,更符合用户习惯,且对SEO友好。 2. 优势2:现代Web开发:利用HTML5 history API,允许在不刷新页面的情况下改变URL,同时保留浏览器的前进、后退功能。 3. 优势3:SEO优化:适合需要搜索引擎优化的场景,因为干净的URL更容易被搜索引擎索引。 4. 劣势1:服务器配置:需要服务器配置支持所有URL都指向应用入口文件。常见的配置包括Apache的.htaccess或Nginx的try_files

createHashRouter: 创建基于URL哈希部分的路由器。它使用URL中的**#**部分来管理路由,这种方式与浏览器的原生行为兼容,不需要服务器端的特别配置。适用于老旧浏览器,或在服务器不支持HTML5 history API的情况下使用。由于URL的变化仅在哈希部分进行,因此无需服务器端的URL重写规则,所有请求都可以直接加载应用的入口文件。 1. 优势1:兼容性:适用于需要兼容老旧浏览器的场景,这些浏览器可能不完全支持HTML5 history API。 2. 优势2:无需服务器配置:哈希路由器不需要特殊的服务器配置,因为URL的变化仅在客户端处理。这对于静态网站托管(如GitHub Pages)非常有用。 3. 劣势1:SEO影响:哈希路由对SEO不友好,因为搜索引擎通常不处理#之后的URL部分。这意味着使用哈希路由的页面可能不会被搜索引擎很好地索引。 4. 劣势2:URL格式:URL中带有#符号,例如http://example.com/#/about,看起来不如直接的路径美观。

使用 createBrowserRouter 为什么需要进行服务器配置?

使用 createBrowserRouter 在一个单页应用(SPA)中涉及的前后端交互比较独特,下面描述一下这个过程

  1. 当用户输入网址 http://example.com 并访问网站,服务器返回 index.html 文件以及相关的 JavaScript、CSS 文件。这时,React Router 负责渲染与当前 URL 匹配的组件,通常是应用的首页。
  2. 用户在应用中点击链接到 /about 页面。使用 createBrowserRouter 的 React Router 通过调用 history.push('/about') 来改变浏览器的地址栏为 http://example.com/about 同时不会向服务器发送请求。React Router 接管路由逻辑,根据路由配置动态渲染 About 组件。
  3. 如果用户直接在浏览器地址栏输入 http://example.com/about 或在该页面上刷新页面,浏览器会向服务器发送请求寻找 /about 路径。由于这是一个单页应用,服务器文件系统中并没有名为 about 的文件或目录,这通常会导致服务器返回404错误,除非进行特别的配置。

为了解决这个问题,确保单页应用能够正确处理直接访问或刷新非首页的 URL,服务器需要被配置为对于所有此类请求都返回 index.html 文件,同时由前端的 React Router 接管路由并渲染正确的组件。通常会使用 Nginx 方案

Nginx方案 & 请求路径的解析过程

server {
    listen 80;
    server_name example.com;

    location / {
        root   /var/www/your-spa;
        try_files $uri $uri/ /index.html;
    }
}

Nginx 的 root 指令用于指定请求文件的根目录。例如,假设你的前端应用构建后的文件存储在 /var/www/your-spa 目录中,那么配置将会是:root /var/www/your-spa

假设前端应用构建后的文件存储在 /var/www/your-spa 中,目录结构如下:

/var/www/your-spa
├── index.html
├── main.js
├── main.css
└── assets/
    ├── logo.png
    └── ...
  1. 当用户访问 http://example.com/ 时( 访问根路径 / ): Nginx 会在 /var/www/your-spa 目录中查找文件 index.html。 找到并返回该文件,由浏览器渲染首页。
  2. 当用户访问 http://example.com/main.js 时(访问静态文件): Nginx 会在 /var/www/your-spa 目录中查找文件 main.js。 找到并返回该文件,浏览器加载和执行该文件。
  3. 当用户访问 http://example.com/about 时(访问路径 /about):
    1. Nginx 会在 /var/www/your-spa 目录中查找文件 about。 由于这是单页应用,实际文件系统中没有 about 文件。
    2. 接着 Nginx 会查找目录 /about/,也不存在。
    3. 最后,Nginx 返回 index.html,前端 JavaScript(例如 React Router)处理并渲染 /about 路由对应的组件。

**try_files** 指令的作用

  1. $uri:检查请求路径对应的文件是否存在,例如 /var/www/your-spa/about
  2. $uri/:如果前者不存在,再检查对应的目录是否存在,例如 /var/www/your-spa/about
  3. /index.html:如果以上都不存在,返回 index.html,从而让前端的路由系统处理该路径。

createHashRouter在Nginx中不需要做任何的特殊处理,直接返回入口文件即可(通常是index.html),路由操作交由前端处理 1. 访问 http://example.com/#/about 时,浏览器发送到服务器请求的是 http://example.com,不包括 #/about,这是因为 # 号后面的内容(称之为 fragmenthash)在HTTP请求中不被发送到服务器,而仅仅在客户端处理,这是URL规范和浏览器行为的一部分。 2. 当你在浏览器中输入 http://example.com/#/about 并回车:浏览器只会向服务器发送 http://example.com/ 请求。服务器接收到请求,并返回根目录的资源(通常是 index.html)。浏览器加载返回的 HTML 内容后,解析和处理 URL 的哈希部分(#/about),并在客户端进行相应的操作(例如,前端路由库会根据哈希部分渲染相应的组件)。 3. 为什么不发送Hash到服务器?Hash 最初用于在同一页面内导航到特定位置,例如,在页面总,有多个标题,跳转 HTML 文档到其中的某个标题。

createBrowserRouter在Nginx中会直接先查找对应的文件和文件夹,如果没有则直接返回index.html文件,交由前端处理 1. 用户访问 http://example.com/about 浏览器向服务器发送请求:http://example.com/about。服务器首先查找 /var/www/your-spa/about 文件,如果不存在则查找 /var/www/your-spa/about/ 目录。如果都不存在,返回 /var/www/your-spa/index.html 文件。 2. 浏览器加载 index.html:前端 JavaScript 代码(例如,React 和 React Router)解析 URL 路径 /about。前端路由库处理路径路由,并渲染相应的组件(AboutPage)。

Outlet useOutlet

Outlet 是一个占位符组件,用于渲染嵌套的子路由。它在父路由组件中定义,当子路由匹配时,Outlet 会渲染相应的子路由组件。 useOutlet 是一个 React Hook,用于在子路由组件中获取 Outlet 渲染的内容。

  1. 嵌套路由处理useOutlet 是专门处理嵌套路由设计的,在组件内部获取并渲染嵌套的子路由内容
  2. 条件渲染:通过 useOutlet 可以非常方便地实现根据嵌套路由,是否匹配来渲染不同的逻辑或内容
  3. 保持路由结构清晰:使用 useOutlet 可以保持路由结构的清晰和简洁,避免复杂的条件判断和状态管理逻辑

具体案例

路由配置:

import React from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import CustomDashboard from './CustomDashboard';
import HomePage from './HomePage';
import AboutPage from './AboutPage';
import SettingsPage from './SettingsPage';
import ProfilePage from './ProfilePage';

import Layout from './Layout';
import ErrorPage from './ErrorPage';

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        // element: <HomePage />
        element: <CustomDashboard />
      },
      {
        path: "about",
        element: <AboutPage />
      },
      {
        path: "settings",
        element: <SettingsPage />
      },
      {
        path: "profile",
        element: <ProfilePage />
      }
    ],
  },
]);

const App = () => (
  <RouterProvider router={router} />
);

export default App;

CustomDashboard.jsx

import React from 'react';
import { useOutlet } from 'react-router-dom';

const CustomDashboard = () => {
  const outlet = useOutlet();

  return (
    <div>
      <h2>Dashboard</h2>
      {outlet ? (
        <div>
          {outlet}
        </div>
      ) : (
        <div>
          <h3>Welcome to your dashboard</h3>
          <p>Here you can see an overview of your account and manage your settings.</p>
        </div>
      )}
    </div>
  );
};

export default CustomDashboard;

Layout.jsx

import React from 'react';
import { Outlet, Link } from 'react-router-dom';

const Layout = () => (
  <div>
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/dashboard">Dashboard</Link>
    </nav>
    <hr />
    <Outlet />
  </div>
);

export default Layout;

/、/about、/settings、/profile 这四个路由命中之后,会分别执行具体的页面组件。它们会被渲染在**Outlet**中。这就是 Outlet 的作用 useOutletCustomDashboard 组件中使用,它的目的是获取 Outlet 渲染的内容,这个意思是假如我的路由触发的是 / 那么会命中 CustomDashboard 组件,在当前组件中通过 useOutlet 去获取 Outlet 渲染的内容,只要不是这三个路由中的一个 /about、/settings、/profile,那么最后得到的值就是undefined,按照 CustomDashboard 组件内部的逻辑,会提示 Welcome to your dashboard,如果命中了另外三个路由,那么就会命中 <div>{outlet}</div> 这套逻辑。

路由的作用和 Outlet 的功能 顶级路由: - /:当用户访问根路径 / 时,会渲染 Layout 组件。 - Layout 组件包含一个导航栏和一个 Outlet。Outlet 是一个占位符,表示在这里渲染匹配的子路由组件。 嵌套路由: - /about:匹配到 AboutPage,并在 Layout 的 Outlet 位置渲染 AboutPage 组件。 - /settings:匹配到 SettingsPage,并在 Layout 的 Outlet 位置渲染 SettingsPage 组件。 - /profile:匹配到 ProfilePage,并在 Layout 的 Outlet 位置渲染 ProfilePage 组件。 默认路由: - 当用户访问根路径 / 时,由于没有指定具体的子路径,默认会渲染 CustomDashboard 组件(通过 index: true 指定)。

useOutlet 的作用: useOutlet 在 CustomDashboard 组件中使用,用于获取当前 Outlet 渲染的内容。

功能:useOutlet 可以访问并返回当前匹配的嵌套路由组件。 逻辑: - 当访问根路径 / 时,useOutlet 返回 null,因为没有嵌套路由匹配。这时,CustomDashboard 显示欢迎信息。 - 当访问 /settings 或 /profile 时,useOutlet 返回相应的组件 (SettingsPage 或 ProfilePage) 并渲染它们。

tip: index 路由:在 App.jsx 中,<Route index element={<CustomDashboard />} /> 表示当用户访问 /dashboard 时,默认渲染 CustomDashboard 组件。如果没有其他子路由匹配时,CustomDashboard 会被渲染。index 路由:用于定义父路径的默认子路由,在没有特定子路径匹配时渲染。

CustomDashboard 中的 useOutlet

**作为子组件的默认页面**:在这种情况下,CustomDashboard 用于处理默认页面,只有在没有特定子路由匹配时才会渲染。它和其他子路由是互斥的。 - 作为默认页面使用:CustomDashboard 配置在子路由中,作为默认页面。当没有具体子路由匹配时,显示默认内容。示例路径:/dashboard 显示 CustomDashboard,/dashboard/settings 显示 SettingsPage。

{
  path: "dashboard",
  element: <DashboardPage />,
  children: [
    {
      index: true,
      element: <CustomDashboard />
    },
    {
      path: "settings",
      element: <SettingsPage />
    },
    {
      path: "profile",
      element: <ProfilePage />
    }
  ]
}

**作为父组件中的中间层**:在这种情况下,CustomDashboard 作为一个中间层组件,用于包裹其他子组件,并且可以在此基础上进行一些扩展和条件判断。 - 作为中间层使用:CustomDashboard 配置在父路由中,作为中间层包裹其他子路由,可以扩展和进行条件判断。示例路径:/dashboard 显示 DashboardPage,/dashboard/settings 显示 SettingsPage,CustomDashboard 作为父层组件在这些路由之上添加额外逻辑或内容。

{
 path: "dashboard",
 element: <CustomDashboard />,
 children: [
   {
     index: true,
     element: <DashboardPage />
   },
   {
     path: "settings",
     element: <SettingsPage />
   },
   {
     path: "profile",
     element: <ProfilePage />
   }
 ]
}

路由配置 和 路由渲染 这两个分别是什么?

路由配置:定义应用的路由规则和路径映射,包括:路径(path)、组件(element)、嵌套路由等等 路由渲染:在运行时根据当前 URL 和路由配置渲染匹配的组件,通常使用 Outlet 组件来处理嵌套路由内容。

路由配置一般有2种方案

第一种使用JSX语法配置路由

import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import HomePage from './HomePage';
import AboutPage from './AboutPage';
import DashboardPage from './DashboardPage';
import Layout from './Layout';
import ErrorPage from './ErrorPage';

const App = () => (
  <Router>
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<HomePage />} />
        <Route path="about" element={<AboutPage />} />
        <Route path="dashboard" element={<DashboardPage />} />
      </Route>
      <Route path="*" element={<ErrorPage />} />
    </Routes>
  </Router>
);

export default App;

第二种使用 createBrowserRouter/createHashRouter

import React from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import HomePage from './HomePage';
import AboutPage from './AboutPage';
import DashboardPage from './DashboardPage';
import Layout from './Layout';
import ErrorPage from './ErrorPage';

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <HomePage />
      },
      {
        path: "about",
        element: <AboutPage />
      },
      {
        path: "dashboard",
        element: <DashboardPage />
      }
    ],
  },
]);

const App = () => (
  <RouterProvider router={router} />
);

export default App;

路由渲染

import React from 'react';
import { Outlet, Link } from 'react-router-dom';

const Layout = () => (
  <div>
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/dashboard">Dashboard</Link>
    </nav>
    <hr />
    <Outlet />
  </div>
);

export default Layout;

layout 组件包含一个导航栏和一个 Outlet 组件。Outlet 是路由渲染的核心部分,负责在匹配到子路由时渲染对应的组件。 Outlet 不仅可以用于渲染嵌套路由,还可以用来渲染顶级路由的默认页面或任何指定的子路由内容。通过正确配置 Outlet 和路由结构,可以实现复杂的页面布局和路由渲染逻辑。

Link 是一个用于创建导航链接的组件。点击 Link 会使浏览器的地址栏 URL 更新,同时不会触发页面刷新。用于在应用内部进行导航,从而避免页面刷新,提高用户体验。

import React from 'react';
import { Link } from 'react-router-dom';

const Navigation = () => (
  <nav>
    <Link to="/">Home</Link>
    <Link to="/about">About</Link>
    <Link to="/dashboard">Dashboard</Link>
  </nav>
);

export default Navigation;
  1. to:目标路径(必需)
  2. replace:是否替换当前历史记录(可选)

NavLink 是一个特殊的 Link,用于创建导航链接,并能够根据当前 URL 路径自动添加样式或类名。用于导航菜单,在用户访问相应路径时,可以自动高亮显示当前激活的链接。

import React from 'react';
import { NavLink } from 'react-router-dom';

const Navigation = () => (
  <nav>
    <NavLink to="/" end>Home</NavLink>
    <NavLink to="/about">About</NavLink>
    <NavLink to="/dashboard">Dashboard</NavLink>
  </nav>
);

export default Navigation;
  1. to:目标路径(必需)
  2. end:精确匹配,当 end 属性设置为 true 时,NavLink 只有在路径完全匹配时才会被激活。
  3. className:自定义类名,可以是一个字符串或函数(可选),可以是一个字符串,表示固定的类名。可以是一个函数,返回一个类名字符串。这个函数接收一个参数 isActive,根据 isActive 动态地返回类名。
  4. style:自定义样式,可以是一个对象或函数(可选)
  5. isActive:判断链接是否激活的布尔值(可选)
<NavLink
  to="/"
  end
  style={({ isActive }) => ({
    fontWeight: isActive ? 'bold' : 'normal',
    color: isActive ? 'green' : 'blue'
  })}
>
  Profile
</NavLink>

它们的基本功能相同,都是用于创建导航链接。但 NavLink 具有一些 Link 不具备的功能:

  1. 自动激活样式和类名:NavLink 会根据当前 URL 路径自动添加激活样式或类名,以便在用户导航时指示当前页面。
  2. 精确匹配(exact matching):NavLink 可以通过 end 属性实现路径的精确匹配,只有当路径完全匹配时才应用激活样式。

Navigate 和 useNavigate 是 React Router 提供的两个工具,用于在应用中实现编程式导航。它们提供了比 更灵活的导航方式,可以在事件处理函数、效果钩子(effects)等地方使用。

Navigate 是一个用于在渲染时进行导航的组件。当该组件渲染时,会立即导航到指定的路径。

import React from 'react';
import { Navigate } from 'react-router-dom';

const RedirectToHome = ({ condition }) => {
  if (condition) {
    return <Navigate to="/" />;
  }
  return <div>Condition not met</div>;
};

export default RedirectToHome;

useNavigate:定义:useNavigate 是一个 React Hook,返回一个函数,该函数用于执行编程式导航。用于在事件处理函数或效果钩子中进行导航,例如在表单提交后导航到另一个页面。

import React from 'react';
import { useNavigate } from 'react-router-dom';

const LoginForm = () => {
  const navigate = useNavigate();

  const handleSubmit = (event) => {
    event.preventDefault();
    // Perform login logic here
    navigate('/dashboard');
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Login</button>
    </form>
  );
};

export default LoginForm;
  • Navigate:用于在渲染时立即导航到指定路径,常用于条件重定向或自动重定向。
  • useNavigate:用于在事件处理函数或效果钩子中进行编程式导航,提供了更灵活的导航方式。

useMatches和useLocation

路由信息访问:useMatches和useLocation用于访问当前路由和位置的信息。

useMatches 是一个 Hook,用于获取当前路由匹配的所有路径段(matches)。useMatches 不需要任何参数。它返回一个数组,包含每个匹配的路由对象。用于获取当前路由匹配的详细信息,特别是在嵌套路由中,可以用来获取父级和子级的路由信息。其中每个元素是一个对象,表示一个路由匹配的详细信息。每个对象可能包含以下属性:

  • id:路由的唯一标识符。
  • pathname:匹配的路径。
  • params:路径参数的对象。
  • data:通过路由加载的任何数据。
  • handle:路由的处理程序。
import React from 'react';
import { useMatches } from 'react-router-dom';

const MatchesInfo = () => {
  const matches = useMatches();

  return (
    <div>
      <h2>Current Matches</h2>
      <ul>
        {matches.map((match, index) => (
          <li key={index}>
            Path: {match.pathname}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default MatchesInfo;

useLocation 是一个 Hook,用于获取当前的位置信息。useLocation 不需要任何参数。它返回一个包含当前路径信息的对象,例如 pathname、search 和 hash。用于在组件中访问当前的 URL 信息,可以根据当前路径或查询参数做出相应的处理。返回一个对象,包含以下属性:

  • pathname:当前的路径名。
  • search:URL 中的查询字符串部分,包括问号。
  • hash:URL 中的哈希部分,包括井号。
  • state:通过导航传递的状态对象。
  • key:唯一标识位置的字符串。
import React from 'react';
import { useLocation } from 'react-router-dom';

const LocationInfo = () => {
  const location = useLocation();

  return (
    <div>
      <h2>Current Location</h2>
      <p>Pathname: {location.pathname}</p>
      <p>Search: {location.search}</p>
      <p>Hash: {location.hash}</p>
      <p>State: {JSON.stringify(location.state)}</p>
    </div>
  );
};

export default LocationInfo;