React Router 5 完整指南
点击上方 前端瓶子君,关注公众号
回复算法,加入前端编程面试算法每日一题群
来源:vortesnail
https://juejin.cn/post/6966242922278682632
最近在搭建自己的网站时,以前一直被自己认为写起来很简单的路由狠狠地给了我一巴掌,我既然怎么也想不到该怎么去合理地设计路由,痛定思痛,阅读了很多文章及官方文档,过程中也读到了这一篇很好的基础性文章,想翻译下来向大家分享下!同时,文中一些没讲到点上的,我都会进行补充,欢迎大家阅读与留言!
另外,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
请保证你电脑上安装了 node
和 npm
,然后利用 create-react-app 来创建一个新的 React 项目,我们直接使用 npx
来进行项目的新建:
npx create-react-app react-router-demo
复制代码
npx
可以使你不需要全局安装 create-react-app 就能创建 cra 项目。
接下来切换到该项目目录下:
cd react-router-demo
复制代码
React Router 库包含三个包:react-router、react-router-dom 和 react-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 中的两种类型的路由:
-
BrowserRouter -
HashRouter
两者主要区别在于他们创建的 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.push
和 history.replace
,它们就可以拿来处理上面的问题。
当你点击一个 <Link>
组件时,history.push
就会被调用,而当你使用一个 <Redirect>
组件时,history.replace
就会被调用。其它的方法比如 history.goBack
和 history.goForward
可以用来在历史堆栈中回溯或前进。
Link
和 Route
组件
可以说 <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
中我们定义了三个组件分别为 Home
、Category
和 Products
。虽然现在这样做还算说得过去,但是当一个组件内的代码变得很多时,最好的方式是为每一个组件建立一个独立的文件。就我的经验来说,如果一个组件占用的代码超过 10
行,我就会为它创建一个新的文件。所以从第二个演示开始,我将会为那些代码过多而放在 App.js
中会显得特别臃肿的组件单独创建一个文件来存放。
在 App
组件中我们已经写好了路由的逻辑,<Route>
的 path
如果与当前位置相匹配的话,对应的组件也会被渲染。在以前,要被渲染的组件应该作为 <Route>
组件的属性传入的,但是现在的版本只要作为 <Route>
的子组件就可以被正确渲染。
在上面的路由设计中,/
将会匹配 /
、/category
以及 /products
,这带来的结果是会同时在页面上渲染三个组件,即 Home
、Category
及 Products
,这不是我们所希望看到的。因此,我们可以通过传入 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
属性来渲染路由时,match
、location
及 history
这些路由属性是隐式地传给被渲染的组件的。但当使用比较新的路由渲染模式时,情况有所不同。
比如,以下面这个组件为例:
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
还有一个剩余属性 rest
,Component
将包含我们的 <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: