[譯] React Router v6:入門指南
原文地址:React Router v6: A Beginner’s Guide[1]
原文作者:James Hibbard[2]
React Router[3] 是 React 的一個標準路由庫。當你想要瀏覽具有多個視圖的 React 應用時,你需要一個路由功能來管理 URL。React Router 就能做到這一點,讓你應用的 UI 和 URL 保持同步。
本教程將向您介紹 React Router v6 以及用它可以做到的許多事情。
引言
React 是一個流行的 JavaScript 庫,用於構建可提供動態內容的交互式 Web 應用。此類應用可能有多個視圖(又稱頁面),但與傳統的多頁面應用不同的是,瀏覽這些視圖不會觸發整個頁面的重新加載,而是在當前頁面中直接渲染出來。
對於習慣於使用多頁面應用的終端用戶來說,他們希望單頁應用具備以下功能:
-
每個視圖都應有一個唯一 URL。這樣,用戶就可以將 URL 加入書籤,供以後直接打開 — 例如,
www.example.com/products
。 -
瀏覽器的 "後退" 和 "前進" 按鈕應能正常工作。
-
動態生成的嵌套視圖最好也有自己的 URL,例如
example.com/products/shoes/101
,其中 101 是產品 ID。
路由提供讓瀏覽器 URL 與頁面渲染內容保持同步的能力。React Router 可讓你聲明式地處理路由,聲明式路由方法允許你通過說 “路由應該是這樣的” 來控制應用中的頁面和路由綁定。
<Route path="/about" element={<About />} />
你可以將 <Route>
組件放在任何位置,它都能按你期望正確渲染內容。因爲 <Route>
、<Link>
和我們將要處理的所有 React Router 其他 API 都只是組件,因此你可以輕鬆地在 React 中接入路由。
注意:大家普遍誤認爲 React Router 是由 Facebook 開發的官方路由解決方案。實際上,它是 Remix Software 開發和維護的第三方庫。
概述
本教程分爲不同部分。首先,我們將使用 npm 安裝 React 和 React Router。然後,我們將直接開始接觸一些基礎知識。你將看到在實際應用中 React Router 的不同代碼示例。本教程涵蓋的示例包括:
-
基本導航路由
-
嵌套路由
-
帶路徑參數的嵌套路由
-
受保護路由
所有概念都會在構建這些示例的過程中介紹。
該項目的全部代碼都可以在 GitHub[4] 上找到。
讓我們開始吧!
安裝 React Router
要學習本教程,你需要在電腦上安裝最新版本的 Node。如果還沒安裝,請訪問 Node 主頁,根據你的系統下載正確的二進制文件 [5]。或者,你也可以考慮使用版本管理器來安裝 Node。我們在此處提供了使用版本管理器的教程 [6]。
Node 捆綁了 npm,它是 JavaScript 的包管理器,我們將用它來安裝一些要使用的庫。有關 npm 的更多信息,請點擊此處 [7]。
你可以在命令行中執行以下命令,檢查兩者是否都已正確安裝:
node -v
> 20.9.0
npm -v
> 10.1.0
完成上述步驟後,讓我們使用 Create React App 工具創建一個新的 React 項目。你可以全局安裝,也可以使用 npx,就像這樣:
npx create-react-app react-router-demo
完成後,切換到新創建的目錄:
cd react-router-demo
React Router 由三個軟件包組成:react-router[8]、react-router-dom[9] 和 react-router-native[10]。核心包是 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 基礎知識
Router
組件
我們需要做的第一件事是用 <Router>
組件(由 React Router 提供)來包裹 <App>
組件。路由有多種類型 [11],在我們的案例中,有兩種路由值得考慮:
-
BrowserRouter[12]
-
HashRouter[13]
它們之間的主要區別體現在所創建的 URL 上:
// <BrowserRouter>
https://example.com/about
// <HashRouter>
https://example.com/#/about
<BrowserRouter>
是一種常用的路由,它利用 HTML5 History API[14] 將用戶界面與 URL 同步,提供了一種沒有 hash 片段的更簡潔的 URL 結構。而 <HashRouter>
利用 URL 的 hash 部分(window.location.hash
)來管理路由,它的優勢在於無需對服務器增加配置和優秀的兼容性。你可以在此閱讀有關差異的更多信息 [15]。
還請注意,在 React Router 的最新版本(v6.4)中引入了四個新的路由,它們支持各種新的數據 API。在本教程中,我們將重點介紹傳統路由,因爲這些路由功能強大、文檔齊全,而且在衆多項目中都有使用。不過,我們將在後面的章節中深入介紹 v6.4 中的新功能。
因此,讓我們導入 <BrowserRouter>
組件,並用它包裹 <App>
組件。將 index.js
改爲如下所示:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
這段代碼爲我們的整個 <App>
組件創建了一個 history
實例。讓我們看看這意味着什麼。
History 小知識
history
庫能讓你在 JavaScript 中輕鬆管理會話歷史。history
對象抽象化了各種環境中的差異,並提供了一個最小化的 API,讓你可以管理歷史記錄堆棧、導航,並在會話之間持續保持狀態。—— remix-run[16]
每個 <Router>
組件都會創建一個 history
對象,用於跟蹤堆棧中的當前路由地址和前一個路由地址。噹噹前路由地址發生變化時,視圖就會重新渲染,從而讓你有一種導航的感覺。
如何更改當前路由地址?在 React Router v6 中,useNavigate Hook
[17] 提供了一個路由跳轉的函數: navigate
。當你點擊 <Link>
組件時會調用 navigate
函數,也可以通過傳遞帶有 replace: true
屬性的選項對象來覆蓋當前路由地址。
其他方法(如 navigate(-1)
用於後退,navigate(1)
用於前進)可用於通過後退或前進一頁來瀏覽歷史堆棧。
應用無需創建自己的歷史對象;這項任務由 <Router>
組件處理。簡而言之,它會創建一個 history
對象,訂閱堆棧中的更改,並在 URL 更改時修改其狀態。這會觸發程序的重新渲染,確保顯示正確的用戶界面。
接下來是 Links 和 Routes。
Link
和 Route
組件
<Route>
組件是 React Rtouter 中最重要的組件。如果路由地址和當前 URL 路徑匹配,它就會在界面渲染一些內容。正常情況下,<Route>
組件應該有一個名爲 path
的屬性,如果這個屬性值與當前 URL 路徑匹配,組件內容就會被渲染。
而 <Link>
組件則用於在頁面之間導航。它類似於 HTML 錨點元素 (<a>
)。不過,使用錨鏈接會導致整個頁面刷新,這是我們不希望看到的。因此,我們可以使用 <Link>
來導航到一個特定的 URL,並在不刷新的情況下重新渲染視圖。
現在,我們的程序應該能正常運行了。刪除項目 src
文件夾中除 index.js
和 App.js
以外的所有文件,然後按如下步驟更新 App.js
:
import { Link, Route, Routes } from 'react-router-dom';
const Home = () => (
<div>
<h2>Home</h2>
<p>Welcome to our homepage!</p>
</div>
);
const Categories = () => (
<div>
<h2>Categories</h2>
<p>Browse items by category.</p>
</div>
);
const Products = () => (
<div>
<h2>Products</h2>
<p>Browse individual products.</p>
</div>
);
export default function App() {
return (
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/categories">Categories</Link>
</li>
<li>
<Link to="/products">Products</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/categories" element={<Categories />} />
<Route path="/products" element={<Products />} />
</Routes>
</div>
);
}
在這裏,我們聲明瞭三個組件:<Home>
、<Categories>
和 <Products>
,它們分別代表應用中的不同頁面。從 React Router 中導入的 <Routes>
和 <Route>
組件用於定義路由邏輯。
在 <App>
組件中,我們有一個基本的導航菜單,其中每個菜單項都是 React Router 中的 <Link>
組件。<Link>
組件用於創建導航鏈接,每個鏈接分別與特定路徑(/
、/categories
和 /products
)相關聯。請注意,在一個較大的應用中,這個菜單可以封裝在一個佈局組件中,以便在不同的視圖中保持一致的結構。你可能還想爲當前選定的導航項添加某種激活態類名(如使用 NavLink 組件)。不過,爲了專注基礎知識,我們在此略過這塊內容。
在導航菜單代碼下方,<Routes>
組件作爲容器,內部包括多個 <Route>
組件。每個 <Route>
組件都有路徑和 React 組件參數,當路徑與當前 URL 匹配時,React 組件將被呈現。例如,當 URL 爲 /categories
時,將呈現 <Categories>
組件。
注意:在以前版本的 React Router 中,/
會同時匹配 /
和 /categories
,這意味着兩個組件都會被渲染。解決這個問題的辦法是給指定 exact
屬性,確保只匹配精確路徑。這種行爲在 v6 版本中有所改變,現在默認情況下所有路徑都是完全匹配的。正如我們將在下一節看到的,如果因爲有子路由而想匹配更多的 URL,可以使用尾部的 *
,例如 <Route path="categories/*" ...>。
如果你有在跟隨教程運行代碼,請花點時間點擊一下程序,確保一切都符合預期。
嵌套路由
頂級路由固然很好,但不久之後,大多數應用程序都需要嵌套路由,例如,顯示特定產品或編輯特定用戶。
在 React Router v6 中,路由是通過在 JSX 代碼中將 <Route>
組件置於其他 <Route>
組件內來嵌套的。這樣,嵌套的 <Route>
組件就自然反映了它們所代表的 URL 的嵌套結構。
讓我們看看如何在應用中實現這一點。像這樣更改 App.js
(其中...
表示前面的代碼保持不變):
import { Link, Route, Routes } from 'react-router-dom';
import { Categories, Desktops, Laptops } from './Categories';
const Home = () => ( ... );
const Products = () => ( ... );
export default function App() {
return (
<div>
<nav>...</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/categories/" element={<Categories />}>
<Route path="desktops" element={<Desktops />} />
<Route path="laptops" element={<Laptops />} />
</Route>
<Route path="/products" element={<Products />} />
</Routes>
</div>
);
}
如你所見,我們將 <Categories>
組件移到了單獨的文件中,現在又從其文件導入了兩個組件,即 <Desktops>
和 <Laptops>
。
我們還對 <Routes>
組件進行了一些修改,稍後再看這塊內容。
首先,在與 App.js
文件相同的文件夾中創建 Categories.js
文件。然後添加以下代碼:
// src/Categories.js
import { Link, Outlet } from 'react-router-dom';
export const Categories = () => (
<div>
<h2>Categories</h2>
<p>Browse items by category.</p>
<nav>
<ul>
<li>
<Link to="desktops">Desktops</Link>
</li>
<li>
<Link to="laptops">Laptops</Link>
</li>
</ul>
</nav>
<Outlet />
</div>
);
export const Desktops = () => <h3>Desktop PC Page</h3>;
export const Laptops = () => <h3>Laptops Page</h3>;
刷新應用(如果開發服務器正在運行,則會自動刷新),然後點擊 Categories 鏈接。現在,你應該可以看到兩個新的菜單項(Desktops 和 Laptops),點擊其中任何一個都會在原 Categories 頁面內顯示一個新內容。
我們剛纔做了什麼?
在 App.js
中,我們將 /categories
路由改成這樣:
<Route path="/categories/" element={<Categories />}>
<Route path="desktops" element={<Desktops />} />
<Route path="laptops" element={<Laptops />} />
</Route>
在更新後的代碼中,/categories
的 <Route>
組件已被修改爲包含兩個嵌套的 <Route>
組件 —— 一個是 /categories/desktops
,另一個是 /categories/laptops
。這一修改說明了 React Router 如何通過路由配置實現路由組合。
通過在 /categories
<Route>
中嵌套 <Route>
組件,我們可以創建結構更合理的 URL 和 UI 層次結構。這樣,當用戶導航到 /categories/desktops
或 /categories/laptops
時,相應的 <Desktops>
或 <Laptops>
組件將在 <Categories>
組件中渲染,從而清晰地展示出各個路由和組件之間的父子關係。
注意:嵌套路由的路徑是由其祖先的路徑和自身的路徑連接而成的。
我們還修改了 <Categories>
組件,使其包含一個 <Outlet />
:
export const Categories = () => (
<div>
<h2>Categories</h2>
...
<Outlet />
</div>
);
<Outlet>
位於父路由元素中,用於渲染其子路由元素。這樣就可以在渲染子路由時顯示嵌套的界面。
這種組合式方法使路由配置更具聲明性,更易於理解,與 React 基於組件的架構非常吻合。
使用 Hook 訪問路由屬性
在之前版本中,某些屬性是隱式傳遞給組件的。例如:
const Home = (props) => {
console.log(props);
return ( <h2>Home</h2> );
};
上述代碼將輸出如下內容:
{
history: { ... },
location: { ... },
match: { ... }
}
在 React Router 第 6 版中,傳遞路由屬性的方法發生了變化,提供了一種更明確的基於 Hook 的方法。路由屬性 history、location 和 match 不再隱式傳遞給組件。相反,現在使用一組 Hook 來訪問這些信息。
例如,要訪問 location
對象,可以使用 useLocation Hook[18]。useMatch Hook[19] 會返回匹配到的路徑參數。history
對象不再顯式使用,而是通過 useNavigate Hook[20] 返回一個函數,讓你以編程方式進行導航。
還有更多 Hook 值得探索,我不在此一一列舉,而是建議你查看官方文檔 [21],在左側邊欄中可以找到可用的 Hook。
接下來,讓我們更詳細地瞭解其中一個 Hook,使我們之前的示例更具活力。
嵌套動態路由
首先,像這樣更改 App.js
中的路由:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/categories/" element={<Categories />}>
<Route path="desktops" element={<Desktops />} />
<Route path="laptops" element={<Laptops />} />
</Route>
<Route path="/products/*" element={<Products />} />
</Routes>
眼尖的人會發現,/products
路由的尾部多了一個 /*
。在 React Router 第 6 版中,/*
是表示 <Products>
組件可以有子路由的一種方式,而且它還是 URL 中 /products
之後可能出現的任何其他路徑段的佔位符。這樣,當你導航到 /products/laptops
這樣的 URL 時,<Products>
組件仍將被匹配和渲染,並能使用自己的嵌套 <Route>
元素進一步處理路徑中的 laptops
部分。
接下來,讓我們把 <Products>
組件移到它自己的文件中:
// src/App.js
...
import Products from './Products';
const Home = () => ( ... );
export default function App() { ... }
最後,創建一個 Products.js
文件並添加以下代碼:
// src/Products.js
import { Route, Routes, Link, useParams } from 'react-router-dom';
const Item = () => {
const { name } = useParams();
return (
<div>
<h3>{name}</h3>
<p>Product details for the {name}</p>
</div>
);
};
const Products = () => (
<div>
<h2>Products</h2>
<p>Browse individual products.</p>
<nav>
<ul>
<li>
<Link to="dell-optiplex-3900">Dell OptiPlex 3090</Link>
</li>
<li>
<Link to="lenovo-thinkpad-x1">Lenovo ThinkPad X1</Link>
</li>
</ul>
</nav>
<Routes>
<Route path=":name" element={<Item />} />
</Routes>
</div>
);
export default Products;
在這裏,我們在 <Route>
(在頁面頂部聲明) 添加 <Item>
組件中。路由的路徑設置爲 :name
,這將匹配其父路由之後的任何路徑,並將該路徑作爲名爲 name
的參數傳遞給 <Item>
組件。
在 <Item>
組件中,我們使用了 useParams Hook
[22]。它會返回當前 URL 中動態參數的鍵 / 值對對象。在 /products/laptops
路徑下,如果我們參數輸出到控制檯,我們會看到:
Object { "*": "laptops", name: "laptops" }
我們可以使用對象解構 [23] 來直接獲取該參數,然後將其渲染在 <h3>
標記中。
試試看! 正如你所看到的,<Item>
組件會捕捉你在導航欄中輸入的任何鏈接,並動態創建一個頁面。
你還可以嘗試添加更多的菜單項:
<li>
<Link to="cyberpowerpc-gamer-xtreme">CyberPowerPC Gamer Xtreme</Link>
</li>
我們的應用將把這些新頁面考慮在內。
這種捕捉 URL 動態片段並將其作爲組件參數的方法可以讓我們根據 URL 結構實現更靈活的路由和組件渲染。
讓我們在下一節中繼續學習。
嵌套路由與路徑參數
在實際應用中,路由必須處理數據並動態顯示數據。假設我們有一些由 API 接口返回的產品數據,格式如下:
const productData = [
{
id: 1,
name: "Dell OptiPlex 3090",
description:
"The Dell OptiPlex 3090 is a compact desktop PC that offers versatile features to meet your business needs.",
status: "Available",
},
{
id: 2,
name: "Lenovo ThinkPad X1 Carbon",
description:
"Designed with a sleek and durable build, the Lenovo ThinkPad X1 Carbon is a high-performance laptop ideal for on-the-go professionals.",
status: "Out of Stock",
},
{
id: 3,
name: "CyberPowerPC Gamer Xtreme",
description:
"The CyberPowerPC Gamer Xtreme is a high-performance gaming desktop with powerful processing and graphics capabilities for a seamless gaming experience.",
status: "Available",
},
{
id: 4,
name: "Apple MacBook Air",
description:
"The Apple MacBook Air is a lightweight and compact laptop with a high-resolution Retina display and powerful processing capabilities.",
status: "Out of Stock",
},
];
我們假設還需要以下路徑的路由:
-
/products
:顯示產品列表。 -
/products/:productId
:如果存在帶有:productId
的產品,則應顯示產品數據;如果不存在,則應顯示錯誤信息。
用以下內容替換 Products.js
的當前內容(確保複製了上面的產品數據):
import { Link, Route, Routes } from "react-router-dom";
import Product from "./Product";
const productData = [ ... ];
const Products = () => {
const linkList = productData.map((product) => {
return (
<li key={product.id}>
<Link to={`${product.id}`}>{product.name}</Link>
</li>
);
});
return (
<div>
<h3>Products</h3>
<p>Browse individual products.</p>
<ul>{linkList}</ul>
<Routes>
<Route path=":productId" element={<Product data={productData} />} />
<Route index element={<p>Please select a product.</p>} />
</Routes>
</div>
);
};
export default Products;
在組件內部,我們使用每個產品的 id
屬性建立一個 <Link>
組件列表。我們將其存儲在 linkList
變量中,然後再將其渲染到頁面上。
接下來是兩個 <Route>
組件。第一個有 path
屬性,值爲 :productId
,(如前所述)這是一個路由參數。這樣,我們就可以使用該 URL 中 productId
值。此 <Route>
組件的 element
屬性被設置爲 <Product>
組件,並將 productData
數組作爲屬性傳遞給它。只要 URL 匹配上,就會渲染 <Product>
組件,並從 URL 中獲取到相應的 productId
。
第二個 <Route>
組件使用 index 屬性 [24],只要 URL 與基礎路徑完全匹配,就會渲染文本 "請選擇產品"。index
參數表示此路由是 <Routes>
設置中的基礎路由或 “默認” 路由。因此,當 URL 與基礎路徑(即 /products
)相匹配時,就會顯示這條信息。
現在,我們來看看上面提到的 <Product>
組件的代碼。你需要創建該文件 - src/Product.js
:
import { useParams } from 'react-router-dom';
const Product = ({ data }) => {
const { productId } = useParams();
const product = data.find((p) => p.id === Number(productId));
return (
<div>
{product ? (
<div>
<h3> {product.name} </h3>
<p>{product.description}</p>
<hr />
<h4>{product.status}</h4>
</div>
) : (
<h2>Sorry. Product doesn't exist.</h2>
)}
</div>
);
};
export default Product;
在這裏,我們使用 useParams
Hook 以鍵 / 值對的形式訪問 URL 路徑的動態部分。然後我們使用解構來獲取我們想要的數據(productId
)。
在 data
數組中使用 find
方法搜索並返回第一個 id
屬性與從 URL 參數中獲取的 productId
匹配的元素。
現在,當你在瀏覽器中訪問應用並選擇產品時,就會看到一個子菜單,該菜單會顯示產品數據。
在繼續之前,請先試玩一下 demo 程序。確保一切正常,並瞭解代碼中發生了什麼。
受保護的路由
許多現代 web 應用的一個共同要求是確保只有登錄用戶才能訪問網站的某些部分。在下一節中,我們將介紹如何實現受保護路由,這樣如果有人試圖訪問 /admin
,就會被要求登錄。
不過,我們需要先了解 React Router 的幾個知識點。
在 React Router v6 中通過編程手動導航
在第 6 版中,可通過 useNavigate
Hook 以編程方式重定向到新地址。該 Hook 提供了一個函數,可用於以編程方式導航到不同的路徑。它可以接受一個對象作爲第二個參數,用於指定各種選項。例如:
const navigate = useNavigate();
navigate('/login', {
state: { from: location },
replace: true
});
這將把用戶重定向到 /login
,同時傳遞一個 location
值以存儲在歷史狀態中,然後我們可以通過 useLocation
Hook 在目標路徑上訪問該值。指定 replace: true
還將替換歷史堆棧中的當前條目,而不是添加新條目。這模仿了 v5 中現已停用的 <Redirect>
組件的行爲。
概括地說:如果有人在登出狀態下試圖訪問 /admin
路由,他們將被重定向到 /login
路由。有關當前位置的信息通過 state
屬性傳遞,因此如果身份驗證成功,用戶就會被重定向到他們最初試圖訪問的頁面。
自定義路由
接下來我們需要了解的是自定義路由。React Router 中的自定義路由是一個用戶定義的組件,可以在路由過程中實現額外的功能或行爲。它可以封裝特定的路由邏輯(如身份驗證檢查),並根據特定條件渲染不同的組件或執行操作。
在 src
目錄中創建一個新文件 PrivateRoute.js
,並添加以下內容:
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { fakeAuth } from './Login';
const PrivateRoute = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
if (!fakeAuth.isAuthenticated) {
navigate('/login', {
state: { from: location },
replace: true,
});
}
}, [navigate, location]);
return fakeAuth.isAuthenticated ? children : null;
};
export default PrivateRoute;
這裏有幾個步驟。首先,我們導入了一個名爲 fakeAuth
的東西,它提供了一個 isAuthenticated
屬性。我們很快就會更詳細地瞭解這個屬性,但現在只要知道我們將用它來確定用戶的登錄狀態就足夠了。
該組件接受一個 children
屬性。這將是 <PrivateRoute>
組件被調用時包裹的受保護內容。例如:
<PrivateRoute>
<Admin /> <-- children
</PrivateRoute>
在組件主體中,useNavigate
和 useLocation
Hook 分別用於獲取 navigate
函數和當前 location
對象。如果用戶未通過 !fakeAuth.isAuthenticated
檢查,則會調用 navigate
函數將用戶重定向到 /login
路由,如上一節所述。
組件的返回語句會再次檢查身份驗證狀態。如果用戶已通過身份驗證,則渲染其子組件。如果用戶未通過身份驗證,則返回空值,不會渲染任何內容。
還請注意,我們是在 React 的 useEffect
[25] Hook 中調用 navigate
。這是因爲 navigate
函數不應直接在組件主體內部調用,因爲它會在渲染過程中觸發狀態更新。將其寫在 useEffect
Hook 中可確保在組件渲染後調用。
重要安全聲明
在實際應用中,「對服務器上受保護資源的任何請求,你都需要做認證」。讓我再說一遍...
在實際應用中,「對服務器上受保護資源的任何請求,你都需要做認證」。
這是因爲在客戶端上運行的任何程序都有可能被反編譯和篡改。例如,在上面的代碼中,只要打開 React 的開發工具,將 isAuthenticated
的值改爲 true
,就可以訪問受保護的內容。
React 應用中的身份驗證值得單獨編寫教程,但實現身份驗證的一種方法是使用 JSON Web Token[26]。例如,你可以在服務器上設置一個接受用戶名和密碼的接口。當它收到這些信息(通過 Ajax)時,它會檢查憑證是否有效。如果有效,它會響應一個 JWT,React 應用程序會保存該 JWT(例如,保存在 sessionStorage
中);如果無效,它會向客戶端發送一個 401 Unauthorized
響應。
假設登錄成功,客戶端就會將 JWT 作爲請求頭信息與任何受保護資源請求一起發送。然後,服務器在發送響應之前會對其進行驗證。
在存儲密碼時,「服務器不會以明文形式存儲」。相反,它會對密碼進行加密,例如使用 bcryptjs[27]。
實現受保護的路由
現在,讓我們實現受保護的路由。像這樣修改 App.js
:
import { Link, Route, Routes } from 'react-router-dom';
import { Categories, Desktops, Laptops } from './Categories';
import Products from './Products';
import Login from './Login';
import PrivateRoute from './PrivateRoute';
const Home = () => (
<div>
<h2>Home</h2>
<p>Welcome to our homepage!</p>
</div>
);
const Admin = () => (
<div>
<h2>Welcome admin!</h2>
</div>
);
export default function App() {
return (
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/categories">Categories</Link>
</li>
<li>
<Link to="/products">Products</Link>
</li>
<li>
<Link to="/admin">Admin area</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/categories/" element={<Categories />}>
<Route path="desktops" element={<Desktops />} />
<Route path="laptops" element={<Laptops />} />
</Route>
<Route path="/products/*" element={<Products />} />
<Route path="/login" element={<Login />} />
<Route
path="/admin"
element={
<PrivateRoute>
<Admin />
</PrivateRoute>
}
/>
</Routes>
</div>
);
}
如你所見,我們在文件頂部添加了 <Admin>
組件,並在 <Routes>
組件中使用了 <PrivateRoute>
。如前所述,如果用戶已登錄,該自定義路由將渲染 <Admin>
組件。否則,用戶將被重定向到 /login
。
最後,創建 Login.js
,並添加以下代碼:
// src/Login.js
import { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
export default function Login() {
const navigate = useNavigate();
const { state } = useLocation();
const from = state?.from || { pathname: '/' };
const [redirectToReferrer, setRedirectToReferrer] = useState(false);
const login = () => {
fakeAuth.authenticate(() => {
setRedirectToReferrer(true);
});
};
useEffect(() => {
if (redirectToReferrer) {
navigate(from.pathname, { replace: true });
}
}, [redirectToReferrer, navigate, from.pathname]);
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);
},
};
在上面的代碼中,我們試圖獲取用戶在被要求登錄之前要訪問的 URL 的值。如果不存在,我們就將其設置爲 { pathname:"/" }
。
然後,我們使用 React 的 useState Hook[28] 將 redirectToReferrer
屬性初始化爲 false
。根據該屬性的值,用戶要麼會被重定向到他們要去的地方(即用戶已登錄),要麼用戶會看到一個登錄按鈕。
一旦按鈕被點擊,fakeAuth.authenticate
方法就會被執行,它會將 fakeAuth.isAuthenticated
設置爲 true
,並(在回調函數中)將 redirectToReferrer
的值更新爲 true
。這將觸發組件重新渲染,用戶被重定向。
可運行的 Demo
讓我們把所有內容拼起來。下面是我們使用 React 路由構建的應用的最終示例。
最終示例 [29]
React Router 6.4
在結束之前,我們應該提到 React Router v6.4 的發佈。儘管看起來只是一個不起眼的版本,但這個版本引入了一些突破性的新功能。例如,它現在包含了 Remix 中的數據加載和可變 (mutation) API,爲用戶界面與數據保持同步引入了全新的範式。
從 6.4 版開始,您可以爲每個路由定義一個 loader
函數,負責獲取該路由所需的數據。在組件中,你可以使用 useLoaderData
Hook 來訪問加載的數據。當用戶導航到路由時,React Router 會自動調用相關的 loader
函數來獲取數據,並通過 useLoaderData
鉤子將數據傳遞給組件,而無需使用任何 useEffect
。這促進了數據獲取與路由直接綁定的模式。
此外還有一個新的 <Form>
組件,它可以防止瀏覽器將請求發送到服務器,而是將其發送到路由的 action
中。React Router 會在 action
完成後自動重新驗證頁面上的數據,這意味着所有 useLoaderData
hooks 都會更新,用戶界面也會自動與數據保持同步。
要使用這些新的 APIS,你需要使用新的 <RouterProvider />
組件。這需要使用新的 createBrowserRouter[30] 函數創建一個 router
參數。
詳細討論所有這些更改超出了本文的討論範圍,但如果你想了解更多信息,我建議您閱讀 React Router 官方教程 [31]。
總結
正如你在本文中所看到的,React Router 是一個功能強大的庫,它與 React 相輔相成,可以在你的 React 應用中構建更好的聲明式路由。在撰寫本文時,React Router 的當前版本是 v6.18,自 v5 版本以來,該庫已發生了巨大的變化,部分原因是受到了 Remix[32] 的影響,Remix 是由同一作者編寫的全棧 Web 框架。
在本教程中,我們學習了:
-
如何設置和安裝 React 路由
-
路由的基礎知識和一些基礎組件,例如
<Routes>
、<Route>
和<Link>
-
如何創建一個小的路由系統,其中包含導航路由和嵌套路由
-
如何使用路徑參數創建動態路由
-
如何使用 React 路由的 Hook 及其更新的路由渲染模式
最後,在編寫受保護路由的最終示例時,我們學習了一些高級路由技術。
FAQs
「React Router v6 中的路由是如何工作的?」
該版本推出了 <Routes>
和 <Route>
的新路由語法。<Routes>
組件包裹 <Route>
組件,這些組件指定了路徑和路徑與 URL 匹配時要渲染的元素。
「如何設置嵌套路由?」
嵌套路由是通過在 JSX 代碼中將 <Route>
組件置於其他 <Route>
組件內來創建的。這樣,嵌套的 <Route>
組件就自然地反映了它們所代表的 URL 的嵌套結構。
「如何將用戶重定向到另一個頁面?」
你可以使用 useNavigate
hook 以編程方式將用戶導航到另一個頁面。例如,const navigate = useNavigate();
然後使用 navigate('/path');
重定向到想要跳轉的路徑。
「如何將參數傳遞給組件?」
在第 6 版中,你可以在 <Route>
組件的 element
屬性中把參數傳遞給組件,就像這樣: <Route path="/path" element={<Component prop={value} />} />
.
「如何在 React Router v6 中訪問 URL 參數?」
可以使用 useParams
hook 訪問 URL 參數。例如,如果路由定義爲 <Route path=":id" element={<Component />} />
,則可以使用 const { id } = useParams();
訪問 <Component />
內的 id 參數。
「React Router v6.4 有哪些新特性?」
6.4 版引入了許多受 Remix 啓發的新功能,如數據加載器 (loader) 和 createBrowserRouter,旨在改進數據獲取和提交。你可以在這裏 [33] 找到新功能的詳盡列表。
「如何從 React Router v5 遷移到 v6?」
遷移包括將路由配置更新爲新的 <Routes>
和 <Route>
組件語法,將 hooks 和其他 API 方法更新爲 v6 對應方法,以及處理應用中路由邏輯的任何破壞性更改。你可以在此處 [34] 找到官方指南。
Reference
[1]
React Router v6: A Beginner’s Guide: https://www.sitepoint.com/react-router-complete-guide
[2]
James Hibbard: https://www.sitepoint.com/author/jhibbard/
[3]
React Router: https://reactrouter.com/en/main
[4]
GitHub: https://github.com/sitepoint-editors/react-router-demo
[5]
下載正確的二進制文件: https://nodejs.org/en/download/
[6]
使用版本管理器的教程: https://www.sitepoint.com/quick-tip-multiple-versions-node-nvm/
[7]
請點擊此處: https://www.sitepoint.com/npm-guide/
[8]
react-router: https://www.npmjs.com/package/react-router
[9]
react-router-dom: https://www.npmjs.com/package/react-router-dom
[10]
react-router-native: https://www.npmjs.com/package/react-router-native
[11]
多種類型: https://reactrouter.com/en/main/routers/picking-a-router
[12]
BrowserRouter: https://reactrouter.com/en/main/router-components/browser-router
[13]
HashRouter: https://reactrouter.com/en/main/router-components/hash-router
[14]
HTML5 History API: https://developer.mozilla.org/en-US/docs/Web/API/History_API
[15]
在此閱讀有關差異的更多信息: https://stackoverflow.com/questions/51974369/what-is-the-difference-between-hashrouter-and-browserrouter-in-react
[16]
remix-run: https://github.com/remix-run/history
[17]
useNavigate Hook
: https://reactrouter.com/en/main/hooks/use-navigate
[18]
useLocation Hook: https://reactrouter.com/en/main/hooks/use-location
[19]
useMatch Hook: https://reactrouter.com/en/main/hooks/use-match
[20]
useNavigate Hook: https://reactrouter.com/en/main/hooks/use-navigate
[21]
官方文檔: https://reactrouter.com/en/main
[22]
useParams Hook
: https://reactrouter.com/en/main/hooks/use-params
[23]
對象解構: https://www.sitepoint.com/es6-destructuring-assignment/
[24]
index 屬性: https://reactrouter.com/en/main/route/route#index
[25]
useEffect
: https://react.dev/reference/react/useEffect
[26]
JSON Web Token: https://www.sitepoint.com/using-json-web-tokens-node-js/
[27]
bcryptjs: https://www.npmjs.com/package/bcrypt
[28]
useState Hook: https://react.dev/reference/react/useState
[29]
最終示例: https://codesandbox.io/p/sandbox/react-router-demo-wpwmcv
[30]
createBrowserRouter: https://reactrouter.com/en/main/routers/create-browser-router
[31]
React Router 官方教程: https://reactrouter.com/en/main/start/tutorial
[32]
Remix: https://remix.run/
[33]
這裏: https://reactrouter.com/en/main/start/overview
[34]
此處: https://reactrouter.com/en/main/upgrading/v5
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/-cE9Zt9Z13II-lh85mOITg