Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Router 5 完整指南 #18

Open
vortesnail opened this issue May 25, 2021 · 0 comments
Open

React Router 5 完整指南 #18

vortesnail opened this issue May 25, 2021 · 0 comments
Labels

Comments

@vortesnail
Copy link
Owner

vortesnail commented May 25, 2021

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

另外,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”,你会看到一个子菜单,并且显示了产品的数据。

尝试着好好理解下这个演示中的代码,确保你要掌握这部分内容。

权限路由

在如今大多数网站应用中,只有登录了的用户才能访问网站的某些部分,比如掘金登录之后才会有进入到个人主页的入口。接下来这一节,我会告诉大家如何去实现一个权限路由,也就是说如果有人试图访问 /admin ,他将会首先被要求登录。

然而,我们需要先了解 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:

Edit React Router Demo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant