[譯] 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 與頁面渲染內容保持同步的能力。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],在我們的案例中,有兩種路由值得考慮:

它們之間的主要區別體現在所創建的 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。

<Route> 組件是 React Rtouter 中最重要的組件。如果路由地址和當前 URL 路徑匹配,它就會在界面渲染一些內容。正常情況下,<Route> 組件應該有一個名爲 path 的屬性,如果這個屬性值與當前 URL 路徑匹配,組件內容就會被渲染。

<Link> 組件則用於在頁面之間導航。它類似於 HTML 錨點元素 (<a>)。不過,使用錨鏈接會導致整個頁面刷新,這是我們不希望看到的。因此,我們可以使用 <Link> 來導航到一個特定的 URL,並在不刷新的情況下重新渲染視圖。

現在,我們的程序應該能正常運行了。刪除項目 src 文件夾中除 index.jsApp.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.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>

在組件主體中,useNavigateuseLocation 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 框架。

在本教程中,我們學習了:

最後,在編寫受保護路由的最終示例時,我們學習了一些高級路由技術。

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