面试题 #

1. 什么是 React Router V6,它如何在 React 应用中工作? #

React Router v6 是 React Router 库的最新主要版本,它是用于在 React 应用程序中添加页面路由(导航)功能的标准库。React Router v6 带来了一些重要的更新和改进,使路由更加简洁和高效。以下是 React Router v6 的一些关键特性及其在 React 应用中的工作方式:

关键特性

  1. 更简洁的 API:

    • React Router v6 简化了 API,减少了配置的复杂性。例如,<Switch>被替换为<Routes>,提供了更直观的路由匹配。
  2. 自动路由选择:

    • 在 v6 中,路由选择变得更加智能。<Routes>组件会自动选择最佳匹配的路由,而不需要手动指定。
  3. 性能改进:

    • v6 引入了性能优化,包括更有效的路由匹配和渲染逻辑。
  4. 相对路由和链接:

    • 支持相对路由和链接,使得嵌套路由的构建更加灵活和简单。
  5. 新的导航钩子:

    • 引入了新的钩子(Hooks),如useNavigateuseSearchParams,为导航和参数处理提供了更多的灵活性。
  6. 增强的路由元素:

    • 路由元素(如<Route>)现在可以直接接收组件作为其子元素,而不是通过componentrender属性。

工作方式

在 React 应用中,React Router v6 通过以下方式工作:

  1. 安装和设置:

    • 首先,通过 npm 或 yarn 安装 React Router 库。然后,在 React 应用的顶层组件中,使用<BrowserRouter>(或其他路由器类型)包裹应用的内容。
  2. 配置路由:

    • 使用<Routes>组件来定义应用的路由结构。在<Routes>内部,使用<Route>组件来定义每个路由的路径和对应的组件。
  3. 导航和链接:

    • 使用<Link>组件来创建导航链接,允许用户在应用中的不同页面间导航。
    • 使用useNavigate钩子在组件中编程式地导航。
  4. 参数和查询处理:

    • 使用useParams钩子来访问动态路由参数。
    • 使用useSearchParams钩子来处理 URL 查询参数。
  5. 嵌套路由:

    • 在 v6 中,可以更容易地创建嵌套路由。在父路由组件内部定义子路由,使得路由结构更加清晰和模块化。
  6. 重定向和守卫:

    • 使用<Navigate>组件来实现重定向。
    • 通过条件渲染和钩子来实现路由守卫,如保护私有路由。

React Router v6 的这些特性和工作方式使得在 React 应用中实现复杂的路由需求变得更加简单和高效。

2.请解释 React Router v6 中BrowserRouter和HashRouter的区别? #

在 React Router v6 中,<BrowserRouter><HashRouter>是两种不同的路由器组件,它们提供了不同的方式来处理 URL 和导航。理解这两者的区别对于选择适合特定应用的路由策略非常重要。

<BrowserRouter>

  1. 使用 HTML5 历史 API:

    • <BrowserRouter>使用 HTML5 的历史 API(特别是pushStatereplaceStatepopstate事件)来保持 UI 和 URL 的同步。
  2. 干净的 URL:

    • 这种方式产生的 URL 看起来很干净,没有额外的字符。例如:http://example.com/about
  3. 服务器配置:

    • 使用<BrowserRouter>时,服务器必须正确配置,以便对所有可能的 URL 路径返回同一个 HTML 文件(通常是index.html)。这是因为应用的路由是前端控制的,服务器不会对每个路由有实际的页面。
  4. 适用场景:

    • <BrowserRouter>适用于大多数单页应用(SPA),特别是在你可以控制服务器行为时。

<HashRouter>

  1. 使用 URL 的哈希部分:

    • <HashRouter>使用 URL 的哈希部分(即#后面的部分)来保持 UI 和 URL 的同步。它不依赖于 HTML5 历史 API。
  2. 带哈希的 URL:

    • 这种方式产生的 URL 包含#,例如:http://example.com/#/about。哈希后面的部分表示应用的前端路由。
  3. 无需特殊服务器配置:

    • 由于 URL 的路径部分(#之前的部分)不变,服务器只需对根 URL 返回应用即可。这意味着不需要特殊配置,因为服务器不会看到哈希后的路径。
  4. 适用场景:

    • <HashRouter>适用于那些不能或不想在服务器上进行特殊配置的场景。它也是在文件系统(如file://)或无服务器环境中运行 React 应用的好选择。

总结

在实际应用中,选择哪种路由器通常取决于你对项目的部署环境和 URL 外观的需求。

3. 如何在 React Router v6 中配置路由? #

在 React Router v6 中配置路由涉及几个关键步骤,包括设置路由器(如<BrowserRouter><HashRouter>),定义路由路径和关联的组件,以及在应用中使用链接进行导航。以下是配置路由的基本步骤:

1. 安装 React Router

首先,确保你已经安装了 React Router v6。如果没有,你可以通过 npm 或 yarn 来安装它:

npm install react-router-dom@6

或者

yarn add react-router-dom@6

2. 设置路由器

在 React 应用的顶层组件中,使用<BrowserRouter>(或<HashRouter>,取决于你的需求)包裹应用的内容。这通常在index.jsApp.js文件中完成:

import { BrowserRouter } from "react-router-dom";

function App() {
  return <BrowserRouter>{/* 应用的其余部分 */}</BrowserRouter>;
}

export default App;

3. 定义路由

使用<Routes>组件来定义应用的路由结构,并在其中使用<Route>组件来指定每个路由的路径和对应的组件。这通常在你的应用的主组件或专门的路由配置文件中完成:

import { Routes, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import NotFound from "./NotFound";

function AppRoutes() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="about" element={<About />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

在这个例子中,Home组件将在访问根路径/时渲染,About组件将在访问/about时渲染,而NotFound组件将用于处理所有未匹配的路由(通常用于显示 404 页面)。

4. 使用链接进行导航

在应用的其他部分,使用<Link>组件来创建导航链接,允许用户在不同页面间进行导航:

import { Link } from "react-router-dom";

function Navbar() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
    </nav>
  );
}

5. 嵌套路由(可选)

如果你的应用需要嵌套路由,你可以在<Route>组件内部再定义<Route>组件:

<Route path="dashboard" element={<Dashboard />}>
  <Route path="profile" element={<Profile />} />
  <Route path="settings" element={<Settings />} />
</Route>

在这个例子中,Dashboard组件可以包含用于渲染ProfileSettings组件的<Outlet>组件,这些子路由将在访问/dashboard/profile/dashboard/settings时渲染。

4. 请解释 React Router v6 中Link组件与普通a标签的区别 #

在 React Router v6 中,<Link>组件和传统的 HTML <a>标签都用于创建导航链接,但它们在处理页面导航时有重要的区别:

<Link>组件

  1. 单页应用(SPA)导航:

    • <Link>组件用于在 React 的单页应用中创建内部导航链接。当用户点击这些链接时,<Link>会告诉 React Router 更新 URL,但不会导致页面重新加载。
  2. 不触发全页刷新:

    • 使用<Link>进行导航时,页面不会重新加载。这意味着应用不会丢失当前状态,并且页面切换更快,因为只有必要的组件会被重新渲染。
  3. 与 React Router 集成:

    • <Link>组件是 React Router 的一部分,它知道如何与 React Router 的路由系统协同工作。它使用to属性来指定目标路由,例如 <Link to="/about">About</Link>
  4. 自定义行为:

    • 你可以轻松地向<Link>添加自定义行为或样式,例如,根据当前路由路径改变链接的样式。

<a>标签

  1. 传统的 HTML 链接:

    • <a>标签是 HTML 中的标准元素,用于创建指向不同页面的链接。在 React 应用中,它们通常用于指向应用外部的页面。
  2. 触发页面刷新:

    • 当使用<a>标签进行导航时,浏览器会进行全页刷新,加载新页面。这意味着应用的当前状态会丢失,且页面加载可能更慢。
  3. 不了解 React Router:

    • <a>标签不了解 React Router 的存在。它们只是简单地根据href属性的值改变浏览器的 URL,并加载新页面。
  4. 适用于外部链接:

    • 在 React 应用中,你通常会使用<a>标签来创建指向外部网站或页面的链接,例如 <a href="https://example.com">Visit Example.com</a>

总结

通过使用<Link>,React Router 能够优化导航体验,保持应用状态,并提供更快的页面切换,这对于提升用户体验至关重要。

5. 如何在 React Router v6 中传递和接收参数? #

在 React Router v6 中传递和接收参数主要涉及使用 useParams 钩子和定义路由参数。下面是基本步骤:

定义带参数的路由

首先,在你的路由定义中,你需要指定参数。例如,如果你有一个用户详情页面,你可能会这样定义路由:

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import UserDetails from "./UserDetails";
function App() {
  return (
    <Router>
      <Routes>
        <Route path="/user/:userId" element={<UserDetails />} />
        {/* 其他路由 */}
      </Routes>
    </Router>
  );
}

在这个例子中,:userId 是一个路由参数。

在组件中接收参数

UserDetails 组件中,你可以使用 useParams 钩子来访问这个参数:

import { useParams } from "react-router-dom";

function UserDetails() {
  let { userId } = useParams();

  // 使用 userId 来获取用户详情
  // ...

  return <div>{/* 渲染用户详情 */}</div>;
}

传递参数

当你想导航到这个路由并传递参数时,你可以使用 Link 组件或编程式导航:

import { Link } from "react-router-dom";

function UserList() {
  return (
    <div>
      {/* 假设 users 是一个用户数组 */}
      {users.map((user) => (
        <Link to={`/user/${user.id}`} key={user.id}>
          {user.name}
        </Link>
      ))}
    </div>
  );
}

或者使用编程式导航:

import { useNavigate } from "react-router-dom";

function SomeComponent() {
  let navigate = useNavigate();

  function goToUser(userId) {
    navigate(`/user/${userId}`);
  }
}

在 React Router v6 中传递查询参数(也称为搜索参数)也是一个重要的功能。这些参数通常用于在 URL 中传递非结构化数据,例如搜索词或分页信息。下面是如何传递和接收查询参数的步骤:

传递查询参数

查询参数可以通过 Link 组件或编程式导航添加到 URL 中。例如,假设你想根据搜索词过滤用户列表:

使用 Link 组件:

import { Link } from "react-router-dom";

function SearchLink({ query }) {
  return <Link to={`/search?query=${encodeURIComponent(query)}`}>搜索</Link>;
}

使用编程式导航:

import { useNavigate } from "react-router-dom";

function SearchButton({ query }) {
  let navigate = useNavigate();

  function handleSearch() {
    navigate(`/search?query=${encodeURIComponent(query)}`);
  }

  return <button onClick={handleSearch}>搜索</button>;
}

在这些示例中,encodeURIComponent 函数用于确保查询字符串是正确编码的。

接收查询参数

在目标组件中,你可以使用 useSearchParams 钩子来访问查询参数。例如,如果你有一个用于显示搜索结果的组件:

import { useSearchParams } from "react-router-dom";

function SearchResults() {
  let [searchParams] = useSearchParams();
  let query = searchParams.get("query");

  // 使用 query 来获取和显示搜索结果
  // ...

  return <div>{/* 渲染搜索结果 */}</div>;
}

在这个例子中,useSearchParams 提供了一个接口来读取和修改当前位置的搜索参数。你可以使用 get 方法来检索特定的查询参数。

6. 请解释 React Router v6 中的withRouter高阶组件的作用 #

在 React Router v6 中,withRouter 高阶组件(HOC)已经不再使用。在以前的版本中(特别是 v4 和 v5),withRouter 是一个非常有用的工具,它能够将路由的 props(如 history, location, match 等)注入到一个不是直接通过路由渲染的组件中。这对于那些需要访问路由信息但不是路由组件的情况特别有用。

在 React Router v6 中的变化

在 React Router v6 中,设计理念发生了一些变化,使得 withRouter HOC 变得不必要:

  1. 使用钩子(Hooks): React Router v6 鼓励使用自定义钩子(如 useNavigate, useLocation, useParams, useSearchParams 等)来访问路由相关信息。这些钩子提供了一种更简洁和直观的方式来获取和操作路由状态。

  2. 组件组合: v6 更加侧重于使用 React 的组件组合模式,而不是依赖于高阶组件。这使得组件的重用和测试更加简单直接。

  3. 改进的路由结构: v6 采用了更简化和一致的路由结构,减少了对额外工具或模式的需求。

替代方案

如果你是从旧版本迁移到 v6 并且以前依赖于 withRouter,你可以使用以下替代方案:

例如:

import { useNavigate, useLocation, useParams } from "react-router-dom";

function withRouter(Component) {
  return function WrapperComponent(props) {
    const navigate = useNavigate();
    const location = useLocation();
    const params = useParams();

    return (
      <Component
        navigate={navigate}
        location={location}
        params={params}
        {...props}
      />
    );
  };
}

使用这种方式,你可以为类组件提供类似于 withRouter 的功能。但通常推荐的方法是尽可能使用函数组件和钩子,以充分利用 React Router v6 的新特性。

7.如何在 React Router v6 中实现路由守卫(例如,用于身份验证的路由)? #

在 React Router v6 中实现路由守卫,特别是用于身份验证的路由,通常涉及创建一个高阶组件或使用钩子(Hooks)来控制对特定路由的访问。这里有两种常见的方法来实现路由守卫:

方法 1:使用高阶组件(HOC)

你可以创建一个高阶组件(HOC),它包裹目标组件,并根据用户的认证状态决定是否允许访问该组件。

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

const RequireAuth = ({ children }) => {
  const isAuthenticated = /* 逻辑来检查用户是否认证 */;

  if (!isAuthenticated) {
    // 用户未认证,重定向到登录页面
    return <Navigate to="/login" />;
  }

  return children;
};

然后,在你的路由配置中使用这个高阶组件来保护需要认证的路由:

import { Routes, Route } from "react-router-dom";
import Dashboard from "./Dashboard";
import Login from "./Login";
import RequireAuth from "./RequireAuth";

function AppRoutes() {
  return (
    <Routes>
      <Route path="/login" element={<Login />} />
      <Route
        path="/dashboard"
        element={
          <RequireAuth>
            <Dashboard />
          </RequireAuth>
        }
      />
      {/* 其他路由 */}
    </Routes>
  );
}

方法 2:使用钩子(Hooks)

另一种方法是使用自定义钩子(如useAuth)来检查用户的认证状态,并在组件内部进行条件渲染或重定向。

首先,创建一个用于检查认证状态的钩子:

const useAuth = () => {
  // 实现检查用户认证状态的逻辑
  const user = /* 获取用户信息 */;
  return user && user.isAuthenticated;
};

然后,在需要保护的组件内使用这个钩子:

import { useNavigate } from 'react-router-dom';
import { useAuth } from './useAuth';

const Dashboard = () => {
  const isAuthenticated = useAuth();
  const navigate = useNavigate();

  if (!isAuthenticated) {
    // 用户未认证,重定向到登录页面
    navigate('/login');
    return null;
  }

  return (
    // 你的受保护组件内容
  );
};

在这种方法中,你可以在任何需要保护的组件内部使用useAuth钩子来检查用户是否认证,并根据认证状态进行相应的处理。

总结

这两种方法都可以有效地在 React Router v6 中实现路由守卫。选择哪一种取决于你的应用结构和个人偏好。高阶组件方法提供了一种集中控制访问权限的方式,而钩子方法则提供了更多的灵活性和细粒度控制。

8.React Router v5 与 v6 有什么主要区别? #

React Router v5 和 v6 之间有几个关键的区别,这些区别主要涉及路由配置、性能优化、API 的变化以及一些新特性的引入。了解这些区别对于在现有项目中升级或开始新项目时选择合适的版本非常重要。

1. 路由配置

2. 性能优化

3. API 变化

4. 新特性

5. 移除的特性

总结

React Router v6 带来了许多改进和新特性,包括更简洁的 API、性能优化、更好的嵌套路由支持和新的钩子。这些变化使得路由更加灵活和高效,但也需要开发者适应新的 API 和概念。对于新项目,建议使用 v6,但如果你正在维护一个基于 v5 的项目,需要考虑迁移的成本和好处。

9. 如何在 React Router v6 中处理 404 或“未找到”页面? #

在 React Router v6 中处理 404 或“未找到”页面的方法是使用一个没有 path 属性的 Route 组件,作为 Routes 组件的最后一个子元素。这个“无路径”路由将捕获所有未被前面的路由匹配的路径,从而充当一个通用的回退选项。以下是如何实现这一点的步骤:

步骤 1:创建一个 404 组件

首先,创建一个表示 404 页面的 React 组件:

function NotFound() {
  return (
    <div>
      <h2>404 Not Found</h2>
      <p>抱歉,您访问的页面不存在。</p>
    </div>
  );
}

步骤 2:在路由配置中添加 404 路由

然后,在你的路由配置中,添加一个不带 path 属性的 Route,并将其放在所有其他路由定义的最后:

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import NotFound from "./NotFound";

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        {/* ...其他路由... */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Router>
  );
}

在这个例子中,<Route path="*" element={<NotFound />}> 是捕获所有未匹配路径的路由。当用户访问一个不存在的路由时,React Router 将渲染 NotFound 组件。

注意

通过以上步骤,你可以在 React Router v6 中有效地处理 404 或“未找到”页面,为用户提供更好的导航和错误处理体验。

10. 请解释 React Router v6 中组件的用途及其如何使用。 #

在 React Router v6 中,<NavLink>组件是一种特殊类型的<Link>,用于创建导航菜单项。它的主要特点是可以根据当前的路由路径自动改变样式或类名,这对于指示当前激活的导航链接非常有用。

用途

<NavLink>的主要用途是在用户导航到与该链接相关的路由时,能够向用户反馈哪个链接是“活跃”的。这通常通过改变链接的样式或类名来实现,例如,改变字体颜色或背景色。

如何使用

使用<NavLink>时,你可以通过classNamestyle属性来定义活跃和非活跃状态下的样式。React Router v6 提供了一个函数形式的classNamestyle属性,这个函数接收一个状态对象,该对象包含isActive属性,表示链接是否活跃。

基本示例

import { NavLink } from "react-router-dom";

function Navigation() {
  return (
    <nav>
      <NavLink to="/" className={({ isActive }) => (isActive ? "active" : "")}>
        Home
      </NavLink>
      <NavLink
        to="/about"
        className={({ isActive }) => (isActive ? "active" : "")}
      >
        About
      </NavLink>
      {/* 其他链接 */}
    </nav>
  );
}

在这个示例中,当用户处于对应的路由时,对应的<NavLink>将获得'active'类名。

使用内联样式

你也可以使用内联样式来定义活跃状态:

<NavLink
  to="/profile"
  style={({ isActive }) =>
    isActive ? { color: "green", fontWeight: "bold" } : {}
  }
>
  Profile
</NavLink>

在这个例子中,当Profile链接活跃时,它的颜色会变为绿色,字体变为粗体。

总结

<NavLink>是 React Router 中用于创建导航链接的组件,它的特点是可以根据路由的活跃状态自动改变样式或类名。这使得它非常适合用于创建反映当前导航状态的菜单项。通过使用classNamestyle属性的函数形式,你可以根据链接是否活跃来动态地应用不同的样式。

11. 在 React Router v6 中,useHistory、useLocation、useParams和useRouteMatch这些钩子的作用是什么? #

在 React Router v6 中,useHistory, useLocation, useParams, 和 useRouteMatch 是一组重要的钩子(Hooks),它们提供了访问和操作路由状态的能力。不过,值得注意的是,从 v6 开始,useHistory 被替换为了 useNavigate。我将逐一解释它们的作用:

useNavigate (替代了 useHistory)

useLocation

useParams

useRouteMatch(在 v6 中已不推荐使用)

总结

这些钩子是 React Router 的核心部分,它们提供了访问和操作路由状态的能力,使得在 React 应用中的路由管理变得更加灵活和方便。在 v6 中,通过使用这些钩子,你可以有效地管理路由跳转、获取 URL 参数和路径信息,从而为用户创造一个流畅的导航体验。

12. 如何在 React Router 中实现嵌套路由? #

在 React Router v6 中实现嵌套路由是一个相对直观的过程。嵌套路由允许你在父路由组件内部定义子路由,这样可以更好地组织和管理复杂的路由结构。下面是实现嵌套路由的基本步骤:

1. 创建基本组件

首先,创建你的基本组件。例如,假设你有一个布局组件Layout,一个首页组件Home,和两个子页面组件AboutContact

const Layout = ({ children }) => (
  <div>
    <header>Header</header>
    <main>{children}</main>
    <footer>Footer</footer>
  </div>
);

const Home = () => <div>Home Page</div>;
const About = () => <div>About Page</div>;
const Contact = () => <div>Contact Page</div>;

2. 设置路由

在你的应用中设置路由,使用<Routes><Route>组件来定义路由结构。在父路由组件内部定义子路由。

import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="contact" element={<Contact />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

在这个例子中,Layout组件是顶层路由组件,它有三个子路由:Home(作为默认子路由,使用index属性),AboutContact

3. 在父组件中渲染Outlet

在父组件(在这个例子中是Layout)中,使用<Outlet>组件来渲染子路由。<Outlet>组件是 React Router 提供的一个特殊组件,用于渲染当前路由匹配的子路由组件。

import { Outlet } from "react-router-dom";

const Layout = () => (
  <div>
    <header>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/contact">Contact</Link>
      </nav>
    </header>
    <main>
      <Outlet /> {/* 子路由将在这里渲染 */}
    </main>
    <footer>Footer</footer>
  </div>
);

4. 使用链接导航

使用<Link>组件在应用中创建导航链接。这些链接将允许用户在不同的路由之间导航。

<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>

总结

通过以上步骤,你可以在 React Router v6 中实现嵌套路由。嵌套路由提供了一种强大的方式来组织和管理复杂的路由结构,使得代码更加模块化和易于维护。使用<Outlet>组件在父路由中渲染子路由是实现嵌套路由的关键。

13. 如何在 React Router v6 中管理状态? #

在 React Router v6 中管理状态,你可以利用几种不同的方法和技术。这些方法包括使用 React 的状态管理功能(如useStateuseContext钩子),结合全局状态管理库(如 Redux 或 MobX),以及利用路由本身的状态管理能力。下面是一些常见的方法来在 React Router v6 中管理状态:

1. 使用 React 的内置状态管理

2. 结合全局状态管理库

3. 使用路由的状态管理能力

4. 使用自定义钩子

总结

在 React Router v6 中管理状态的方法取决于你的具体需求。对于简单的或局部的状态管理,React 的内置钩子(如useStateuseContext)通常就足够了。对于更复杂的全局状态管理,你可能需要使用 Redux 或 MobX 等库。此外,利用 React Router 本身的状态传递能力也是一种有效的方法,特别是对于需要在路由间共享状态的场景。

14. 请解释 React Router v6 中的Redirect组件的作用 #

在 React Router v6 中,<Redirect> 组件已经被移除,取而代之的是使用 useNavigate 钩子或者 <Navigate> 组件来实现重定向。在以前的版本中,<Redirect> 被用来自动将用户从一个路由重定向到另一个路由。但在 v6 中,需要使用不同的方法来实现类似的功能。

使用 <Navigate> 组件实现重定向

<Navigate> 组件是 React Router v6 中用于重定向的主要方式。它可以在路由配置中使用,来代替应该渲染的组件,从而实现重定向。

例如,如果你想从旧的 URL 重定向到新的 URL,你可以这样做:

import { Routes, Route, Navigate } from "react-router-dom";

function App() {
  return (
    <Routes>
      <Route path="/old-path" element={<Navigate to="/new-path" replace />} />
      <Route path="/new-path" element={<NewComponent />} />
      {/* 其他路由 */}
    </Routes>
  );
}

在这个例子中,访问 /old-path 会立即重定向到 /new-path

使用 useNavigate 钩子实现编程式重定向

如果你需要在事件处理或者某个特定逻辑触发时进行重定向,可以使用 useNavigate 钩子。

import { useNavigate } from "react-router-dom";

function MyComponent() {
  let navigate = useNavigate();

  function handleClick() {
    navigate("/new-path"); // 重定向到新路径
  }

  return <button onClick={handleClick}>Go to New Path</button>;
}

注意

通过上述方法,你可以在 React Router v6 中有效地实现重定向功能,无论是在路由配置中还是在组件逻辑中。

1. React 路由原理 #

1.1 HashRouter #

public\index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #root{
            border:1px solid red;
        }
    </style>
</head>
<body>
    <div id="root"></div>
    <ul>
        <li><a href="#/a">/a</a></li>
        <li><a href="#/b">/b</a></li>
    </ul>
    <script>
        window.addEventListener('hashchange',()=>{
            console.log(window.location.hash);
            let pathname = window.location.hash.slice(1);//把最前面的那个#删除
            root.innerHTML = pathname;
        });

    </script>
</body>
</html>

1.2 BrowserRouter #

1.2.1 history #

浏览器的 history 对象是 JavaScript BOM(Browser Object Model)的一部分,它提供了与浏览器历史记录进行交互的方法和属性。这个对象可以让你在用户的浏览历史中移动,类似于在浏览器中点击前进和后退按钮。

注意,由于安全原因,大多数浏览器都限制了 history 对象的使用。例如,你不能查看用户的历史记录(只能知道历史记录的数量),也不能跨域修改历史记录。此外,pushStatereplaceState 方法可能会受到浏览器的限制,防止你过于频繁地调用它们。

1.2.1.1 history.length #

返回浏览器历史列表中的 URL 数量。这个数量包括当前页面。

1.2.1.2 history.back() #

与在浏览器中点击后退按钮相同,使用户导航到前一个历史记录。

1.2.1.3 history.forward #

与在浏览器中点击前进按钮相同,使用户导航到下一个历史记录。

1.2.1.4 history.go(n) #

使用户导航到他们的历史记录中的特定点。这个 n 参数是一个整数,表示相对于当前页面的位置。例如,history.go(1) 相当于 history.forward()history.go(-1) 相当于 history.back()

1.2.1.5 history.pushState(state, title, url) #

向历史堆栈添加一个新的状态和 URL,而不需要重新加载页面。state 参数是一个与添加的历史记录相关联的状态对象,title 参数是新页面的标题(但大多数浏览器目前都忽略这个参数),url 参数是新历史记录的 URL。

1.2.1.6 history.replaceState(state, title, url) #

更新当前历史记录的状态对象、标题和 URL,而不需要重新加载页面。参数的含义与 pushState 方法相同。

1.2.2 案例 #

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #root {
        border: 1px solid red;
        height: 20px;
        padding: 10px;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script>
      var historyObj = window.history;
      var root = document.getElementById("root");
      function updateContent(pathname) {
        root.innerHTML = pathname;
      }
      window.addEventListener("popstate", (event) => {
        updateContent(window.location.pathname);
      });
      (function (historyObj) {
        let oldPushState = history.pushState;
        historyObj.pushState = function (state, title, pathname) {
          let result = oldPushState.apply(history, arguments);
          updateContent(pathname);
          return result;
        };
      })(historyObj);
      setTimeout(
        () => historyObj.pushState({ page: 1 }, "page1", "/page1"),
        1000
      );
      setTimeout(
        () => historyObj.pushState({ page: 2 }, "page2", "/page2"),
        2000
      );
      setTimeout(
        () => historyObj.pushState({ page: 3 }, "page3", "/page3"),
        3000
      );
      setTimeout(() => historyObj.back(), 4000);
      setTimeout(
        () => historyObj.pushState({ page: 4 }, "page4", "/page4"),
        5000
      );
      setTimeout(() => historyObj.go(1), 6000);
    </script>
  </body>
</html>

2.使用基本路由 #

2.1 安装 #

react-router-dom 是 React 中一个非常重要的库,它可以用于在你的 React 应用程序中添加路由功能。基本上,你可以使用它在你的应用程序中的不同部分之间导航,就像在不同的页面之间导航一样,但不需要重新加载整个应用程序。

  1. HashRouter:和 BrowserRouter 类似,HashRouter 是一个顶层的路由组件,它会为你的应用程序创建一个路由环境。不同之处在于,HashRouter 使用 URL 的 hash(#)部分来保持 UI 和 URL 的同步。它在历史记录管理上,所有的页面跳转、后退都可以通过 hash 实现,可以用于那些不能使用 HTML5 history API 的老式浏览器。

  2. Routes:在React Router v6中,Routes取代了v5中的Switch组件。它用于包含多个 Route 组件,并且只渲染与当前路径最匹配的 Route。注意,在 v6 中,RoutesRoute 的使用方式有所变化,所有的 Route 都需要作为 Routes 的子组件。

  3. Route:这是最基本的组件,用于在应用程序中定义不同的路由。每个 Route 都有一个 path 属性,表示当 URL 与此路径匹配时应该渲染哪个组件。

在这个例子中,首先我们使用 HashRouter 包裹我们的应用程序。然后我们定义了 Routes 组件,所有的 Route 组件都被包含在其中。注意 Route 组件的新用法,我们将要渲染的组件作为 element 属性传递进去。当 URL 的路径与 Routepath 属性匹配时,就会渲染对应的 element

npm i react-router-dom history --save

2.2 src\index.js #

// 引入React核心库
import React from "react";
// 引入ReactDOM,用于DOM操作
import ReactDOM from "react-dom/client";
// 引入react-router-dom中的HashRouter, BrowserRouter, Routes, Route组件
import { HashRouter, BrowserRouter, Routes, Route } from "./react-router-dom";
// 引入Home组件
import Home from "./components/Home";
// 引入User组件
import User from "./components/User";
// 引入Profile组件
import Profile from "./components/Profile";
// 创建并渲染根组件
ReactDOM.createRoot(document.getElementById("root")).render(
  // 使用HashRouter包裹整个应用,实现哈希路由功能
  <HashRouter>
    // 定义路由规则
    <Routes>
      // 定义主页路由,当URL为'/'时渲染Home组件
      <Route path="/" element={<Home />} />
      // 定义用户页面路由,当URL为'/user'时渲染User组件
      <Route path="/user" element={<User />} />
      // 定义个人资料页面路由,当URL为'/profile'时渲染Profile组件
      <Route path="/profile" element={<Profile />} />
    </Routes>
  </HashRouter>
);

这段代码包括了React的引入,React DOM的引入,以及react-router-dom中的路由组件的引入。接着,它引入了三个自定义组件(Home、User、Profile),并使用ReactDOM.createRoot来渲染一个包含了HashRouterRoutes的React组件树。这为应用程序提供了基于哈希的路由功能,允许应用程序根据URL的不同部分来显示不同的组件。

2.3 Home.js #

src\components\Home.js

import React from "react";
function Home(props) {
  console.log(props);
  return <div>Home</div>;
}
export default Home;

2.4 User.js #

src\components\User.js

import React from "react";
function User() {
  return <div>User</div>;
}
export default User;

2.5 Profile.js #

src\components\Profile.js

import React from "react";
function Profile() {
  return <div>Profile</div>;
}
export default Profile;

3.实现基本路由 #

3.1 react-router-dom\index.js #

src\react-router-dom\index.js

// 导入React
import React from "react"; 
 // 从react-router中导入Router组件
import { Router } from "../react-router";
// 从history库中导入createHashHistory和createBrowserHistory函数
import { createHashHistory, createBrowserHistory } from "history"; 
// 导出react-router中的所有内容
export * from "../react-router"; 
// 定义一个HashRouter函数组件,接收children作为props
export function HashRouter({ children }) {
  // 使用useRef创建一个可变的ref对象,用于存储历史对象的引用
  let historyRef = React.useRef(); 
   // 如果historyRef的current属性为null,表示尚未创建历史对象
  if (historyRef.current == null) {
   // 创建一个哈希历史对象并赋值给historyRef的current属性
    historyRef.current = createHashHistory(); 
  }
  // 获取当前的历史对象
  let history = historyRef.current; 
  // 使用useState创建一个状态,存储当前的动作和位置
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location,
  });
  //使用useLayoutEffect在历史对象发生变化时更新状态
  React.useLayoutEffect(() => history.listen(setState), [history]);
  // 渲染Router组件,并传递children、位置、导航类型和历史对象
  return (
    <Router
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  ); 
}
// 定义一个BrowserRouter函数组件,接收children作为props
export function BrowserRouter({ children }) {
  // 使用useRef创建一个可变的ref对象,用于存储历史对象的引用
  let historyRef = React.useRef(); 
   // 如果historyRef的current属性为null,表示尚未创建历史对象
  if (historyRef.current == null) {
    // 创建一个浏览器历史对象并赋值给historyRef的current属性
    historyRef.current = createBrowserHistory();
  }
  // 获取当前的历史对象
  let history = historyRef.current; 
  // 使用useState创建一个状态,存储当前的动作和位置
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location,
  });
   // 使用useLayoutEffect在历史对象发生变化时更新状态
  React.useLayoutEffect(() => history.listen(setState), [history]);
  //渲染Router组件,并传递children、位置、导航类型和历史对象
  return (
    <Router
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
}

这段代码定义了两个 React 组件:HashRouterBrowserRouter。这两个组件都利用react-routerhistory库来创建和管理路由。HashRouter使用 URL 的哈希部分来处理路由,而BrowserRouter使用 HTML5 的历史 API。两者都通过监听历史对象的变化来更新路由状态,并将更新的状态传递给Router组件以实现路由导航。这种方式为 React 应用程序提供了灵活的路由管理能力。

3.2 src\react-router\index.js #

src\react-router\index.js

// 导入React库
import React from "react";
// 创建导航上下文
const NavigationContext = React.createContext({});
// 创建位置上下文
const LocationContext = React.createContext({});
// 导出上下文
export { NavigationContext, LocationContext };
// 定义Router函数组件
export function Router({ children, location, navigator }) {
  // 使用useMemo钩子创建导航上下文对象
  const navigationContext = React.useMemo(() => ({ navigator }), [navigator]);
  // 使用useMemo钩子创建位置上下文对象
  const locationContext = React.useMemo(() => ({ location }), [location]);
  // 返回Router组件结构
  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider value={locationContext} children={children} />
    </NavigationContext.Provider>
  );
}
// 定义Routes函数组件
export function Routes({ children }) {
  // 使用useRoutes钩子创建路由
  return useRoutes(createRoutesFromChildren(children));
}
// 定义useLocation钩子
export function useLocation() {
  // 返回当前位置上下文的位置信息
  return React.useContext(LocationContext).location;
}
// 定义useSearchParams钩子
export function useSearchParams() {
  // 从位置上下文中获取当前位置
  const location = React.useContext(LocationContext).location;
  const pathname = location.pathname;
  // 返回当前URL的查询参数
  return new URLSearchParams(pathname.split("?")[1]);
}
// 定义useRoutes钩子
export function useRoutes(routes) {
  // 获取当前位置
  let location = useLocation(); 
  let pathname = location.pathname || "/"; 
  // 遍历路由数组匹配当前路径
  for (let i = 0; i < routes.length; i++) {
    let { path, element } = routes[i];
    let match = matchPath(path, pathname);
    if (match) {
      return element;
    }
  }
  return null;
}
// 从子元素创建路由数组
export function createRoutesFromChildren(children) {
  let routes = [];
  // 遍历子元素创建路由对象
  React.Children.forEach(children, (element) => {
    let route = {
      path: element.props.path,
      element: element.props.element,
    };
    routes.push(route);
  });
  return routes;
}
// 定义Route函数组件(空实现)
export function Route(props) {}
// 编译路径为正则表达式
function compilePath(path) {
  let regexpSource = "^" + path;
  regexpSource += "$";
  let matcher = new RegExp(regexpSource);
  return matcher;
}
// 匹配路径和当前路径名
export function matchPath(path, pathname) {
  let matcher = compilePath(path);
  let match = pathname.match(matcher);
  if (!match) return null;
  return match;
}

这段代码是一个React路由管理工具的实现,包括创建上下文(NavigationContextLocationContext),定义路由器(Router)和路由(RoutesRoute),以及一些自定义钩子(useLocation, useSearchParams, useRoutes)用于管理路由状态和解析URL参数。核心功能是根据React的子组件定义动态路由,以及提供路径匹配功能。代码风格注重模块化和可复用性,适合用于构建复杂的单页应用程序。

4.实现 history #

4.1 createBrowserHistory.js #

src\history\createBrowserHistory.js

// 定义 createBrowserHistory 函数
function createBrowserHistory() {
  // 获取全局历史对象
  const globalHistory = window.history;
  // 定义状态变量
  let state;
  // 初始化监听器数组
  let listeners = [];
  // 添加历史监听器
  function listen(listener) {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter(item => item !== listener);
    };
  }
  // 前进或后退指定的历史记录条数
  function go(n) {
    globalHistory.go(n);
  }
  // 后退一条历史记录
  function goBack() {
    globalHistory.go(-1);
  }
  // 前进一条历史记录
  function goForward() {
    globalHistory.go(1);
  }
  // 添加新的历史记录
  function push(pathname, nextState) {
    const action = 'PUSH';
    if (typeof pathname === 'object') {
      state = pathname.state;
      pathname = pathname.pathname;
    } else {
      state = nextState;
    }
    globalHistory.pushState(state, null, pathname);
    let location = { pathname, state };
    notify({ action, location });
  }
  // 替换当前历史记录
  function replace(pathname, nextState) {
    const action = 'REPLACE';
    if (typeof pathname === 'object') {
      state = pathname.state;
      pathname = pathname.pathname;
    } else {
      state = nextState;
    }
    globalHistory.replaceState(state, null, pathname);
    let location = { pathname, state };
    notify({ action, location });
  }
  // 监听浏览器历史变化
  window.addEventListener('popstate', () => {
    let location = {
      state: globalHistory.state,
      pathname: window.location.pathname
    };
    notify({ action: 'POP', location });
  });
  // 通知所有监听器
  function notify({ action, location }) {
    history.action = action;
    history.location = location;
    history.length = globalHistory.length;
    listeners.forEach(listener => {
      listener({ action, location });
    });
  }
  // 初始化 history 对象
  const history = {
    action: 'POP',
    go,
    goBack,
    goForward,
    push,
    replace,
    listen,
    location: {
      pathname: window.location.pathname,
      state: globalHistory.state
    }
  };
  // 返回 history 对象
  return history;
}
// 导出 createBrowserHistory 函数
export default createBrowserHistory;

createBrowserHistory 函数用于创建一个自定义的浏览器历史管理对象。这个对象允许应用程序监听和操作浏览器的历史记录。它提供了几个关键功能:添加新的历史记录(push)、替换当前记录(replace)、前进和后退(gogoBackgoForward)、以及订阅历史变化事件(listen)。这个函数利用了浏览器的 History APIpopstate 事件来实现这些功能。

4.2 createHashHistory.js #

src\history\createHashHistory.js

// 定义一个函数 createHashHistory
function createHashHistory() {
  // 声明一个栈来存储历史记录
  let stack = [];
  // 当前位置的索引
  let index = -1;
  // 当前动作(默认为'POP')
  let action = 'POP';
  // 当前的状态
  let state;
  // 监听函数数组
  let listeners = [];
  // 添加一个监听函数
  function listen(listener) {
    // 将监听器添加到数组中
    listeners.push(listener);
    // 返回一个函数,用于移除监听器
    return () => {
      listeners = listeners.filter(item => item !== listener);
    };
  }
  // 添加一个新的历史记录
  function push(pathname, nextState) {
    // 设置动作为'PUSH'
    action = 'PUSH';
    // 检查 pathname 是否为对象
    if (typeof pathname === 'object') {
      // 从对象中获取 state 和 pathname
      state = pathname.state;
      pathname = pathname.pathname;
    } else {
      // 使用提供的 nextState
      state = nextState;
    }
    // 更新浏览器的哈希值
    window.location.hash = pathname;
  }
  // 移动到历史记录中的某个位置
  function go(n) {
    // 设置动作为'POP'
    action = 'POP';
    // 更新索引
    index += n;
    // 获取新位置的历史记录
    let nextLocation = stack[index];
    // 更新状态
    state = nextLocation.state;
    // 更新浏览器的哈希值
    window.location.hash = nextLocation.pathname;
  }
  // 后退
  function goBack() {
    // 调用 globalHistory.go 函数后退一步
    globalHistory.go(-1);
  }
  // 前进
  function goForward() {
    // 调用 globalHistory.go 函数前进一步
    globalHistory.go(1);
  }
  // 处理哈希值变化的函数
  function handleHashChange() {
    // 获取新的 pathname
    const pathname = window.location.hash.slice(1);
    // 更新历史对象的 action
    history.action = action;
    // 构建新的 location 对象
    const location = {
      pathname,
      state
    };
    // 更新历史对象的 location
    history.location = location;
    // 如果是 PUSH 动作,更新栈和索引
    if (action === 'PUSH') {
      stack[++index] = location;
    }
    // 通知所有监听器
    listeners.forEach(listener => {
      listener({
        action,
        location
      });
    });
  }
  // 监听 hashchange 事件
  window.addEventListener('hashchange', handleHashChange);
  // 创建一个历史对象
  const history = {
    action: 'POP',
    go,
    goBack,
    goForward,
    push,
    listen,
    location: {
      pathname: undefined,
      state: undefined
    }
  };
  // 初始化历史对象
  if (window.location.hash) {
    action = 'PUSH';
    handleHashChange();
  } else {
    window.location.hash = '/';
  }
  // 返回创建的历史对象
  return history;
}
// 导出 createHashHistory 函数
export default createHashHistory;

4.3 history\index.js #

src\history\index.js

export { default as createBrowserHistory } from "./createBrowserHistory";
export { default as createHashHistory } from "./createHashHistory";

4.4 react-router-dom\index.js #

src\react-router-dom\index.js

import React from 'react'
import { Router } from '../react-router';
+import { createHashHistory, createBrowserHistory } from "../history";
export * from '../react-router';

export function HashRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createHashHistory();
    }
    let history = historyRef.current;
    let [state, setState] = React.useState({
        action: history.action,
        location: history.location
    });
    React.useLayoutEffect(() => history.listen(setState), [history]);
    return (
        <Router
            children={children}
            location={state.location}
            navigationType={state.action}
            navigator={history}
        />
    );
}

export function BrowserRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createBrowserHistory();
    }
    let history = historyRef.current;
    let [state, setState] = React.useState({
        action: history.action,
        location: history.location
    });
    React.useLayoutEffect(() => history.listen(setState), [history]);
    return (
        <Router
            children={children}
            location={state.location}
            navigationType={state.action}
            navigator={history}
        />
    );
}

5. path-to-regexp #

5.1 /home 结束 #

let {pathToRegexp} = require("path-to-regexp");
let regxp = pathToRegexp("/home", [], { end: true });
console.log(regxp);// /^\/home[\/#\?]?$/i
console.log(regxp.test("/home"));
console.log(regxp.test("/home/2"));

homereg

这个正则表达式 /^\/home[\/#\?]?$/i 用于匹配特定格式的字符串,具体解释如下:

  1. ^:表示字符串的开始。这个符号确保匹配必须从字符串的开始处进行。

  2. \/:匹配斜杠 /。在正则表达式中,斜杠是一个特殊字符,因此它前面有一个反斜杠用作转义字符。

  3. home:直接匹配文本 "home"。

  4. [\/#\?]:这是一个字符集,用于匹配集合中的任意一个字符。

    • \/:匹配一个斜杠 /
    • #:匹配井号 #
    • \?:匹配问号 ?。问号也是正则表达式中的特殊字符,所以需要用反斜杠进行转义。
  5. ?:表示前面的字符集 [\/#\?] 是可选的,即这个字符集中的字符出现零次或一次。

  6. $:表示字符串的结束。这个符号确保匹配必须在字符串的末尾结束。

  7. i:这是一个修饰符,表示匹配时不区分大小写。

综上所述,这个正则表达式用于匹配以下类型的字符串:

例如,它可以匹配 /home/Home/home//home#/home?,但不会匹配 /homepage/home/123

5.2 /home 非结束 #

let {pathToRegexp} = require("path-to-regexp");
let regExp = pathToRegexp("/home", [], { end: false });
console.log(regExp);
// /^\/home(?:[\/#\?](?=[]|$))?(?=[\/#\?]|[]|$)/i   
console.log(regExp.exec("/home"));
console.log(regExp.exec("/home/"));
console.log(regExp.exec("/home//"));
console.log(regExp.exec("/home/2"));
console.log(regExp.exec("/home#"));
console.log(regExp.exec("/home#top"));
console.log(regExp.exec("/home?"));
console.log(regExp.exec("/home?a=1"));
console.log(/^\/home(?:[\/#\?](?=[]|$))?/.exec("/home?a=1"));

这个正则表达式用于匹配特定的 URL 路径。让我们分解并理解它的每个部分:

  1. ^: 这个字符表示正则表达式的开始。它确保匹配必须从字符串的开头开始。

  2. \/home: 这部分匹配字符串 "/home"。斜杠 (/) 被转义(即前面有一个反斜杠 \),这是因为在正则表达式中,斜杠是一个特殊字符,需要转义来表示它的字面值。

  3. (?:[\/#\?](?=[]|$))?: 这是一个非捕获组(由 (?: ... ) 表示),它用于对一组字符进行分组,但不保存该分组用于后续的引用。这个组的内容是:

    • [\/#\?]: 这是一个字符集,匹配一个字符,可以是 /# 或者 ?
    • (?=[]|$): 这是一个零宽正向前瞻断言,表示接下来的位置必须是正则表达式 []|$ 的起始位置。由于 [] 是一个空的字符集,所以这实际上等同于只检查 $(字符串的结尾)。
    • ?: 表示前面的非捕获组是可选的。
  4. (?=[\/#\?]|[]|$): 这又是一个零宽正向前瞻断言,用于确保当前位置后面是 [/#?] 中的一个字符,或者是字符串的结尾。

  5. i: 这是一个修饰符,表示正则表达式的匹配是不区分大小写的。

这个正则表达式用于匹配以 /home 开头的字符串,后面可以选择性地跟着一个 /#? 字符。如果有这些字符之一,它们必须是字符串的结尾或者紧跟着字符串的结尾。由于使用了 i 修饰符,所以匹配是不区分大小写的。

这种模式通常用于路由匹配,如在 web 应用中检测是否导航到了 "/home" 路径,且路径可能附带查询参数或哈希值。

5.3 路径参数 #

let params = [];
let regExp = pathToRegExp("/user/:id", params, { end: true });
console.log(regExp, params);
/**
/^\/user\/(?:([^\/]+?))\/?$/i
[ { name: 'id', optional: false, offset: 7 } ]
**/

uerreg

5.4 正则匹配 #

表达式 含义
() 表示捕获分组,()会把每个分组里的匹配的值保存起来,使用$n(n 是一个数字,表示第 n 个捕获组的内容)
(?:) 表示非捕获分组,和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来
(?...) 表示命名捕获分组,反向引用一个命名分组的语法是 \k,在 replace() 方法的替换字符串中反向引用是用 $
//分组获取
console.log("1ab".match(/1[a-z]([b-c])/));
//分组不捕获
console.log("1ab".match(/1[a-z](?:[a-z])/));
//?<x> 表示命名捕获分组
console.log("11-22".replace(/(?<x>\d{2})-(?<y>\d{2})/, "$<y>-$<x>"));
//$1引用第一个捕获组
console.log("11-22".replace(/(?<x>\d{2})-(?<y>\d{2})/, "$2-$1"));
表达式 含义
(?=pattern) 正向肯定查找(前瞻),后面必须跟着什么
(?!pattern) 正向否定查找(前瞻),后面不能跟着什么
(?<=pattern) 反向肯定条件查找(后顾),不捕获
(?<!pattern) 反向否定条件查找(后顾)
//会消耗掉字符的
//console.log('1a'.match(/\d[a-z][a-z]/));
//?= 正向肯定查找 不消费字符 正向前瞻
//console.log('1a'.match(/\d(?=[a-z])[a-z]/));

//正向肯定前瞻
console.log("1a".match(/\d(?=[a-z])[a-z]/));
//正向否定前瞻
console.log("1a".match(/\d(?![A-Z])[a-z]/));
//反向肯定前瞻
console.log("1a".match(/(?<=[a-z])\d[a-z]/));
//反向否定前瞻
console.log("1a".match(/(?<![A-Z])\d[a-z]/));

let array = ["1ab"];
array.index = 0;
array.input = "1ab";
array.groups = undefined;
console.log(array);

6. 路径参数 #

6.1 src\index.js #

import React from "react";
import ReactDOM from "react-dom/client";
import { HashRouter, BrowserRouter, Routes, Route } from "./react-router-dom";
import Home from "./components/Home";
import User from "./components/User";
import Profile from "./components/Profile";
+import Post from './components/Post';
ReactDOM.createRoot(document.getElementById("root")).render(
<HashRouter>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/user" element={<User />} />
      <Route path="/profile" element={<Profile />} />
+     <Route path="/post/:id" element={<Post />} />
    </Routes>
  </HashRouter>);

6.2 src\react-router\index.js #

src\react-router\index.js

import React from "react";
const NavigationContext = React.createContext({});
const LocationContext = React.createContext({});
export { NavigationContext, LocationContext };
export function Router({
    children,
    location,
    navigator
}) {
    const navigationContext = React.useMemo(() => ({
        navigator
    }), [navigator]);
    const locationContext = React.useMemo(() => ({
        location
    }), [location]);
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider value={locationContext} children={children} />
        </NavigationContext.Provider>
    );
}
export function Routes({
    children
}) {
    return useRoutes(createRoutesFromChildren(children));
}
export function useLocation() {
    return React.useContext(LocationContext).location;
}
export function useSearchParams() {
    const location = React.useContext(LocationContext).location;
    const pathname = location.pathname;
    return new URLSearchParams(pathname.split("?")[1]);
}
export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname || "/";
    for (let i = 0; i < routes.length; i++) {
        let { path, element } = routes[i];
        let match = matchPath(path, pathname);
        if (match) {
            return element;
        }
    }
    return null;
}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, element => {
        let route = {
            path: element.props.path,
            element: element.props.element
        };
        routes.push(route);
    });
    return routes;
}
export function Route(props) { }
function compilePath(path) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/:(\w+)/g, (_, key) => {
            paramNames.push(key);
            return "([^\\/]+)";
        });
    regexpSource += "$";
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}
export function matchPath(path, pathname) {
    let [matcher, paramNames] = compilePath(path);
    let match = pathname.match(matcher);
    if (!match) return null;
    let [matchedPathname,...values] = match;
    let params = paramNames.reduce(
        (memo, paramName, index) => {
            memo[paramName] = values[index];
            return memo;
        },
        {}
    );
    return { params,matchedPathname };
}

6.3 Post.js #

src\components\Post.js

import React from "react";
function Post(props) {
  return <div>Post</div>;
}
export default Post;

7.1 src\index.js #

import React from "react";
import ReactDOM from "react-dom/client";
+import { HashRouter, BrowserRouter, Routes, Route,Link } from "./react-router-dom";
import Home from "./components/Home";
import User from "./components/User";
import Profile from "./components/Profile";
import Post from './components/Post';
ReactDOM.createRoot(document.getElementById("root")).render(
<HashRouter>
+  <ul>
+    <li><Link to="/">首页</Link></li>
+    <li><Link to="/user" >用户管理</Link></li>
+    <li><Link to="/profile" >个人中心</Link></li>
+  </ul>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/user" element={<User />} />
      <Route path="/profile" element={<Profile />} />
      <Route path="/post/:id" element={<Post />} />
    </Routes>
  </HashRouter>);

7.2 react-router-dom\index.js #

src\react-router-dom\index.js

import React from "react";
+import { Router,useNavigate  } from "../react-router";
import { createHashHistory, createBrowserHistory } from "../history";
export * from "../react-router";
export function HashRouter({
  children
}) {
  let historyRef = React.useRef();
  if (historyRef.current == null) {
    historyRef.current = createHashHistory();
  }
  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });
  React.useLayoutEffect(() => history.listen(setState), [history]);
  return <Router children={children} location={state.location} navigationType={state.action} navigator={history} />;
}
export function BrowserRouter({
  children
}) {
  let historyRef = React.useRef();
  if (historyRef.current == null) {
    historyRef.current = createBrowserHistory();
  }
  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });
  React.useLayoutEffect(() => history.listen(setState), [history]);
  return <Router children={children} location={state.location} navigationType={state.action} navigator={history} />;
}
+export function Link({ to, children }) {
+  const navigate = useNavigate();
+  return (
+    <a href={to} onClick={(event) => {
+      event.preventDefault();
+      navigate(to);
+    }} >{children}</a>
+  )
+}

7.3 react-router\index.js #

src\react-router\index.js

import React from "react";
const NavigationContext = React.createContext({});
const LocationContext = React.createContext({});
export { NavigationContext, LocationContext };
export function Router({
    children,
    location,
    navigator
}) {
    const navigationContext = React.useMemo(() => ({
        navigator
    }), [navigator]);
    const locationContext = React.useMemo(() => ({
        location
    }), [location]);
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider value={locationContext} children={children} />
        </NavigationContext.Provider>
    );
}
export function Routes({
    children
}) {
    return useRoutes(createRoutesFromChildren(children));
}
export function useLocation() {
    return React.useContext(LocationContext).location;
}
export function useSearchParams() {
    const location = React.useContext(LocationContext).location;
    const pathname = location.pathname;
    return new URLSearchParams(pathname.split("?")[1]);
}
export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname || "/";
    for (let i = 0; i < routes.length; i++) {
        let { path, element } = routes[i];
        let match = matchPath(path, pathname);
        if (match) {
            return element;
        }
    }
    return null;
}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, element => {
        let route = {
            path: element.props.path,
            element: element.props.element
        };
        routes.push(route);
    });
    return routes;
}
export function Route(props) { }
function compilePath(path) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/:(\w+)/g, (_, key) => {
            paramNames.push(key);
            return "([^\\/]+)";
        });
    regexpSource += "$";
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}
export function matchPath(path, pathname) {
    let [matcher, paramNames] = compilePath(path);
    let match = pathname.match(matcher);
    if (!match) return null;
    let [matchedPathname,...values] = match;
    let params = paramNames.reduce(
        (memo, paramName, index) => {
            memo[paramName] = values[index];
            return memo;
        },
        {}
    );
    return { params,matchedPathname };
}
+export function useNavigate() {
+    let { navigator } = React.useContext(NavigationContext);
+    let navigate = React.useCallback((to) => {
+        navigator.push(to);
+    }, [navigator]);
+    return navigate;
+}

8. 使用嵌套路由 #

8.1 src\index.js #

src\index.js

import React from "react";
import ReactDOM from "react-dom/client";
+import { HashRouter, BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Home from "./components/Home";
import User from "./components/User";
+import UserAdd from './components/UserAdd';
+import UserList from './components/UserList';
+import UserDetail from './components/UserDetail';
import Profile from "./components/Profile";
import Post from './components/Post';
ReactDOM.createRoot(document.getElementById("root")).render(
  <HashRouter>
    <ul>
      <li><Link to="/">首页</Link></li>
      <li><Link to="/user" >用户管理</Link></li>
      <li><Link to="/profile" >个人中心</Link></li>
    </ul>
    <Routes>
      <Route path="/" element={<Home />} />
+     <Route path="/user" element={<User />} >
+       <Route path="add" element={<UserAdd />} />
+       <Route path="list" element={<UserList />} />
+       <Route path="detail/:id" element={<UserDetail />} />
+     </Route>
    </Routes>
  </HashRouter>);

8.2 User.js #

src\components\User.js

import React from "react";
+import { Link, Outlet } from 'react-router-dom';
function User() {
  return (
+   <div>
+     <ul>
+       <li><Link to="/user/list">用户列表</Link></li>
+       <li><Link to="/user/add">添加用户</Link></li>
+     </ul>
+     <div>
+       <Outlet />
+     </div>
+   </div>
  )
}
export default User;

8.3 utils.js #

src\utils.js

export const UserAPI = {
    list() {
      let usersStr = localStorage.getItem("users");
      let users = usersStr ? JSON.parse(usersStr) : [];
      return users;
    },
    add(user) {
      let users = UserAPI.list();
      users.push(user);
      localStorage.setItem("users", JSON.stringify(users));
    },
    find(id) {
      let users = UserAPI.list();
      return users.find((user) => user.id === id);
    },
  };

8.4 UserAdd.js #

src\components\UserAdd.js

import React from "react";
import { useNavigate } from "react-router-dom";
import { UserAPI } from "../utils";
export default function UserAdd() {
  const navigate = useNavigate();
  const nameRef = React.useRef();
  const handleSubmit = (event) => {
    event.preventDefault();
    let name = nameRef.current.value;
    UserAPI.add({ id: Date.now() + "", name });
    navigate("/user/list");
  };
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={nameRef} />
      <button type="submit">添加</button>
    </form>
  );
}

8.5 UserList.js #

src\components\UserList.js

import React from "react";
import { Link } from "react-router-dom";
import { UserAPI } from "../utils";
export default function User() {
  const [users, setUsers] = React.useState([]);
  React.useEffect(() => {
    let users = UserAPI.list();
    setUsers(users);
  }, []);
  return (
    <ul>
      {users.map((user, index) => (
        <li key={index}>
          <Link to={`/user/detail/${user.id}`} state={user}>
            {user.name}
          </Link>
        </li>
      ))}
    </ul>
  );
}

8.6 UserDetail.js #

src\components\UserDetail.js

import React from "react";
import { useLocation, useParams } from "react-router-dom";
import { UserAPI } from "../utils";
export default function UserDetail(props) {
  const location = useLocation();
  const { id } = useParams();
  const [user, setUser] = React.useState({});
  React.useEffect(() => {
    let user = location.state;
    if (!user) {
      if (id) {
        user = UserAPI.find(id);
      }
    }
    if (user) setUser(user);
  }, []);
  return (
    <div>
      {user.id}:{user.name}
    </div>
  );
}

9.createRoutesFromChildren #

9.1 react-router\index.js #

src\react-router\index.js

import React from "react";
const NavigationContext = React.createContext({});
const LocationContext = React.createContext({});
export { NavigationContext, LocationContext };
export function Router({
    children,
    location,
    navigator
}) {
    const navigationContext = React.useMemo(() => ({
        navigator
    }), [navigator]);
    const locationContext = React.useMemo(() => ({
        location
    }), [location]);
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider value={locationContext} children={children} />
        </NavigationContext.Provider>
    );
}
export function Routes({
    children
}) {
    return useRoutes(createRoutesFromChildren(children));
}
export function useLocation() {
    return React.useContext(LocationContext).location;
}
export function useSearchParams() {
    const location = React.useContext(LocationContext).location;
    const pathname = location.pathname;
    return new URLSearchParams(pathname.split("?")[1]);
}
export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname || "/";
    for (let i = 0; i < routes.length; i++) {
        let { path, element } = routes[i];
        let match = matchPath(path, pathname);
        if (match) {
            return element;
        }
    }
    return null;
}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, element => {
        let route = {
            path: element.props.path,
            element: element.props.element
        };
+       if (element.props.children) {
+           route.children = createRoutesFromChildren(element.props.children);
+       }
        routes.push(route);
    });
    return routes;
}
export function Route(props) { }
function compilePath(path) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/:(\w+)/g, (_, key) => {
            paramNames.push(key);
            return "([^\\/]+)";
        });
    regexpSource += "$";
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}
export function matchPath(path, pathname) {
    let [matcher, paramNames] = compilePath(path);
    let match = pathname.match(matcher);
    if (!match) return null;
    let [matchedPathname, ...values] = match;
    let params = paramNames.reduce(
        (memo, paramName, index) => {
            memo[paramName] = values[index];
            return memo;
        },
        {}
    );
    return { params, matchedPathname };
}
export function useNavigate() {
    let { navigator } = React.useContext(NavigationContext);
    let navigate = React.useCallback((to) => {
        navigator.push(to);
    }, [navigator]);
    return navigate;
}

