React Router 是 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 可让你声明式地处理路由,声明式路由方法允许你通过说 "路由应该是这样的" 来控制应用中的页面和路由绑定。
js
<Route path="/about" element={<About />} />
你可以将 <Route>
组件放在任何位置,它都能按你期望正确渲染内容。因为 <Route>
、<Link>
和我们将要处理的所有 React Router 其他 API 都只是组件,因此你可以轻松地在 React 中接入路由。
注意:大家普遍误认为 React Router 是由 Facebook 开发的官方路由解决方案。实际上,它是 Remix Software 开发和维护的第三方库。
概述
本教程分为不同部分。首先,我们将使用 npm 安装 React 和 React Router。然后,我们将直接开始接触一些基础知识。你将看到在实际应用中 React Router 的不同代码示例。本教程涵盖的示例包括:
- 基本导航路由
- 嵌套路由
- 带路径参数的嵌套路由
- 受保护路由
所有概念都会在构建这些示例的过程中介绍。
该项目的全部代码都可以在 GitHub 上找到。
让我们开始吧!
安装 React Router
要学习本教程,你需要在电脑上安装最新版本的 Node。如果还没安装,请访问 Node 主页,根据你的系统下载正确的二进制文件。或者,你也可以考虑使用版本管理器来安装 Node。我们在此处提供了使用版本管理器的教程。
Node 捆绑了 npm,它是 JavaScript 的包管理器,我们将用它来安装一些要使用的库。有关 npm 的更多信息,请点击此处。
你可以在命令行中执行以下命令,检查两者是否都已正确安装:
node
node -v
> 20.9.0
npm -v
> 10.1.0
完成上述步骤后,让我们使用 Create React App 工具创建一个新的 React 项目。你可以全局安装,也可以使用 npx,就像这样:
js
npx create-react-app react-router-demo
完成后,切换到新创建的目录:
js
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
软件包:
js
npm install react-router-dom
然后启动开发服务器:
js
npm run start
恭喜。 你现在拥有了一个安装了 React Router 的可运行 React 应用。您可以在 http://localhost:3000/ 上查看程序的运行情况。
React Router 基础知识
Router
组件
我们需要做的第一件事是用 <Router>
组件(由 React Router 提供)来包裹 <App>
组件。路由有多种类型,在我们的案例中,有两种路由值得考虑:
它们之间的主要区别体现在所创建的 URL 上:
js
// <BrowserRouter>
https://example.com/about
// <HashRouter>
https://example.com/#/about
<BrowserRouter>
是一种常用的路由,它利用 HTML5 History API 将用户界面与 URL 同步,提供了一种没有 hash 片段的更简洁的 URL 结构。而 <HashRouter>
利用 URL 的 hash 部分(window.location.hash
)来管理路由,它的优势在于无需对服务器增加配置和优秀的兼容性。你可以在此阅读有关差异的更多信息。
还请注意,在 React Router 的最新版本(v6.4)中引入了四个新的路由,它们支持各种新的数据 API。在本教程中,我们将重点介绍传统路由,因为这些路由功能强大、文档齐全,而且在众多项目中都有使用。不过,我们将在后面的章节中深入介绍 v6.4 中的新功能。
因此,让我们导入 <BrowserRouter>
组件,并用它包裹 <App>
组件。将 index.js
改为如下所示:
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
每个 <Router>
组件都会创建一个 history
对象,用于跟踪堆栈中的当前路由地址和前一个路由地址。当当前路由地址发生变化时,视图就会重新渲染,从而让你有一种导航的感觉。
如何更改当前路由地址?在 React Router v6 中,useNavigate Hook
提供了一个路由跳转的函数: 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
:
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
(其中...
表示前面的代码保持不变):
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
文件。然后添加以下代码:
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
路由改成这样:
js
<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 />
:
js
export const Categories = () => (
<div>
<h2>Categories</h2>
...
<Outlet />
</div>
);
<Outlet>
位于父路由元素中,用于渲染其子路由元素。这样就可以在渲染子路由时显示嵌套的界面。
这种组合式方法使路由配置更具声明性,更易于理解,与 React 基于组件的架构非常吻合。
使用 Hook 访问路由属性
在之前版本中,某些属性是隐式传递给组件的。例如:
js
const Home = (props) => {
console.log(props);
return ( <h2>Home</h2> );
};
上述代码将输出如下内容:
js
{
history: { ... },
location: { ... },
match: { ... }
}
在 React Router 第 6 版中,传递路由属性的方法发生了变化,提供了一种更明确的基于 Hook 的方法。路由属性 history、location 和 match 不再隐式传递给组件。相反,现在使用一组 Hook 来访问这些信息。
例如,要访问 location
对象,可以使用 useLocation Hook。useMatch Hook 会返回匹配到的路径参数。history
对象不再显式使用,而是通过 useNavigate Hook 返回一个函数,让你以编程方式进行导航。
还有更多 Hook 值得探索,我不在此一一列举,而是建议你查看官方文档,在左侧边栏中可以找到可用的 Hook。
接下来,让我们更详细地了解其中一个 Hook,使我们之前的示例更具活力。
嵌套动态路由
首先,像这样更改 App.js
中的路由:
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>
组件移到它自己的文件中:
js
// src/App.js
...
import Products from './Products';
const Home = () => ( ... );
export default function App() { ... }
最后,创建一个 Products.js
文件并添加以下代码:
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
。它会返回当前 URL 中动态参数的键/值对对象。在 /products/laptops
路径下,如果我们参数输出到控制台,我们会看到:
js
Object { "*": "laptops", name: "laptops" }
我们可以使用对象解构来直接获取该参数,然后将其渲染在 <h3>
标记中。
试试看! 正如你所看到的,<Item>
组件会捕捉你在导航栏中输入的任何链接,并动态创建一个页面。
你还可以尝试添加更多的菜单项:
js
<li>
<Link to="cyberpowerpc-gamer-xtreme">CyberPowerPC Gamer Xtreme</Link>
</li>
我们的应用将把这些新页面考虑在内。
这种捕捉 URL 动态片段并将其作为组件参数的方法可以让我们根据 URL 结构实现更灵活的路由和组件渲染。
让我们在下一节中继续学习。
嵌套路由与路径参数
在实际应用中,路由必须处理数据并动态显示数据。假设我们有一些由 API 接口返回的产品数据,格式如下:
js
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
的当前内容(确保复制了上面的产品数据):
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 属性,只要 URL 与基础路径完全匹配,就会渲染文本 "请选择产品"。index
参数表示此路由是 <Routes>
设置中的基础路由或 "默认" 路由。因此,当 URL 与基础路径(即 /products
)相匹配时,就会显示这条信息。
现在,我们来看看上面提到的 <Product>
组件的代码。你需要创建该文件 - src/Product.js
:
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 提供了一个函数,可用于以编程方式导航到不同的路径。它可以接受一个对象作为第二个参数,用于指定各种选项。例如:
js
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
,并添加以下内容:
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>
组件被调用时包裹的受保护内容。例如:
js
<PrivateRoute>
<Admin /> <-- children
</PrivateRoute>
在组件主体中,useNavigate
和 useLocation
Hook 分别用于获取 navigate
函数和当前 location
对象。如果用户未通过 !fakeAuth.isAuthenticated
检查,则会调用 navigate
函数将用户重定向到 /login
路由,如上一节所述。
组件的返回语句会再次检查身份验证状态。如果用户已通过身份验证,则渲染其子组件。如果用户未通过身份验证,则返回空值,不会渲染任何内容。
还请注意,我们是在 React 的 useEffect
Hook 中调用 navigate
。这是因为 navigate
函数不应直接在组件主体内部调用,因为它会在渲染过程中触发状态更新。将其写在 useEffect
Hook 中可确保在组件渲染后调用。
重要安全声明
在实际应用中,对服务器上受保护资源的任何请求,你都需要做认证。让我再说一遍...
在实际应用中,对服务器上受保护资源的任何请求,你都需要做认证。
这是因为在客户端上运行的任何程序都有可能被反编译和篡改。例如,在上面的代码中,只要打开 React 的开发工具,将 isAuthenticated
的值改为 true
,就可以访问受保护的内容。
React 应用中的身份验证值得单独编写教程,但实现身份验证的一种方法是使用 JSON Web Token。例如,你可以在服务器上设置一个接受用户名和密码的接口。当它收到这些信息(通过 Ajax)时,它会检查凭证是否有效。如果有效,它会响应一个 JWT,React 应用程序会保存该 JWT(例如,保存在 sessionStorage
中);如果无效,它会向客户端发送一个 401 Unauthorized
响应。
假设登录成功,客户端就会将 JWT 作为请求头信息与任何受保护资源请求一起发送。然后,服务器在发送响应之前会对其进行验证。
在存储密码时,服务器不会以明文形式存储 。相反,它会对密码进行加密,例如使用 bcryptjs。
实现受保护的路由
现在,让我们实现受保护的路由。像这样修改 App.js
:
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
,并添加以下代码:
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 将 redirectToReferrer
属性初始化为 false
。根据该属性的值,用户要么会被重定向到他们要去的地方(即用户已登录),要么用户会看到一个登录按钮。
一旦按钮被点击,fakeAuth.authenticate
方法就会被执行,它会将 fakeAuth.isAuthenticated
设置为 true
,并(在回调函数中)将 redirectToReferrer
的值更新为 true
。这将触发组件重新渲染,用户被重定向。
可运行的 Demo
让我们把所有内容拼起来。下面是我们使用 React 路由构建的应用的最终示例。
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 函数创建一个 router
参数。
详细讨论所有这些更改超出了本文的讨论范围,但如果你想了解更多信息,我建议您阅读 React Router 官方教程。
总结
正如你在本文中所看到的,React Router 是一个功能强大的库,它与 React 相辅相成,可以在你的 React 应用中构建更好的声明式路由。在撰写本文时,React Router 的当前版本是 v6.18,自 v5 版本以来,该库已发生了巨大的变化,部分原因是受到了 Remix 的影响,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,旨在改进数据获取和提交。你可以在这里找到新功能的详尽列表。
如何从 React Router v5 迁移到 v6?
迁移包括将路由配置更新为新的 <Routes>
和 <Route>
组件语法,将 hooks 和其他 API 方法更新为 v6 对应方法,以及处理应用中路由逻辑的任何破坏性更改。你可以在此处找到官方指南。