React Router 5 完整指南
點擊上方 前端瓶子君,關注公眾號
回復算法,加入前端編程面試算法每日一題群

來源:vortesnail
https://juejin.cn/post/6966242922278682632
最近在搭建自己的網(wǎng)站時,以前一直被自己認為寫起來很簡單的路由狠狠地給了我一巴掌,我既然怎么也想不到該怎么去合理地設計路由,痛定思痛,閱讀了很多文章及官方文檔,過程中也讀到了這一篇很好的基礎性文章,想翻譯下來向大家分享下!同時,文中一些沒講到點上的,我都會進行補充,歡迎大家閱讀與留言!
另外,Twitter 已經(jīng)私信給原作者,得到了翻譯許可!
React Router 是 React 社區(qū)最受歡迎的路由庫,當你需要在一個有多個頁面的 React 應用程序中根據(jù) URL 來導航到對應的頁面時,就可以使用 React Router 來處理這個問題,它會使你的應用的 UI 和 URL 保持同步。
本教程將會向你介紹 React Router 5 以及你可以利用它而做到的一大堆事情。
介紹
我們都知道 React 是一個用于創(chuàng)建在客戶端進行渲染單頁應用(SPA)的流行庫,在一個 SPA 中可能有多個視圖(也可以叫頁面),但是與傳統(tǒng)的多頁應用程序不同的是,瀏覽這些頁面時不會導致整個頁面被重新加載。我們希望的是這些頁面能夠在當前頁面中進行內(nèi)聯(lián)渲染,當然了,如果我們習慣了多頁應用程序,那么希望 SPA 中也要具有以下的功能:
-
每個頁面都應該有一個唯一指定該頁面的 URL,這是為了能讓用戶可以將 URL 加入書簽或直接輸入瀏覽器而訪問,比如 www.example.com/products。 -
點擊瀏覽器的后腿和前進按鈕都應該如其如期工作。 -
動態(tài)生成的嵌套頁面最好也有一個自己的 URL,比如 www.example.com/products/shoes/101,其中101是產(chǎn)品 ID。
路由是使瀏覽器的 URL 與頁面上正在展示的頁面保持同步的過程。React Router 讓你以聲明的方式處理路由,聲明式路由方法允許你控制應用程序中的數(shù)據(jù)流,基本的使用方式就像下面一樣簡單:
<Route path="/about">
<About />
</Route>
復制代碼
這里簡單提一下聲明式路由和函數(shù)式路由分別長啥樣:
-
聲明式: <NavLink to='/products' />。 -
函數(shù)式: histor.push('/products')。
你可以把 <Route> 組件放在任何你想渲染路由的地方,因為 <Route> ,<Link> 以及其它 React Router 的 APIs 都只是組件而已,所以你可以很容易地在 React 中啟動和運行路由。
?? 注意:有一個普遍的誤解,認為 React Router 是由 Facebook 開發(fā)的官方路由解決方案。實際上,它只是一個第三方庫,但因其設計和簡單性而廣受歡迎。
概覽
本教程將會分為幾個小節(jié),首先我們會使用 npm 來安裝 React 和 React Router,接著就直接介紹 React Router 的基礎知識。你會看到根據(jù)不同知識點而寫的不同的代碼演示,本教程中涉及的例子有:
-
基本的導航路由 -
嵌套路由 -
帶路徑參數(shù)的嵌套路由 -
權(quán)限路由
所有與構(gòu)建這些路由有關的概念都將在此過程中討論。另外,該項目的全部代碼可在 GitHub repo 上找到。 現(xiàn)在就讓我們搞起來吧!
安裝 React Router
請保證你電腦上安裝了 node 和 npm ,然后利用 create-react-app 來創(chuàng)建一個新的 React 項目,我們直接使用 npx 來進行項目的新建:
npx create-react-app react-router-demo
復制代碼
npx可以使你不需要全局安裝 create-react-app 就能創(chuàng)建 cra 項目。
接下來切換到該項目目錄下:
cd react-router-demo
復制代碼
React Router 庫包含三個包:react-router、react-router-dom 和 react-router-native 。路由操作相關的核心包是 react-router,而其他兩個是特定環(huán)境下使用的。如果你正在開發(fā)一個 web 應用,你應該使用 react-router-dom,如果你在使用 React Native 開發(fā)移動應用,則應該使用 react-router-native。 使用 npm 來安裝 react-router-dom:
npm install react-router-dom
復制代碼
然后執(zhí)行以下命令來啟動本地服務:
npm run start
復制代碼
好了,你現(xiàn)在已經(jīng)有了一個安裝了 React Router 的 React 應用,你可以在 http://localhost:3000/ 查看該應用的運行情況了。
React Router 基礎知識
現(xiàn)在讓我們熟悉一下 React Router 的基礎知識,為了做到這一點,我們將制作一個有三個獨立頁面的應用程序:Home,Category 和 Products。
Router 組件
我們需要做的第一件事是將我們的 <App> 組件包裹在一個 <Router> 組件中(由 React Router 提供)。由于我們正在建立的是一個基于瀏覽器的 web 應用程序,我們可以使用 React Router API 中的兩種類型的路由:
-
BrowserRouter -
HashRouter
兩者主要區(qū)別在于他們創(chuàng)建的 URL 上:
// <BrowserRouter>
http://example.com/about
// <HashRouter>
http://example.com/#/about
復制代碼
<BrowserRouter> 在兩者中會更受歡迎些,因為它使用的是 HTML5 History API 來保持應用的頁面與 URL 同步,而 <HashRouter> 則使用的是 URL 的哈希部分(window.location.hash)。如果你的代碼運行在不支持 History API 的傳統(tǒng)瀏覽器上,你應該使用 <HashRouter> ,否則 <BrowserRouter> 對于大多數(shù)情況來說是更好的選擇。
導入 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")
);
復制代碼
在上面代碼中,我們?yōu)檎麄€ <App> 組件創(chuàng)建了一個 history 實例,等會向大家解釋這意味著什么。
?? 為了能讓大家更加明白這兩者有啥區(qū)別,我會在下面做一個簡短的說明。
<BrowserRouter> 和 <HashRouter> 區(qū)別
BrowserRouter:
BrowserRouter 要求服務端對發(fā)送的不同的 URL 都要返回對應的 HTML,比如說現(xiàn)在有如下兩個 URL 發(fā)送 GET 請求到服務端:
http://example.com/home http://example.com/about
復制代碼
那么這個時候服務端拿到的是完整的 URL,這時候服務端就必須分別對 /home 和 /about 做處理并返回相應的 HTML 來給到客戶端渲染。這個帶來的影響就是,如果你切換到某個服務端沒有做相應處理的頁面路由,比如:
http://example.com/article
復制代碼
如果你在 SPA 中寫了這部分路由要渲染的頁面,在頁面無刷新情況下跳轉(zhuǎn)是沒啥問題的。但是如果你直接在此路由下進行頁面的刷新,就會得到一個 404。
HashRouter
HashRouter 在 URL 中使用哈希符號(#)來使服務端忽略 # 后面所有的 URL 內(nèi)容,比如你在瀏覽器地址欄中直接輸入以下 URL:
http://example.com/#/home http://example.com/#/about
復制代碼
服務端拿到的只會是 http://example.com/ ,這樣服務端只需要對這個路由做處理并返回 HTML,然后后面的路由 /home 或 /about 將全部交給客戶端(也就是我們的 SPA 應用)來處理并渲染對應的頁面。所以你在任意的路由進行頁面的刷新都不會是 404。
History 的小知識
history這個庫可以讓你在 JavaScript 運行的任何地方都能輕松地管理回話歷史,history對象抽象化了各個環(huán)境中的差異,并提供了最簡單易用的的 API 來給你管理歷史堆棧、導航,并保持會話之間的持久化狀態(tài)。— React Training 文檔
每個 <Router> 組件都會創(chuàng)建一個 history 對象,它記錄了當前的位置(history.location),還記錄了堆棧中以前的位置。在當前位置發(fā)生變化時,頁面會被重新渲染,于是你就有一種導航跳轉(zhuǎn)的感覺。
那么如何改變當前的位置呢?也就是說如何做到導航跳轉(zhuǎn)呢?這時候 history 的作用就來了,這個對象暴露了一些方法,比如 history.push 和 history.replace ,它們就可以拿來處理上面的問題。
當你點擊一個 <Link> 組件時,history.push 就會被調(diào)用,而當你使用一個 <Redirect> 組件時,history.replace 就會被調(diào)用。其它的方法比如 history.goBack 和 history.goForward 可以用來在歷史堆棧中回溯或前進。
Link 和 Route 組件
可以說 <Route> 組件是 React Router 中最重要的組件了,如果當前的位置與路由的路徑匹配,就會渲染對應的 UI。理想情況下,<Route> 組件應該有一個名為 path 的屬性,如果路徑名稱與當前位置匹配,它就會被渲染。
<Link> 組件被用來在頁面之間進行導航,它其實就是 HTML 中的 <a> 標簽的上層封裝,不過在其源碼中使用 event.preventDefault 禁止了其默認行為,然后使用 history API 自己實現(xiàn)了跳轉(zhuǎn)。我們都知道,如果使用 <a> 標簽去進行導航的話,整個頁面都會被刷新,這是我們不希望看到的,當然,跳轉(zhuǎn)到首頁這種行為我倒是蠻喜歡用 <a> 標簽的~
所以我們使用 <Link> 組件來導航到一個目標 URL,可以在不刷新頁面的情況下重新渲染頁面。 現(xiàn)在我們已經(jīng)知道了所有要完成我們的 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 。雖然現(xiàn)在這樣做還算說得過去,但是當一個組件內(nèi)的代碼變得很多時,最好的方式是為每一個組件建立一個獨立的文件。就我的經(jīng)驗來說,如果一個組件占用的代碼超過 10 行,我就會為它創(chuàng)建一個新的文件。所以從第二個演示開始,我將會為那些代碼過多而放在 App.js 中會顯得特別臃腫的組件單獨創(chuàng)建一個文件來存放。
在 App 組件中我們已經(jīng)寫好了路由的邏輯,<Route> 的 path 如果與當前位置相匹配的話,對應的組件也會被渲染。在以前,要被渲染的組件應該作為 <Route> 組件的屬性傳入的,但是現(xiàn)在的版本只要作為 <Route> 的子組件就可以被正確渲染。
在上面的路由設計中,/ 將會匹配 / 、/category 以及 /products ,這帶來的結(jié)果是會同時在頁面上渲染三個組件,即 Home 、Category 及 Products ,這不是我們所希望看到的。因此,我們可以通過傳入 exact 屬性給 <Route> 組件來避免這個問題出現(xiàn):
<Route exact path="/">
<Home />
</Route>
復制代碼
所以如果你期望的是根據(jù)一個安全匹配的 path 去渲染對應的組件,你就應該考慮使用屬性 exact 了。
嵌套路由
如果想要使用嵌套路由,我們要更加深入地理解 <Route> 組件的工作方式,接下來我們一探究竟。 通過 React Router 官方文檔 可知,使用 <Route> 渲染一個頁面(或組件)的最佳方式是使用子元素方式,就像我們上面的演示一樣。然而,還是有一些其它的方式,這些方式是為了兼容在沒有引進 hooks 之前的早期版本的 React Router 構(gòu)建的 APP:
-
component:當 URL 匹配時,React Router 會使用React.createElement從給定的組件創(chuàng)建一個 React 元素。 -
render:能使你便捷的渲染內(nèi)聯(lián)組件或是嵌套組件,你可以給這個屬性傳入一個函數(shù),當路由的路徑匹配時調(diào)用,返回一個元素。 -
children:與render屬性有些類似,它也是接收一個函數(shù),不同的是,無論現(xiàn)在path是否與當前位置匹配,這個函數(shù)都會被執(zhí)行。
路徑和匹配
屬性 path 是用于識別路由應該被匹配到的 URL 部分,它使用 path-to-regexp 庫將字符串形式的 path 轉(zhuǎn)換為一個正則表達式,然后將它與當前的位置進行匹配。
如果路由的 path 與當前位置完全匹配時,一個 match 對象 就會被創(chuàng)建,這個對象中有關于 URL 和路徑的更多信息,這些信息可以通過這個對象的屬性來進行訪問,下面為大家列出有哪些屬性:
-
match.url:一個字符串(string),返回 URL 匹配的部分,這對于構(gòu)建嵌套的<Link>組件特別有用。 -
match.path:一個字符串(string),返回路由的path,即<Route path="">,我們將使用它來構(gòu)建嵌套的<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: { ... }
}
復制代碼
但是現(xiàn)在如果以這種方式渲染路由:
<Route exact path="/">
<Home />
</Route>
復制代碼
控制臺打印的日志將會是這樣:
{}
復制代碼
可能你會覺得以這種方式來使用不太好,因為我們在渲染的組件中拿不到路由屬性了。但是不用擔心,React v5.1 引入了幾個 hooks,通過在組件內(nèi)部使用這些 hooks 可以助你訪問到上面隱式傳遞的任何路由屬性,這是一種新的管理路由狀態(tài)的方法,并在一定程度上使我們的組件更加整潔。 我將在本教程中使用其中的一些 hooks,但是如果你想要更深入地了解,可以查看 React Router v5.1 的發(fā)布公告。請注意,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 部分用于動態(tài)路由,它將匹配斜杠后面的任何東西,并且這個匹配到的值在被渲染的組件中是可以拿到的,我們會在下一節(jié)演示如何取這個值。
現(xiàn)在我們知道了關于 <Route> 和 <Switch> 組件的一切,讓我們看看本節(jié)的主題嵌套路由的示例吧。
動態(tài)嵌套路由
在上面的示例中我們創(chuàng)建了 / 、/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>
);
}
復制代碼
你應該注意到了,我已經(jīng)把 Category 組件獨立出來了,而我們的嵌套路由就在這個組件中去定義,那么現(xiàn)在就來創(chuàng)建 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 匹配的部分,用于構(gòu)建嵌套鏈接。match.path 為路由的 path ,用于構(gòu)建嵌套路由。 如果你覺得在 match 對象中的屬性有理解上的困難,沒關系,console.log(useRouteMatch()) 打印在控制臺仔細看看它的屬性的值是什么,你就大概能知道啥意思了。
<Route path={`${path}/:name`}>
<Item />
</Route>
復制代碼
這就是我們對動態(tài)路由的第一次嘗試,因為我們沒有將路由寫死,而是在屬性 path 中使用了一個變量,:name 是一個路徑參數(shù),可以捕捉到 category/ 之后的所有內(nèi)容,直到遇到另外一個正斜杠(/)。因此,像 category/running-shoes 這樣的路徑名稱將會創(chuàng)建一個 params 對象,如下所示:
{
name: "running-shoes";
}
復制代碼
為了在 <Item> 組件中訪問到這個值,我們使用 useParams hook ,它返回一個 URL 參數(shù)的鍵值對的對象。 你可以在控制臺中打印下看看返回的到底是什么,那么現(xiàn)在 Category 應該就會有三個子路由了。
帶路徑參數(shù)的嵌套路由
我們把這個例子在復雜化一點,以便我們更好地去理解。在實際開發(fā)中,我們的路由必須具有處理數(shù)據(jù)并動態(tài)展示它們的功能。假設有一些 API 返回的產(chǎn)品數(shù)據(jù),其格式如下:
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",
},
];
復制代碼
假設我們還需要為以下的路徑創(chuàng)建路由:
-
/products:這應該顯示一個產(chǎn)品列表。 -
/products/:productId:如果匹配到:productId那么就應該顯示這個產(chǎn)品的數(shù)據(jù),如果沒有就顯示一個錯誤信息。
創(chuàng)建一個新文件 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 ,然歐根據(jù)每個產(chǎn)品的 id 屬性來建立一個 <Link> 組件的列表,并將其返回存儲到一個 linkList 變量中。
第一個路由使用 path 中的一個變量,它與產(chǎn)品 id 對應,當匹配成功時,我們就會渲染 <Product> 組件(我們馬上進行定義),將我們的產(chǎn)品數(shù)據(jù)傳遞給它:
<Route path={`${url}/:productId`}>
<Product data={productData} />
</Route>
復制代碼
注意到第二個路由中有一個 exact 屬性,只有當 URL 是 /products 且其后面沒有任何路徑參數(shù)時才會渲染。
OK,下面是 <Product> 組件的代碼,你只需要在 src/Product.js 創(chuàng)建這個文件:
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 方法用于在產(chǎn)品數(shù)組中搜索一個 id 屬性與 match.params.productId 相同的對象。如果該產(chǎn)品存在,就會渲染對應的數(shù)據(jù)。如果不存在,就會顯示 “產(chǎn)品不存在”的信息。
最后,更新你的 <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>
);
}
復制代碼
現(xiàn)在你就可以在瀏覽器中訪問你寫的這些路由了,如果你選擇“Products”,你會看到一個子菜單,并且顯示了產(chǎn)品的數(shù)據(jù)。 嘗試著好好理解下這個演示中的代碼,確保你要掌握這部分內(nèi)容。
權(quán)限路由
在如今大多數(shù)網(wǎng)站應用中,只有登錄了的用戶才能訪問網(wǎng)站的某些部分,比如掘金登錄之后才會有進入到個人主頁的入口。接下來這一節(jié),我會告訴大家如何去實現(xiàn)一個權(quán)限路由,也就是說如果有人試圖訪問 /admin ,他將會首先被要求登錄。
然而,我們需要先了解 React Router 的幾個方面。
<Redirect> 組件
與服務端的重定向類似,React Router 的 Redirect component 將會用一個新的位置替換歷史棧中的當前位置,新的位置是由 to 屬性來指向的。那么接下來我就會向大家介紹如何使用 <Redirect> :
<Redirect to={{ pathname: '/login', state: { from: location }}}
復制代碼
如果有人試圖在未登錄狀態(tài)下訪問 /admin 路由,他就會被重定向到 /login 路由,關于當前位置的信息是由 state 屬性進行傳遞的,這樣做是為了在用戶登錄成功之后,用戶又可以被重定向到他試圖訪問的路由頁面。
自定義路由
如果我們需要決定一個路由是否應該被渲染,那么編寫一個自定義路由是個好辦法,接下來在 src 目錄下創(chuàng)建一個新文件 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;
復制代碼
如你所見,在函數(shù)定義中,我們將接收到的 props 中拿到一個 Component 還有一個剩余屬性 rest ,Component 將包含我們的 <PrivateRoute> 所保護的任何組件(在該例中為 Admin 組件),其余的屬性將會通過 rest 傳遞給 <Route> 。
我們返回的是一個 <Route> 組件,該組件會根據(jù)用戶是否登錄來決定是否渲染受到保護的組件,如果沒有登錄將會重定向到 /login 路由。這是由 fakeAuth.isAuthenticated 屬性決定的,這個屬性從 <Login> 組件中導入。 這種封裝的方法好處在于是聲明式的,而且 <PrivateRoute> 可被重復使用。
實踐權(quán)限路由
現(xiàn)在我們可以修改 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> 組件。正如前面所說,如果用戶已經(jīng)登錄的話,這個自定義路由將會渲染的是 <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 屬性帶過來的。然后我們使用對象的解構(gòu)來獲取用戶在被要求登錄之前試圖訪問的 URL,這個這個值不存在,我們就設為 { pathname: "/" } 。
然后我們使用 React 的 useState 鉤子來初始化一個 redirectToReferrer 狀態(tài)為 false ,根據(jù)這個值來決定用戶是被重定向到他們想要訪問的路徑(也就是說用戶已經(jīng)登錄了),還是向用戶展示一個按鈕讓他們登錄。
一旦按鈕被點擊,fakeAuth.authenticate 這個方法就會被執(zhí)行,它將 fakeAuth.isAuthenticated 設為 true ,并(在一個回調(diào)函數(shù)中)將 redirectToReferrer 狀態(tài)更新為 true ,這將導致組件重新渲染,用戶將被重定向。
完整示例
以下就是我們使用學到的東西做出來的最終 demo:
