React Router 5 完整指南
點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)
回復(fù)算法,加入前端編程面試算法每日一題群

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