原始的 createRoutesFromChildren 函数遍历 React 子元素(通常是路由组件),为每个子元素创建一个路由对象。这个路由对象包含两个属性:

修改后的代码增加了以下部分:

if (element.props.children) {
    route.children = createRoutesFromChildren(element.props.children);
}

这个修改的作用是:

这样的修改使得函数能够处理嵌套路由。嵌套路由是指一个路由下面还有子路由,这在复杂的应用中很常见。例如,一个博客应用可能有一个主路由 /posts,而每篇文章(如 /posts/article-1)又是它的子路由。

通过这种方式,createRoutesFromChildren 函数不仅可以构建一个平面的路由数组,还能根据 React 组件树的嵌套结构创建一个层次化的路由结构,这对于构建具有复杂页面层次的应用是非常有用的。

10.flattenRoutes #

[
  {
    "routePath": "/",
    "routeMetas": [
      {
        "route": {
          "path": "/",
          "element": Home
        }
      }
    ]
  },
  {
    "routePath": "/user/add",
    "routeMetas": [
      {
        "route": {
          "path": "/user",
          "element": User
        }
      },
      {
        "route": {
          "path": "add",
          "element": UserAdd
        }
      }
    ]
  },
  {
    "routePath": "/user/list",
    "routeMetas": [
      {
        "route": {
          "path": "/user",
          "element": User
        }
      },
      {
        "route": {
          "path": "list",
          "element": UserList
        }
      }
    ]
  },
  {
    "routePath": "/user/detail/:id",
    "routeMetas": [
      {
        "route": {
          "path": "/user",
          "element": User
        }
      },
      {
        "route": {
          "path": "detail/:id",
          "element": UserDetail
        }
      }
    ]
  },
  {
    "routePath": "/user",
    "routeMetas": [
      {
        "route": {
          "path": "/user",
          "element": User
        }
      }
    ]
  }
]

10.1 react-router\index.js #

src\react-router\index.js

import { match } from "path-to-regexp";
import React from "react";
const NavigationContext = React.createContext({});
const LocationContext = React.createContext({});
export { NavigationContext, LocationContext };
export function Router({
    children,
    location,
    navigator
}) {
    const navigationContext = React.useMemo(() => ({
        navigator
    }), [navigator]);
    const locationContext = React.useMemo(() => ({
        location
    }), [location]);
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider value={locationContext} children={children} />
        </NavigationContext.Provider>
    );
}
export function Routes({
    children
}) {
    return useRoutes(createRoutesFromChildren(children));
}
export function useLocation() {
    return React.useContext(LocationContext).location;
}
export function useSearchParams() {
    const location = React.useContext(LocationContext).location;
    const pathname = location.pathname;
    return new URLSearchParams(pathname.split("?")[1]);
}
+export function Outlet(){
+    return null;
+}
+export function useParams(){
+    return {};
+}
export function useRoutes(routes) {
   let location = useLocation();
   let pathname = location.pathname;
+  matchRoutes(routes, pathname);
+  return null;
}
+function matchRoutes(routes, pathname) {
+   const branches = flattenRoutes(routes);
+   console.log('branches',branches);
+}
+function flattenRoutes(routes,branches=[],parentMetas=[],parentPath='') {
+    routes.forEach((route)=>{
+        let routeMeta = {
+            route
+        }
+        const routePath = joinPaths([parentPath,routeMeta.route.path])
+        const routeMetas = [...parentMetas,routeMeta];
+        if(route.children){
+            flattenRoutes(route.children,branches,routeMetas,routePath);
+        }
+        branches.push({
+            routePath,
+            routeMetas
+        })
+    })
+    return branches;
+}
+function joinPaths(paths) {
+    return paths.join('/').replace(/\/+/g,'/');
+}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, element => {
        let route = {
            path: element.props.path,
            element: element.props.element
        };
        if (element.props.children) {
            route.children = createRoutesFromChildren(element.props.children);
        }
        routes.push(route);
    });
    return routes;
}
export function Route(props) { }
function compilePath(path) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/:(\w+)/g, (_, key) => {
            paramNames.push(key);
            return "([^\\/]+)";
        });
    regexpSource += "$";
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}
export function matchPath(path, pathname) {
    let [matcher, paramNames] = compilePath(path);
    let match = pathname.match(matcher);
    if (!match) return null;
    let [matchedPathname, ...values] = match;
    let params = paramNames.reduce(
        (memo, paramName, index) => {
            memo[paramName] = values[index];
            return memo;
        },
        {}
    );
    return { params, matchedPathname };
}
export function useNavigate() {
    let { navigator } = React.useContext(NavigationContext);
    let navigate = React.useCallback((to) => {
        navigator.push(to);
    }, [navigator]);
    return navigate;
}

11.matchRouteBranch #

11.1 react-router\index.js #

src\react-router\index.js

import { match } from "path-to-regexp";
import React from "react";
const NavigationContext = React.createContext({});
const LocationContext = React.createContext({});
export { NavigationContext, LocationContext };
export function Router({
    children,
    location,
    navigator
}) {
    const navigationContext = React.useMemo(() => ({
        navigator
    }), [navigator]);
    const locationContext = React.useMemo(() => ({
        location
    }), [location]);
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider value={locationContext} children={children} />
        </NavigationContext.Provider>
    );
}
export function Routes({
    children
}) {
    return useRoutes(createRoutesFromChildren(children));
}
export function useLocation() {
    return React.useContext(LocationContext).location;
}
export function useSearchParams() {
    const location = React.useContext(LocationContext).location;
    const pathname = location.pathname;
    return new URLSearchParams(pathname.split("?")[1]);
}
export function Outlet() {
    return null;
}
export function useParams() {
    return {};
}
export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname;
    matchRoutes(routes, pathname);
    return null;
}
function matchRoutes(routes, pathname) {
+   const branches = flattenRoutes(routes);
+   let matches = null;
+   for (let i = 0; matches == null && i < branches.length; i++) {
+       matches = matchRouteBranch(branches[i], pathname);
+   }
+   console.log('matches', matches);
+   return matches;
}
+function matchRouteBranch(branch, pathname) {
+    const {routeMetas } = branch;
+    const matches = [];
+    let matchedParams = {};
+    let matchedPathname = '';
+    for (let i = 0; i < routeMetas.length; i++) {
+        const { route } = routeMetas[i];
+        const end = i === routeMetas.length - 1;
+        const remainingPathname = pathname.slice(matchedPathname.length);
+        const match = matchPath({ path: route.path, end }, remainingPathname);
+        if (!match) {
+            return null;
+        }
+        matchedParams = Object.assign({}, matchedParams, match.params);
+        matches.push({ route,params: matchedParams });
+        matchedPathname = joinPaths([matchedPathname, match.matchedPathname]);
+    }
+    return matches;
+}
function flattenRoutes(routes, branches = [], parentMetas = [], parentPath = '') {
    routes.forEach((route) => {
        let routeMeta = {
            route
        }
        const routePath = joinPaths([parentPath, routeMeta.route.path])
        const routeMetas = [...parentMetas, routeMeta];
        if (route.children) {
            flattenRoutes(route.children, branches, routeMetas, routePath);
        }
        branches.push({
            routePath,
            routeMetas
        })
    })
    return branches;
}
function joinPaths(paths) {
    return paths.join('/').replace(/\/+/g, '/');
}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, element => {
        let route = {
            path: element.props.path,
            element: element.props.element
        };
        if (element.props.children) {
            route.children = createRoutesFromChildren(element.props.children);
        }
        routes.push(route);
    });
    return routes;
}
export function Route(props) { }
+function compilePath(path,end) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/:(\w+)/g, (_, key) => {
            paramNames.push(key);
            return "([^\\/]+)";
        })
+       .replace(/^\/*/, '/');
+   if(end){
+       regexpSource += "\\/*$";
+   }
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}
+export function matchPath({path,end}, pathname) {
+   let [matcher, paramNames] = compilePath(path,end);
    let match = pathname.match(matcher);
    if (!match) return null;
    let [matchedPathname, ...values] = match;
    let params = paramNames.reduce(
        (memo, paramName, index) => {
            memo[paramName] = values[index];
            return memo;
        },
        {}
    );
    return { params, matchedPathname };
}
export function useNavigate() {
    let { navigator } = React.useContext(NavigationContext);
    let navigate = React.useCallback((to) => {
        navigator.push(to);
    }, [navigator]);
    return navigate;
}
  1. 增加了matchRoutes函数

    • 这个函数用于匹配给定路径(pathname)与一组路由配置(routes)。
    • 它首先通过flattenRoutes函数将路由配置扁平化,然后遍历这些路由,尝试匹配每个路由分支。
    • 如果找到匹配的路由分支,它会记录匹配信息并返回。
  2. 添加了matchRouteBranch函数

    • 这个函数用于匹配单个路由分支。
    • 它会遍历路由分支中的每个路由,检查当前路径是否与这些路由匹配。
    • 匹配是通过matchPath函数完成的,该函数会考虑路由的路径模式和是否为终结路径。
  3. 修改了compilePath函数

    • 这个函数被用于将路由的路径模式编译成正则表达式。
    • 它处理路径中的参数,并生成相应的正则表达式和参数名称列表。
    • 新增的.replace(/^\/*/, '/')确保路径以单个斜杠开始,而.replace(/\/+/g, '/')将连续的斜杠替换为单个斜杠。
  4. 修改了matchPath函数

    • 这个函数用于检查给定的路径是否匹配特定的路由路径模式。
    • 它利用由compilePath生成的正则表达式进行匹配。
    • 如果匹配成功,它会返回匹配的路径和解析出的路由参数。

12.renderMatches #

12.1 react-router\index.js #

src\react-router\index.js

import { match } from "path-to-regexp";
import React from "react";
const NavigationContext = React.createContext({});
const LocationContext = React.createContext({});
+const RouteContext = React.createContext({});
export { NavigationContext, LocationContext };
export function Router({
    children,
    location,
    navigator
}) {
    const navigationContext = React.useMemo(() => ({
        navigator
    }), [navigator]);
    const locationContext = React.useMemo(() => ({
        location
    }), [location]);
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider value={locationContext} children={children} />
        </NavigationContext.Provider>
    );
}
export function Routes({
    children
}) {
    return useRoutes(createRoutesFromChildren(children));
}
export function useLocation() {
    return React.useContext(LocationContext).location;
}
export function useSearchParams() {
    const location = React.useContext(LocationContext).location;
    const pathname = location.pathname;
    return new URLSearchParams(pathname.split("?")[1]);
}
export function Outlet() {
+    return useOutlet();
}
+export function useOutlet() {
+    return React.useContext(RouteContext).outlet;
+}
export function useParams() {
+   const {matches} = React.useContext(RouteContext);
+   return matches[matches.length-1].params;
}
export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname;
+   let matches = matchRoutes(routes, pathname);
+   return _renderMatches(matches);
}
+function _renderMatches(matches){
+    if(!matches){
+        return null;
+    }
+    return matches.reduceRight((outlet,match,index)=>(
+        <RouteContext.Provider value={{outlet,matches:matches.slice(0,index+1)}}>
+            {match.route.element}
+        </RouteContext.Provider>
+    ),null); 
+}
function matchRoutes(routes, pathname) {
    const branches = flattenRoutes(routes);
    let matches = null;
    for (let i = 0; matches == null && i < branches.length; i++) {
        matches = matchRouteBranch(branches[i], pathname);
    }
    return matches;
}
function matchRouteBranch(branch, pathname) {
    const {routeMetas } = branch;
    const matches = [];
    let matchedParams = {};
    let matchedPathname = '';
    for (let i = 0; i < routeMetas.length; i++) {
        const { route } = routeMetas[i];
        const end = i === routeMetas.length - 1;
        const remainingPathname = pathname.slice(matchedPathname.length);
        const match = matchPath({ path: route.path, end }, remainingPathname);
        if (!match) {
            return null;
        }
        matchedParams = Object.assign({}, matchedParams, match.params);
        matches.push({ route,params: matchedParams });
        matchedPathname = joinPaths([matchedPathname, match.matchedPathname]);
    }
    return matches;
}
function flattenRoutes(routes, branches = [], parentMetas = [], parentPath = '') {
    routes.forEach((route) => {
        let routeMeta = {
            route
        }
        const routePath = joinPaths([parentPath, routeMeta.route.path])
        const routeMetas = [...parentMetas, routeMeta];
        if (route.children) {
            flattenRoutes(route.children, branches, routeMetas, routePath);
        }
        branches.push({
            routePath,
            routeMetas
        })
    })
    return branches;
}
function joinPaths(paths) {
    return paths.join('/').replace(/\/+/g, '/');
}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, element => {
        let route = {
            path: element.props.path,
            element: element.props.element
        };
        if (element.props.children) {
            route.children = createRoutesFromChildren(element.props.children);
        }
        routes.push(route);
    });
    return routes;
}
export function Route(props) { }
function compilePath(path,end) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/:(\w+)/g, (_, key) => {
            paramNames.push(key);
            return "([^\\/]+)";
        })
        .replace(/^\/*/, '/');
    if(end){
        regexpSource += "\\/*$";
    }
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}
export function matchPath({path,end}, pathname) {
    let [matcher, paramNames] = compilePath(path,end);
    let match = pathname.match(matcher);
    if (!match) return null;
    let [matchedPathname, ...values] = match;
    let params = paramNames.reduce(
        (memo, paramName, index) => {
            memo[paramName] = values[index];
            return memo;
        },
        {}
    );
    return { params, matchedPathname };
}
export function useNavigate() {
    let { navigator } = React.useContext(NavigationContext);
    let navigate = React.useCallback((to) => {
        navigator.push(to);
    }, [navigator]);
    return navigate;
}

13.rankRouteBranches #

13.1 react-router\index.js #

src\react-router\index.js

import { match } from "path-to-regexp";
import React from "react";
const NavigationContext = React.createContext({});
const LocationContext = React.createContext({});
const RouteContext = React.createContext({});
export { NavigationContext, LocationContext };
export function Router({
    children,
    location,
    navigator
}) {
    const navigationContext = React.useMemo(() => ({
        navigator
    }), [navigator]);
    const locationContext = React.useMemo(() => ({
        location
    }), [location]);
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider value={locationContext} children={children} />
        </NavigationContext.Provider>
    );
}
export function Routes({
    children
}) {
    return useRoutes(createRoutesFromChildren(children));
}
export function useLocation() {
    return React.useContext(LocationContext).location;
}
export function useSearchParams() {
    const location = React.useContext(LocationContext).location;
    const pathname = location.pathname;
    return new URLSearchParams(pathname.split("?")[1]);
}
export function Outlet() {
    return useOutlet();
}
export function useOutlet() {
    return React.useContext(RouteContext).outlet;
}
export function useParams() {
    const { matches } = React.useContext(RouteContext);
    return matches[matches.length - 1].params;
}
+const isSplat = s => s === '*';
+const splatPenalty = -2;
+const indexRouteValue = 2;
+const paramRe = /^:\w+$/;
+const dynamicSegmentValue = 3;
+const emptySegmentValue = 1;
+const staticSegmentValue = 10;
+function computeScore(path, index) {
+    let segments = path.split('/');
+    let initialScore = segments.length;
+    if (segments.some(isSplat)) {
+        initialScore += splatPenalty;
+    }
+    if (index) {
+        initialScore += indexRouteValue;
+    }
+    return segments.filter(s => !isSplat(s)).reduce((score, segment) => {
+        return score + (paramRe.test(segment) ? dynamicSegmentValue : segment === '' ? emptySegmentValue : staticSegmentValue);
+    }, initialScore);
+}
+function rankRouteBranches(branches) {
+    branches.sort((a, b) => {
+        return a.score !== b.score ? b.score - a.score : compareIndexes(
+            a.routeMetas.map(meta => meta.childrenIndex),
+            b.routeMetas.map(meta => meta.childrenIndex)
+        );
+    });
+}
+function compareIndexes(a, b) {
+    let sibling = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i])
+    return sibling ? a[a.length - 1] - b[b.length - 1] : 0;
+}
export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname;
    let matches = matchRoutes(routes, pathname);
    return _renderMatches(matches);
}
function _renderMatches(matches) {
    if (!matches) {
        return null;
    }
    return matches.reduceRight((outlet, match, index) => (
        <RouteContext.Provider value={{ outlet, matches: matches.slice(0, index + 1) }}>
            {match.route.element}
        </RouteContext.Provider>
    ), null);

}
function matchRoutes(routes, pathname) {
    const branches = flattenRoutes(routes);
+   rankRouteBranches(branches);
    let matches = null;
    for (let i = 0; matches == null && i < branches.length; i++) {
        matches = matchRouteBranch(branches[i], pathname);
    }
    return matches;
}
function matchRouteBranch(branch, pathname) {
    const { routeMetas } = branch;
    const matches = [];
    let matchedParams = {};
    let matchedPathname = '';
    for (let i = 0; i < routeMetas.length; i++) {
        const { route } = routeMetas[i];
        const end = i === routeMetas.length - 1;
        const remainingPathname = pathname.slice(matchedPathname.length);
        const match = matchPath({ path: route.path, end }, remainingPathname);
        if (!match) {
            return null;
        }
        matchedParams = Object.assign({}, matchedParams, match.params);
        matches.push({ route, params: matchedParams });
        matchedPathname = joinPaths([matchedPathname, match.matchedPathname]);
    }
    return matches;
}
function flattenRoutes(routes, branches = [], parentMetas = [], parentPath = '') {
    routes.forEach((route,index) => {
        let routeMeta = {
            route,
+           childrenIndex: index
        }
        const routePath = joinPaths([parentPath, routeMeta.route.path])
        const routeMetas = [...parentMetas, routeMeta];
        if (route.children) {
            flattenRoutes(route.children, branches, routeMetas, routePath);
        }
        branches.push({
            routePath,
            routeMetas,
+           score: computeScore(routePath, route.index)
        })
    })
    return branches;
}
function joinPaths(paths) {
    return paths.join('/').replace(/\/+/g, '/');
}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, element => {
        let route = {
            path: element.props.path,
            element: element.props.element
        };
        if (element.props.children) {
            route.children = createRoutesFromChildren(element.props.children);
        }
        routes.push(route);
    });
    return routes;
}
export function Route(props) { }
function compilePath(path, end) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/:(\w+)/g, (_, key) => {
            paramNames.push(key);
            return "([^\\/]+)";
        })
        .replace(/^\/*/, '/');
    if (end) {
        regexpSource += "\\/*$";
    }
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}
export function matchPath({ path, end }, pathname) {
    let [matcher, paramNames] = compilePath(path, end);
    let match = pathname.match(matcher);
    if (!match) return null;
    let [matchedPathname, ...values] = match;
    let params = paramNames.reduce(
        (memo, paramName, index) => {
            memo[paramName] = values[index];
            return memo;
        },
        {}
    );
    return { params, matchedPathname };
}
export function useNavigate() {
    let { navigator } = React.useContext(NavigationContext);
    let navigate = React.useCallback((to) => {
        navigator.push(to);
    }, [navigator]);
    return navigate;
}
  1. 新增函数和常量:

    • isSplat: 检查字符串是否是一个通配符 (*)。
    • splatPenalty, indexRouteValue, dynamicSegmentValue, emptySegmentValue, staticSegmentValue: 这些常量用于在计算路由分数时使用,以帮助确定路由的匹配优先级。
    • computeScore: 计算路由的分数,用于确定匹配的优先级。它考虑了路由路径的不同部分(如静态片段、动态片段、空片段等)以及是否是索引路由或包含通配符。
    • rankRouteBranches: 对路由分支进行排序,基于 computeScore 函数计算的分数。
    • compareIndexes: 在路由分数相同时,用于比较路由分支的索引。
  2. 修改的函数:

    • matchRoutes: 这个函数现在在匹配路由之前会调用 rankRouteBranches 函数,以确保路由是根据优先级排序的。
    • flattenRoutes: 添加了路由分数的计算。当处理每个路由时,它现在将 childrenIndex 和路由的分数 score 添加到每个分支上。
  3. 路由匹配逻辑的增强: 这些修改引入了一种更复杂但更灵活的方式来处理路由匹配。通过计算分数并对路由进行排序,该模块能够更精确地确定哪些路由应该优先匹配,尤其是在处理包含通配符、动态参数和嵌套路由的情况下。

  4. 总体目的: 这些改进使得路由系统能够更好地处理复杂的路由情况,特别是在大型应用程序中,其中可能有许多不同级别的嵌套路由,以及需要精确匹配和排序的动态路由。通过引入这种分数和排序机制,可以确保应用程序总是按照预期的方式响应 URL 更改。

14.1 public\index.html #

public\index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <title>React App</title>
+   <style>
+     .active {
+       color: red;
+     }
+   </style>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

14.2 src\index.js #

src\index.js

import React from "react";
import ReactDOM from "react-dom/client";
+import { HashRouter, BrowserRouter, Routes, Route, NavLink } from "./react-router-dom";
import Home from "./components/Home";
import User from "./components/User";
import UserAdd from './components/UserAdd';
import UserList from './components/UserList';
import UserDetail from './components/UserDetail';
import Profile from "./components/Profile";
import Post from './components/Post';
+const activeStyle = { backgroundColor: 'green' };
+const activeClassName = 'active';
+const activeNavProps = {
+  style: ({ isActive }) => isActive ? activeStyle : {},
+  className: ({ isActive }) => isActive ? activeClassName : ''
+}
ReactDOM.createRoot(document.getElementById("root")).render(
  <HashRouter>
    <ul>
+     <li><NavLink end={true} to="/" {...activeNavProps}>首页</NavLink></li>
+     <li><NavLink to="/user/list" {...activeNavProps}>用户管理</NavLink></li>
+     <li><NavLink to="/profile" {...activeNavProps}>个人中心</NavLink></li>
    </ul>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/user" element={<User />} >
        <Route path="add" element={<UserAdd />} />
        <Route path="list" element={<UserList />} />
        <Route path="detail/:id" element={<UserDetail />} />
      </Route>
      <Route path="/profile" element={<Profile />} />
      <Route path="/post/:id" element={<Post />} />
    </Routes>
  </HashRouter>);

14.3 react-router-dom\index.js #

src\react-router-dom\index.js

import React from "react";
+import { Router,useNavigate,useLocation  } from "../react-router";
import { createHashHistory, createBrowserHistory } from "../history";
export * from "../react-router";
export function HashRouter({
  children
}) {
  let historyRef = React.useRef();
  if (historyRef.current == null) {
    historyRef.current = createHashHistory();
  }
  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });
  React.useLayoutEffect(() => history.listen(setState), [history]);
  return <Router children={children} location={state.location} navigationType={state.action} navigator={history} />;
}
export function BrowserRouter({
  children
}) {
  let historyRef = React.useRef();
  if (historyRef.current == null) {
    historyRef.current = createBrowserHistory();
  }
  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });
  React.useLayoutEffect(() => history.listen(setState), [history]);
  return <Router children={children} location={state.location} navigationType={state.action} navigator={history} />;
}
+export function Link({ to, children,...rest }) {
  const navigate = useNavigate();
  return (
+   <a  {...rest} href={to} onClick={(event) => {
      event.preventDefault();
      navigate(to);
    }} >{children}</a>
  )
}

+export function NavLink({ className: classNameProp = '', end = false, style: styleProp = {}, to, children, ...+rest }) {
+  let location = useLocation();
+  let path = { pathname: to };
+  let locationPathname = location.pathname;
+  let toPathname = path.pathname;
+  let isActive = locationPathname === toPathname
+    || (!end && locationPathname.startsWith(toPathname) && locationPathname.charAt(toPathname.length) === '/')
+  let className;
+  if (typeof classNameProp === 'function') {
+    className = classNameProp({
+      isActive
+    });
+  }
+  let style;
+  if (typeof styleProp === 'function') {
+    style = styleProp({
+      isActive
+    });
+  }
+  return (
+    <Link {...rest} to={to} className={className} style={style}>{children}</Link>
+  )
+}

15. 跳转和重定向 #

15.1 src\index.js #

src\index.js

import React from "react";
import ReactDOM from "react-dom/client";
+import { HashRouter, BrowserRouter, Routes, Route, NavLink,Navigate  } from "./react-router-dom";
import Home from "./components/Home";
import User from "./components/User";
import UserAdd from './components/UserAdd';
import UserList from './components/UserList';
import UserDetail from './components/UserDetail';
import Profile from "./components/Profile";
import Post from './components/Post';
const activeStyle = { backgroundColor: 'green' };
const activeClassName = 'active';
const activeNavProps = {
  style: ({ isActive }) => isActive ? activeStyle : {},
  className: ({ isActive }) => isActive ? activeClassName : ''
}
ReactDOM.createRoot(document.getElementById("root")).render(
  <HashRouter>
    <ul>
      <li><NavLink end={true} to="/" {...activeNavProps}>首页</NavLink></li>
      <li><NavLink to="/user/list" {...activeNavProps}>用户管理</NavLink></li>
      <li><NavLink to="/profile" {...activeNavProps}>个人中心</NavLink></li>
    </ul>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/user" element={<User />} >
        <Route path="add" element={<UserAdd />} />
        <Route path="list" element={<UserList />} />
        <Route path="detail/:id" element={<UserDetail />} />
      </Route>
      <Route path="/profile" element={<Profile />} />
      <Route path="/post/:id" element={<Post />} />
+     <Route path="*" element={<Navigate to="/" />} />
    </Routes>
  </HashRouter>);

15.2 Home.js #

src\components\Home.js

import React from "react";
+import { useNavigate } from '../react-router-dom';
function Home(props) {
+ let navigate = useNavigate();
+ function navigateTo() {
+   navigate('/profile');
+ };
  return (
    <div>
      <p>Home</p>
+     <button onClick={navigateTo}>跳转到/profile</button>
    </div>
  );
}
export default Home;

15.3 react-router\index.js #

src\react-router\index.js

import { match } from "path-to-regexp";
import React from "react";
const NavigationContext = React.createContext({});
const LocationContext = React.createContext({});
const RouteContext = React.createContext({});
export { NavigationContext, LocationContext };
export function Router({
    children,
    location,
    navigator
}) {
    const navigationContext = React.useMemo(() => ({
        navigator
    }), [navigator]);
    const locationContext = React.useMemo(() => ({
        location
    }), [location]);
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider value={locationContext} children={children} />
        </NavigationContext.Provider>
    );
}
export function Routes({
    children
}) {
    return useRoutes(createRoutesFromChildren(children));
}
export function useLocation() {
    return React.useContext(LocationContext).location;
}
export function useSearchParams() {
    const location = React.useContext(LocationContext).location;
    const pathname = location.pathname;
    return new URLSearchParams(pathname.split("?")[1]);
}
export function Outlet() {
    return useOutlet();
}
export function useOutlet() {
    return React.useContext(RouteContext).outlet;
}
export function useParams() {
    const { matches } = React.useContext(RouteContext);
    return matches[matches.length - 1].params;
}
const isSplat = s => s === '*';
const splatPenalty = -2;
const indexRouteValue = 2;
const paramRe = /^:\w+$/;
const dynamicSegmentValue = 3;
const emptySegmentValue = 1;
const staticSegmentValue = 10;
function computeScore(path, index) {
    let segments = path.split('/');
    let initialScore = segments.length;
    if (segments.some(isSplat)) {
        initialScore += splatPenalty;
    }
    if (index) {
        initialScore += indexRouteValue;
    }
    return segments.filter(s => !isSplat(s)).reduce((score, segment) => {
        return score + (paramRe.test(segment) ? dynamicSegmentValue : segment === '' ? emptySegmentValue : staticSegmentValue);
    }, initialScore);
}
function rankRouteBranches(branches) {
    branches.sort((a, b) => {
        return a.score !== b.score ? b.score - a.score : compareIndexes(
            a.routeMetas.map(meta => meta.childrenIndex),
            b.routeMetas.map(meta => meta.childrenIndex)
        );
    });
}
function compareIndexes(a, b) {
    let sibling = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i])
    return sibling ? a[a.length - 1] - b[b.length - 1] : 0;
}
export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname;
    let matches = matchRoutes(routes, pathname);
    return _renderMatches(matches);
}
function _renderMatches(matches) {
    if (!matches) {
        return null;
    }
    return matches.reduceRight((outlet, match, index) => (
        <RouteContext.Provider value={{ outlet, matches: matches.slice(0, index + 1) }}>
            {match.route.element}
        </RouteContext.Provider>
    ), null);

}
function matchRoutes(routes, pathname) {
    const branches = flattenRoutes(routes);
    rankRouteBranches(branches);
    let matches = null;
    for (let i = 0; matches == null && i < branches.length; i++) {
        matches = matchRouteBranch(branches[i], pathname);
    }
    return matches;
}
function matchRouteBranch(branch, pathname) {
    const { routeMetas } = branch;
    const matches = [];
    let matchedParams = {};
    let matchedPathname = '';
    for (let i = 0; i < routeMetas.length; i++) {
        const { route } = routeMetas[i];
        const end = i === routeMetas.length - 1;
        const remainingPathname = pathname.slice(matchedPathname.length);
        const match = matchPath({ path: route.path, end }, remainingPathname);
        if (!match) {
            return null;
        }
        matchedParams = Object.assign({}, matchedParams, match.params);
        matches.push({ route, params: matchedParams });
        matchedPathname = joinPaths([matchedPathname, match.matchedPathname]);
    }
    return matches;
}
function flattenRoutes(routes, branches = [], parentMetas = [], parentPath = '') {
    routes.forEach((route,index) => {
        let routeMeta = {
            route,
            childrenIndex: index
        }
        const routePath = joinPaths([parentPath, routeMeta.route.path])
        const routeMetas = [...parentMetas, routeMeta];
        if (route.children) {
            flattenRoutes(route.children, branches, routeMetas, routePath);
        }
        branches.push({
            routePath,
            routeMetas,
            score: computeScore(routePath, route.index)
        })
    })
    return branches;
}
function joinPaths(paths) {
    return paths.join('/').replace(/\/+/g, '/');
}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, element => {
        let route = {
            path: element.props.path,
            element: element.props.element
        };
        if (element.props.children) {
            route.children = createRoutesFromChildren(element.props.children);
        }
        routes.push(route);
    });
    return routes;
}
export function Route() { }
function compilePath(path, end) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/:(\w+)/g, (_, key) => {
            paramNames.push(key);
            return "([^\\/]+)";
        })
        .replace(/^\/*/, '/');
    if (end) {
        regexpSource += "\\/*$";
    }
+   if(path === '*'){
+       regexpSource = '.*';
+   }
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}
export function matchPath({ path, end }, pathname) {
    let [matcher, paramNames] = compilePath(path, end);
    let match = pathname.match(matcher);
    if (!match) return null;
    let [matchedPathname, ...values] = match;
    let params = paramNames.reduce(
        (memo, paramName, index) => {
            memo[paramName] = values[index];
            return memo;
        },
        {}
    );
    return { params, matchedPathname };
}
export function useNavigate() {
    let { navigator } = React.useContext(NavigationContext);
    let navigate = React.useCallback((to) => {
        navigator.push(to);
    }, [navigator]);
    return navigate;
}
+export function Navigate({ to }) {
+  let navigate = useNavigate();
+  React.useLayoutEffect(() => {
+    navigate(to)
+  });
+  return null;
+}

16. 受保护路由 #

16.1 src\index.js #

src\index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { HashRouter, BrowserRouter, Routes, Route, NavLink,Navigate  } from "./react-router-dom";
import Home from "./components/Home";
import User from "./components/User";
import UserAdd from './components/UserAdd';
import UserList from './components/UserList';
import UserDetail from './components/UserDetail';
import Profile from "./components/Profile";
import Post from './components/Post';
+import Protected from './components/Protected';
+import Login from './components/Login';
const activeStyle = { backgroundColor: 'green' };
const activeClassName = 'active';
const activeNavProps = {
  style: ({ isActive }) => isActive ? activeStyle : {},
  className: ({ isActive }) => isActive ? activeClassName : ''
}
ReactDOM.createRoot(document.getElementById("root")).render(
  <HashRouter>
    <ul>
      <li><NavLink end={true} to="/" {...activeNavProps}>首页</NavLink></li>
      <li><NavLink to="/user/list" {...activeNavProps}>用户管理</NavLink></li>
      <li><NavLink to="/profile" {...activeNavProps}>个人中心</NavLink></li>
    </ul>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/user" element={<User />} >
        <Route path="add" element={<UserAdd />} />
        <Route path="list" element={<UserList />} />
        <Route path="detail/:id" element={<UserDetail />} />
      </Route>
      <Route path="/post/:id" element={<Post />} />
+     <Route path="/profile" element={<Protected component={Profile} path="/profile"/>} />
+     <Route path="/login" element={<Login />} />
      <Route path="*" element={<Navigate to="/" />} />
    </Routes>
  </HashRouter>);

16.2 Login.js #

src\components\Login.js

import React from "react";
import { useNavigate, useLocation } from "../react-router-dom";
function Login() {
  let navigation = useNavigate();
  let location = useLocation();
  const login = () => {
    localStorage.setItem("login", "true");
    let to = "/";
    if (location.state) {
      to = location.state.from || "/";
    }
    navigation(to);
  };
  return <button onClick={login}>登录</button>;
}
export default Login;

16.3 Protected.js #

src\components\Protected.js

import React from "react";
import { Navigate } from "../react-router-dom";
function Protected(props) {
  let { component: RouteComponent, path } = props;
  return localStorage.getItem("login") ? (
    <RouteComponent />
  ) : (
    <Navigate to={{ pathname: "/login", state: { from: path } }} />
  );
}

export default Protected;

17. 配置式路由和懒加载 #

17.1 src\index.js #

src\index.js

import React from "react";
import ReactDOM from "react-dom/client";
+import { HashRouter, NavLink,useRoutes  } from "./react-router-dom";
const activeStyle = { backgroundColor: 'green' };
const activeClassName = 'active';
const activeNavProps = {
  style: ({ isActive }) => isActive ? activeStyle : {},
  className: ({ isActive }) => isActive ? activeClassName : ''
}
+import routesConfig from './routesConfig';
+const LazyPost = React.lazy(() => import('./components/Post'));
+function App() {
+    let [routes, setRoutes] = React.useState(routesConfig);
+    const addRoute = () => {
+        setRoutes([...routes, {
+            path: '/foo', element: (
+                <React.Suspense fallback={<div>loading...</div>}>
+                    <LazyPost />
+                </React.Suspense>
+            )
+        }]);
+    }
+    return (
+        <div>
+            {useRoutes(routes)}
+            <button onClick={addRoute}>addRoute</button>
+        </div>
+    )
+}
ReactDOM.createRoot(document.getElementById("root")).render(
  <HashRouter>
    <ul>
      <li><NavLink end={true} to="/" {...activeNavProps}>首页</NavLink></li>
      <li><NavLink to="/user/list" {...activeNavProps}>用户管理</NavLink></li>
      <li><NavLink to="/profile" {...activeNavProps}>个人中心</NavLink></li>
    </ul>
+   <App />
  </HashRouter>);

17.2 routesConfig.js #

src\routesConfig.js

import Home from "./components/Home";
import User from "./components/User";
import Profile from "./components/Profile";
import UserAdd from "./components/UserAdd";
import UserDetail from "./components/UserDetail";
import UserList from "./components/UserList";
import NotFound from "./components/NotFound";
import Login from "./components/Login";
import Protected from "./components/Protected";
const routes = [
  { path: "/", element: <Home /> },
  { path: "/profile", element: <Profile /> },
  {
    path: "user",
    element: <User />,
    children: [
      { path: "add", element: <UserAdd /> },
      { path: "list", element: <UserList /> },
      { path: "detail/:id", element: <UserDetail /> },
    ],
  },
  { path: "/profile", element: <Protected component={Profile} /> },
  { path: "/login", element: <Login /> },
  { path: "*", element: <NotFound /> },
];
export default routes;

17.3 NotFound.js #

src\components\NotFound.js

import React from "react";
function NotFound(props) {
  return <div>NotFound</div>;
}
export default NotFound;