likes
comments
collection
share

React Router 5 完整指南

作者站长头像
站长
· 阅读数 16

最近在搭建自己的网站时,以前一直被自己认为写起来很简单的路由狠狠地给了我一巴掌,我既然怎么也想不到该怎么去合理地设计路由,痛定思痛,阅读了很多文章及官方文档,过程中也读到了这一篇很好的基础性文章,想翻译下来向大家分享下!同时,文中一些没讲到点上的,我都会进行补充,欢迎大家阅读与留言!

另外,Twitter 已经私信给原作者,得到了翻译许可!

React Router 是 React 社区最受欢迎的路由库,当你需要在一个有多个页面的 React 应用程序中根据 URL 来导航到对应的页面时,就可以使用 React Router 来处理这个问题,它会使你的应用的 UI 和 URL 保持同步。

本教程将会向你介绍 React Router 5 以及你可以利用它而做到的一大堆事情。

介绍

我们都知道 React 是一个用于创建在客户端进行渲染单页应用(SPA)的流行库,在一个 SPA 中可能有多个视图(也可以叫页面),但是与传统的多页应用程序不同的是,浏览这些页面时不会导致整个页面被重新加载。我们希望的是这些页面能够在当前页面中进行内联渲染,当然了,如果我们习惯了多页应用程序,那么希望 SPA 中也要具有以下的功能:

  • 每个页面都应该有一个唯一指定该页面的 URL,这是为了能让用户可以将 URL 加入书签或直接输入浏览器而访问,比如 www.example.com/products
  • 点击浏览器的后腿和前进按钮都应该如其如期工作。
  • 动态生成的嵌套页面最好也有一个自己的 URL,比如 www.example.com/products/shoes/101 ,其中 101 是产品 ID。

路由是使浏览器的 URL 与页面上正在展示的页面保持同步的过程。React Router 让你以声明的方式处理路由,声明式路由方法允许你控制应用程序中的数据流,基本的使用方式就像下面一样简单:

<Route path="/about">
  <About />
</Route>

这里简单提一下声明式路由函数式路由分别长啥样:

  • 声明式:<NavLink to='/products' />
  • 函数式:histor.push('/products')

你可以把 <Route> 组件放在任何你想渲染路由的地方,因为 <Route><Link> 以及其它 React Router 的 APIs 都只是组件而已,所以你可以很容易地在 React 中启动和运行路由。

⚠️ 注意:有一个普遍的误解,认为 React Router 是由 Facebook 开发的官方路由解决方案。实际上,它只是一个第三方库,但因其设计和简单性而广受欢迎。

概览

本教程将会分为几个小节,首先我们会使用 npm 来安装 React 和 React Router,接着就直接介绍 React Router 的基础知识。你会看到根据不同知识点而写的不同的代码演示,本教程中涉及的例子有:

  • 基本的导航路由
  • 嵌套路由
  • 带路径参数的嵌套路由
  • 权限路由

所有与构建这些路由有关的概念都将在此过程中讨论。另外,该项目的全部代码可在 GitHub repo 上找到。 ​ 现在就让我们搞起来吧!

安装 React Router

请保证你电脑上安装了 nodenpm ,然后利用 create-react-app 来创建一个新的 React 项目,我们直接使用 npx 来进行项目的新建:

npx create-react-app react-router-demo

npx 可以使你不需要全局安装 create-react-app 就能创建 cra 项目。

接下来切换到该项目目录下:

cd react-router-demo

React Router 库包含三个包:react-routerreact-router-domreact-router-native 。路由操作相关的核心包是 react-router,而其他两个是特定环境下使用的。如果你正在开发一个 web 应用,你应该使用 react-router-dom,如果你在使用 React Native 开发移动应用,则应该使用 react-router-native。 ​ 使用 npm 来安装 react-router-dom

npm install react-router-dom

然后执行以下命令来启动本地服务:

npm run start

好了,你现在已经有了一个安装了 React Router 的 React 应用,你可以在 http://localhost:3000/ 查看该应用的运行情况了。

React Router 基础知识

现在让我们熟悉一下 React Router 的基础知识,为了做到这一点,我们将制作一个有三个独立页面的应用程序:Home,Category 和 Products。

Router 组件

我们需要做的第一件事是将我们的 <App> 组件包裹在一个 <Router> 组件中(由 React Router 提供)。由于我们正在建立的是一个基于浏览器的 web 应用程序,我们可以使用 React Router API 中的两种类型的路由:

两者主要区别在于他们创建的 URL 上:

// <BrowserRouter>
http://example.com/about
// <HashRouter>
http://example.com/#/about

<BrowserRouter> 在两者中会更受欢迎些,因为它使用的是 HTML5 History API 来保持应用的页面与 URL 同步,而 <HashRouter> 则使用的是 URL 的哈希部分(window.location.hash)。如果你的代码运行在不支持 History API 的传统浏览器上,你应该使用 <HashRouter> ,否则 <BrowserRouter> 对于大多数情况来说是更好的选择。

导入 BrowserRouter 组件并用其包裹 <App> 组件:

// src/index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter as Router } from "react-router-dom";

ReactDOM.render(
  <Router>
    <App />
  </Router>,
  document.getElementById("root")
);

在上面代码中,我们为整个 <App> 组件创建了一个 history 实例,等会向大家解释这意味着什么。

⚠️ 为了能让大家更加明白这两者有啥区别,我会在下面做一个简短的说明。

<BrowserRouter><HashRouter> 区别

BrowserRouter:

BrowserRouter 要求服务端对发送的不同的 URL 都要返回对应的 HTML,比如说现在有如下两个 URL 发送 GET 请求到服务端:

http://example.com/home http://example.com/about

那么这个时候服务端拿到的是完整的 URL,这时候服务端就必须分别对 /home/about 做处理并返回相应的 HTML 来给到客户端渲染。这个带来的影响就是,如果你切换到某个服务端没有做相应处理的页面路由,比如:

http://example.com/article

如果你在 SPA 中写了这部分路由要渲染的页面,在页面无刷新情况下跳转是没啥问题的。但是如果你直接在此路由下进行页面的刷新,就会得到一个 404。 ​

HashRouter

HashRouter 在 URL 中使用哈希符号(#)来使服务端忽略 # 后面所有的 URL 内容,比如你在浏览器地址栏中直接输入以下 URL:

http://example.com/#/home http://example.com/#/about

服务端拿到的只会是 http://example.com/ ,这样服务端只需要对这个路由做处理并返回 HTML,然后后面的路由 /home/about 将全部交给客户端(也就是我们的 SPA 应用)来处理并渲染对应的页面。所以你在任意的路由进行页面的刷新都不会是 404。

History 的小知识

history 这个库可以让你在 JavaScript 运行的任何地方都能轻松地管理回话历史,history 对象抽象化了各个环境中的差异,并提供了最简单易用的的 API 来给你管理历史堆栈、导航,并保持会话之间的持久化状态。 — React Training 文档

每个 <Router> 组件都会创建一个 history 对象,它记录了当前的位置(history.location),还记录了堆栈中以前的位置。在当前位置发生变化时,页面会被重新渲染,于是你就有一种导航跳转的感觉。

那么如何改变当前的位置呢?也就是说如何做到导航跳转呢?这时候 history 的作用就来了,这个对象暴露了一些方法,比如 history.pushhistory.replace ,它们就可以拿来处理上面的问题。

当你点击一个 <Link> 组件时,history.push 就会被调用,而当你使用一个 <Redirect> 组件时,history.replace 就会被调用。其它的方法比如 history.goBackhistory.goForward 可以用来在历史堆栈中回溯或前进。

LinkRoute 组件

可以说 <Route> 组件是 React Router 中最重要的组件了,如果当前的位置与路由的路径匹配,就会渲染对应的 UI。理想情况下,<Route> 组件应该有一个名为 path 的属性,如果路径名称与当前位置匹配,它就会被渲染。

<Link> 组件被用来在页面之间进行导航,它其实就是 HTML 中的 <a> 标签的上层封装,不过在其源码中使用 event.preventDefault 禁止了其默认行为,然后使用 history API 自己实现了跳转。我们都知道,如果使用 <a> 标签去进行导航的话,整个页面都会被刷新,这是我们不希望看到的,当然,跳转到首页这种行为我倒是蛮喜欢用 <a> 标签的~

所以我们使用 <Link> 组件来导航到一个目标 URL,可以在不刷新页面的情况下重新渲染页面。 ​ 现在我们已经知道了所有要完成我们的 APP 所需要的知识,接着更新 src/App.js ,如下所示:

import React from "react";
import { Link, Route, Switch } from "react-router-dom";

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);

const Category = () => (
  <div>
    <h2>Category</h2>
  </div>
);

const Products = () => (
  <div>
    <h2>Products</h2>
  </div>
);

export default function App() {
  return (
    <div>
      <nav className="navbar navbar-light">
        <ul className="nav navbar-nav">
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/category">Category</Link>
          </li>
          <li>
            <Link to="/products">Products</Link>
          </li>
        </ul>
      </nav>
      {/* 如果当前路径与 path 匹配就会渲染对应的组件 */}
      <Route path="/">
        <Home />
      </Route>
      <Route path="/category">
        <Category />
      </Route>
      <Route path="/products">
        <Products />
      </Route>
    </div>
  );
}

在上面的 App.js 中我们定义了三个组件分别为 HomeCategoryProducts 。虽然现在这样做还算说得过去,但是当一个组件内的代码变得很多时,最好的方式是为每一个组件建立一个独立的文件。就我的经验来说,如果一个组件占用的代码超过 10 行,我就会为它创建一个新的文件。所以从第二个演示开始,我将会为那些代码过多而放在 App.js 中会显得特别臃肿的组件单独创建一个文件来存放。

App 组件中我们已经写好了路由的逻辑,<Route>path 如果与当前位置相匹配的话,对应的组件也会被渲染。在以前,要被渲染的组件应该作为 <Route> 组件的属性传入的,但是现在的版本只要作为 <Route> 的子组件就可以被正确渲染。

在上面的路由设计中,/ 将会匹配 //category 以及 /products ,这带来的结果是会同时在页面上渲染三个组件,即 HomeCategoryProducts ,这不是我们所希望看到的。因此,我们可以通过传入 exact 属性给 <Route> 组件来避免这个问题出现:

<Route exact path="/">
  <Home />
</Route>

所以如果你期望的是根据一个安全匹配的 path 去渲染对应的组件,你就应该考虑使用属性 exact 了。

嵌套路由

如果想要使用嵌套路由,我们要更加深入地理解 <Route> 组件的工作方式,接下来我们一探究竟。 ​ 通过 React Router 官方文档 可知,使用 <Route> 渲染一个页面(或组件)的最佳方式是使用子元素方式,就像我们上面的演示一样。然而,还是有一些其它的方式,这些方式是为了兼容在没有引进 hooks 之前的早期版本的 React Router 构建的 APP: ​

  • component :当 URL 匹配时,React Router 会使用 React.createElement 从给定的组件创建一个 React 元素。
  • render :能使你便捷的渲染内联组件或是嵌套组件,你可以给这个属性传入一个函数,当路由的路径匹配时调用,返回一个元素。
  • children :与 render 属性有些类似,它也是接收一个函数,不同的是,无论现在 path 是否与当前位置匹配,这个函数都会被执行。

路径和匹配

属性 path 是用于识别路由应该被匹配到的 URL 部分,它使用 path-to-regexp 库将字符串形式的 path 转换为一个正则表达式,然后将它与当前的位置进行匹配。

如果路由的 path 与当前位置完全匹配时,一个 match 对象 就会被创建,这个对象中有关于 URL 和路径的更多信息,这些信息可以通过这个对象的属性来进行访问,下面为大家列出有哪些属性:

  • match.url :一个字符串(string),返回 URL 匹配的部分,这对于构建嵌套的 <Link> 组件特别有用。
  • match.path :一个字符串(string),返回路由的 path ,即 <Route path=""> ,我们将使用它来构建嵌套的 <Route> 组件。
  • match.isExact :一个布尔值(boolean),如果匹配时精确的,即没有任何尾部字符,则返回 true
  • match.params :一个对象(object),返回的是从 URL 中解析出来键值对。

属性的隐式传递

请注意,当使用 component 属性来渲染路由时,matchlocationhistory 这些路由属性是隐式地传给被渲染的组件的。但当使用比较新的路由渲染模式时,情况有所不同。

比如,以下面这个组件为例:

const Home = (props) => {
  console.log(props);

  return (
    <div>
      <h2>Home</h2>
    </div>
  );
};

以这种方式渲染路由:

<Route exact path="/" component={Home} />

控制台打印的日志:

{
  history: { ... }
  location: { ... }
  match: { ... }
}

但是现在如果以这种方式渲染路由:

<Route exact path="/">
  <Home />
</Route>

控制台打印的日志将会是这样:

{}

可能你会觉得以这种方式来使用不太好,因为我们在渲染的组件中拿不到路由属性了。但是不用担心,React v5.1 引入了几个 hooks,通过在组件内部使用这些 hooks 可以助你访问到上面隐式传递的任何路由属性,这是一种新的管理路由状态的方法,并在一定程度上使我们的组件更加整洁。 ​ 我将在本教程中使用其中的一些 hooks,但是如果你想要更深入地了解,可以查看 React Router v5.1 的发布公告。请注意,hooks 是在 React 的 16.8 版本中引入的,所以你至少需要在这个版本以上才能使用它们。

Switch 组件

在开始代码演示之前,我想先向大家介绍一下 Switch 组件。当多个 <Route> 被一起使用时,所有匹配到的路由都会被渲染,大家看下下面的代码,我会向大家解释为什么 <Switch> 是有用的:

<Route exact path="/"><Home /></Route>
<Route path="/category"><Category /></Route>
<Route path="/products"><Products /></Route>
<Route path="/:id">
  <p>This text will render for any route other than '/'</p>
</Route>

如果 URL 是 /products ,那么 path/products/:id 的路由会一起在页面渲染出来,这就是这样设计的。然而,这种行为基本不可能是我们所期待的,所以才要用到 <Switch> ,有了 <Switch> ,只有第一个与当前 URL 匹配到的子 <Route> 才会被渲染:

<Switch>
  <Route exact path="/">
    <Home />
  </Route>
  <Route path="/category">
    <Category />
  </Route>
  <Route path="/products">
    <Products />
  </Route>
  <Route path="/:id">
    <p>This text will render for any route other than those defined above</p>
  </Route>
</Switch>

path:id 部分用于动态路由,它将匹配斜杠后面的任何东西,并且这个匹配到的值在被渲染的组件中是可以拿到的,我们会在下一节演示如何取这个值。

现在我们知道了关于 <Route><Switch> 组件的一切,让我们看看本节的主题嵌套路由的示例吧。

动态嵌套路由

在上面的示例中我们创建了 //category/products 路由,但是如果我们想要匹配一个 /category/shoes 的路由咋办呢?让我们更新一波 src/App.js 的代码:

import React from "react";
import { Link, Route, Switch } from "react-router-dom";
import Category from "./Category";

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);

const Products = () => (
  <div>
    <h2>Products</h2>
  </div>
);

export default function App() {
  return (
    <div>
      <nav className="navbar navbar-light">
        <ul className="nav navbar-nav">
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/category">Category</Link>
          </li>
          <li>
            <Link to="/products">Products</Link>
          </li>
        </ul>
      </nav>

      <Switch>
        <Route path="/">
          <Home />
        </Route>
        <Route path="/category">
          <Category />
        </Route>
        <Route path="/products">
          <Products />
        </Route>
      </Switch>
    </div>
  );
}

你应该注意到了,我已经把 Category 组件独立出来了,而我们的嵌套路由就在这个组件中去定义,那么现在就来创建 Category.js 吧!

// src/Category.js

import React from "react";
import { Link, Route, useParams, useRouteMatch } from "react-router-dom";

const Item = () => {
  const { name } = useParams();

  return (
    <div>
      <h3>{name}</h3>
    </div>
  );
};

const Category = () => {
  const { url, path } = useRouteMatch();

  return (
    <div>
      <ul>
        <li>
          <Link to={`${url}/shoes`}>Shoes</Link>
        </li>
        <li>
          <Link to={`${url}/boots`}>Boots</Link>
        </li>
        <li>
          <Link to={`${url}/footwear`}>Footwear</Link>
        </li>
      </ul>
      <Route path={`${path}/:name`}>
        <Item />
      </Route>
    </div>
  );
};

export default Category;

在这里我们使用 useRouteMatch hook 来获取上面我们说过的 match 对象。如前所述,match.url 为 URL 匹配的部分,用于构建嵌套链接。match.path 为路由的 path ,用于构建嵌套路由。 ​ 如果你觉得在 match 对象中的属性有理解上的困难,没关系,console.log(useRouteMatch()) 打印在控制台仔细看看它的属性的值是什么,你就大概能知道啥意思了。 ​

<Route path={`${path}/:name`}>
  <Item />
</Route>

这就是我们对动态路由的第一次尝试,因为我们没有将路由写死,而是在属性 path 中使用了一个变量,:name 是一个路径参数,可以捕捉到 category/ 之后的所有内容,直到遇到另外一个正斜杠(/)。因此,像 category/running-shoes 这样的路径名称将会创建一个 params 对象,如下所示:

{
  name: "running-shoes";
}

为了在 <Item> 组件中访问到这个值,我们使用 useParams hook ,它返回一个 URL 参数的键值对的对象。 ​ 你可以在控制台中打印下看看返回的到底是什么,那么现在 Category 应该就会有三个子路由了。

带路径参数的嵌套路由

我们把这个例子在复杂化一点,以便我们更好地去理解。在实际开发中,我们的路由必须具有处理数据并动态展示它们的功能。假设有一些 API 返回的产品数据,其格式如下:

const productData = [
  {
    id: 1,
    name: "NIKE Liteforce Blue Sneakers",
    description:
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin molestie.",
    status: "Available",
  },
  {
    id: 2,
    name: "Stylised Flip Flops and Slippers",
    description:
      "Mauris finibus, massa eu tempor volutpat, magna dolor euismod dolor.",
    status: "Out of Stock",
  },
  {
    id: 3,
    name: "ADIDAS Adispree Running Shoes",
    description:
      "Maecenas condimentum porttitor auctor. Maecenas viverra fringilla felis, eu pretium.",
    status: "Available",
  },
  {
    id: 4,
    name: "ADIDAS Mid Sneakers",
    description:
      "Ut hendrerit venenatis lacus, vel lacinia ipsum fermentum vel. Cras.",
    status: "Out of Stock",
  },
];

假设我们还需要为以下的路径创建路由:

  • /products :这应该显示一个产品列表。
  • /products/:productId :如果匹配到 :productId 那么就应该显示这个产品的数据,如果没有就显示一个错误信息。

创建一个新文件 src/Products.js 文件,并添加以下代码:

import React from "react";
import { Link, Route, useRouteMatch } from "react-router-dom";
import Product from "./Product";

const Products = ({ match }) => {
  const productData = [ ... ];
  const { url } = useRouteMatch();

  /* Create an array of `<li>` items for each product */
  const linkList = productData.map((product) => {
    return (
      <li key={product.id}>
        <Link to={`${url}/${product.id}`}>{product.name}</Link>
      </li>
    );
  });

  return (
    <div>
      <div>
        <div>
          <h3>Products</h3>
          <ul>{linkList}</ul>
        </div>
      </div>

      <Route path={`${url}/:productId`}>
        <Product data={productData} />
      </Route>
      <Route exact path={url}>
        <p>Please select a product.</p>
      </Route>
    </div>
  );
};

export default Products;

首先我们使用了 useRouteMatch 钩子,并从 match 对象中拿到 URL ,然欧根据每个产品的 id 属性来建立一个 <Link> 组件的列表,并将其返回存储到一个 linkList 变量中。

第一个路由使用 path 中的一个变量,它与产品 id 对应,当匹配成功时,我们就会渲染 <Product> 组件(我们马上进行定义),将我们的产品数据传递给它:

<Route path={`${url}/:productId`}>
  <Product data={productData} />
</Route>

注意到第二个路由中有一个 exact 属性,只有当 URL 是 /products 且其后面没有任何路径参数时才会渲染。

OK,下面是 <Product> 组件的代码,你只需要在 src/Product.js 创建这个文件:

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

const Product = ({ data }) => {
  const { productId } = useParams();
  const product = data.find((p) => p.id === Number(productId));
  let productData;

  if (product) {
    productData = (
      <div>
        <h3> {product.name} </h3>
        <p>{product.description}</p>
        <hr />
        <h4>{product.status}</h4>
      </div>
    );
  } else {
    productData = <h2> Sorry. Product doesn't exist </h2>;
  }

  return (
    <div>
      <div>{productData}</div>
    </div>
  );
};

export default Product;

find 方法用于在产品数组中搜索一个 id 属性与 match.params.productId 相同的对象。如果该产品存在,就会渲染对应的数据。如果不存在,就会显示 “产品不存在”的信息。

最后,更新你的 <App> 组件,如下所示:

import React from "react";
import { Link, Route, Switch } from "react-router-dom";
import Category from "./Category";
import Products from "./Products";

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);

export default function App() {
  return (
    <div>
      <nav className="navbar navbar-light">
        <ul className="nav navbar-nav">
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/category">Category</Link>
          </li>
          <li>
            <Link to="/products">Products</Link>
          </li>
        </ul>
      </nav>

      <Switch>
        <Route exact path="/">
          <Home />
        </Route>
        <Route path="/category">
          <Category />
        </Route>
        <Route path="/products">
          <Products />
        </Route>
      </Switch>
    </div>
  );
}

现在你就可以在浏览器中访问你写的这些路由了,如果你选择“Products”,你会看到一个子菜单,并且显示了产品的数据。 ​ 尝试着好好理解下这个演示中的代码,确保你要掌握这部分内容。

权限路由

然而,我们需要先了解 React Router 的几个方面。

<Redirect> 组件

与服务端的重定向类似,React Router 的 Redirect component 将会用一个新的位置替换历史栈中的当前位置,新的位置是由 to 属性来指向的。那么接下来我就会向大家介绍如何使用 <Redirect>

<Redirect to={{ pathname: '/login', state: { from: location }}}

如果有人试图在未登录状态下访问 /admin 路由,他就会被重定向到 /login 路由,关于当前位置的信息是由 state 属性进行传递的,这样做是为了在用户登录成功之后,用户又可以被重定向到他试图访问的路由页面。

自定义路由

如果我们需要决定一个路由是否应该被渲染,那么编写一个自定义路由是个好办法,接下来在 src 目录下创建一个新文件 PrivateRoute.js ,并写入以下代码:

import React from "react";
import { Redirect, Route, useLocation } from "react-router-dom";
import { fakeAuth } from "./Login";

const PrivateRoute = ({ component: Component, ...rest }) => {
  const location = useLocation();

  return (
    <Route {...rest}>
      {fakeAuth.isAuthenticated === true ? (
        <Component />
      ) : (
        <Redirect to={{ pathname: "/login", state: { from: location } }} />
      )}
    </Route>
  );
};

export default PrivateRoute;

如你所见,在函数定义中,我们将接收到的 props 中拿到一个 Component 还有一个剩余属性 restComponent 将包含我们的 <PrivateRoute> 所保护的任何组件(在该例中为 Admin 组件),其余的属性将会通过 rest 传递给 <Route>

我们返回的是一个 <Route> 组件,该组件会根据用户是否登录来决定是否渲染受到保护的组件,如果没有登录将会重定向到 /login 路由。这是由 fakeAuth.isAuthenticated 属性决定的,这个属性从 <Login> 组件中导入。 ​ 这种封装的方法好处在于是声明式的,而且 <PrivateRoute> 可被重复使用。

实践权限路由

现在我们可以修改 src/App.js

import React from "react";
import { Link, Route, Switch } from "react-router-dom";
import Category from "./Category";
import Products from "./Products";
import Login from "./Login";
import PrivateRoute from "./PrivateRoute";

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);

const Admin = () => (
  <div>
    <h2>Welcome admin!</h2>
  </div>
);

export default function App() {
  return (
    <div>
      <nav className="navbar navbar-light">
        <ul className="nav navbar-nav">
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/category">Category</Link>
          </li>
          <li>
            <Link to="/products">Products</Link>
          </li>
          <li>
            <Link to="/admin">Admin area</Link>
          </li>
        </ul>
      </nav>

      <Switch>
        <Route exact path="/">
          <Home />
        </Route>
        <Route path="/category">
          <Category />
        </Route>
        <Route path="/products">
          <Products />
        </Route>
        <Route path="/login">
          <Login />
        </Route>
        <PrivateRoute path="/admin" component={Admin} />
      </Switch>
    </div>
  );
}

正如你所见,我们在文件的顶部添加了一个 <Admin> 组件,并在 <Switch> 组件下添加了一个 <PrivateRoute> 组件。正如前面所说,如果用户已经登录的话,这个自定义路由将会渲染的是 <Admin> 组件,否则,用户会被重定向到 /login

最后,这里是 Login 组件代码:

import React, { useState } from "react";
import { Redirect, useLocation } from "react-router-dom";

export default function Login() {
  const { state } = useLocation();
  const { from } = state || { from: { pathname: "/" } };
  const [redirectToReferrer, setRedirectToReferrer] = useState(false);

  const login = () => {
    fakeAuth.authenticate(() => {
      setRedirectToReferrer(true);
    });
  };

  if (redirectToReferrer) {
    return <Redirect to={from} />;
  }

  return (
    <div>
      <p>You must log in to view the page at {from.pathname}</p>
      <button onClick={login}>Log in</button>
    </div>
  );
}

/* A fake authentication function */
export const fakeAuth = {
  isAuthenticated: false,
  authenticate(cb) {
    this.isAuthenticated = true;
    setTimeout(cb, 100);
  },
};

我们使用 useLocation hook 来访问路由的 location 属性,也就是从 state 属性带过来的。然后我们使用对象的解构来获取用户在被要求登录之前试图访问的 URL,这个这个值不存在,我们就设为 { pathname: "/" }

然后我们使用 React 的 useState 钩子来初始化一个 redirectToReferrer 状态为 false ,根据这个值来决定用户是被重定向到他们想要访问的路径(也就是说用户已经登录了),还是向用户展示一个按钮让他们登录。

一旦按钮被点击,fakeAuth.authenticate 这个方法就会被执行,它将 fakeAuth.isAuthenticated 设为 true ,并(在一个回调函数中)将 redirectToReferrer 状态更新为 true ,这将导致组件重新渲染,用户将被重定向。

完整示例

以下就是我们使用学到的东西做出来的最终 demo:

React Router 5 完整指南