目录
在早期版本中,一些属性被隐式传递给组件,如下:
const Home = (props) => {
console.log(props);
return ( <h2>Home</h2> );
};
上面内容将记录下面内容:
{
history: { ... },
location: { ... },
match: { ... }
}
在 React Router 版本 6 中, router props 的传递方法已变为提供更明确且基于钩子的方法。路由器 props history
、location
和match
不再隐式传递给组件。相反,提供了一组挂钩来访问此信息。
例如,要访问location
对象,您可以使用useLocation 挂钩。useMatch挂钩返回有关给定路径的路由的匹配数据。该history
对象不再显式地显示,而是useNavigate 挂钩将返回一个允许您以编程方式导航的函数。
还有更多钩子需要探索,具体查看官方文档。
下面我们更详细的了解其中的一个钩子,使我们之前的示例更加动态灵活。
嵌套动态路由
首先,修改App.ts如下:
<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 中,/*
是一种指示组件可以具有子路由的方法,并且它是URL 中<Products>
可能遵循的任何其他路径段的占位符。/products
这样,当您导航到类似 的 URL 时/products/laptops
,<Products>
组件仍将被匹配和渲染,并且它将能够laptops
使用自己的嵌套<Route>
元素进一步处理路径的部分。
接下来,让我们将<Products>
组件移动到它自己的文件中:
// src/App.tsx
...
import Products from './Products';
const Home = () => ( ... );
export default function App() { ... }
创建一个Products.tsx
文件并添加以下内容:
// src/Products.tsx
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
,我们会看到:
Object { "*": "laptops", name: "laptops" }
我们可以通过对象解构来获取这个参数,然后将其渲染在<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.tsx
为以下内容(确保复制上面的产品数据):
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;
在组件内部,我们使用每个产品的属性构建一个<Link>
组件列表。在将其渲染到页面之前,id
我们将其存储在linkList
变量中。
接下来是两个<Route>
组件。第一个有一个path
值为 的 prop :productId
,它(正如我们之前看到的)是一个路由参数。这使我们能够抓取此段中 URL 的值并将其用作productId
。element
该组件的 prop被<Route>
设置为渲染<Product>
组件,并将productData
数组作为 prop 传递给它。每当 URL 与模式匹配时,<Product>
就会呈现该组件,并productId
从 URL 中捕获相应的内容。
当 URL 与基本路径完全匹配时,第二个<Route>
组件使用索引属性来呈现文本“请选择产品”。该index
prop 表示该路由是此设置中的基本或“索引”路由<Routes>
。因此,当 URL 与基本路径匹配时(也就是说,/products
)将显示此消息。
<Product>
现在,这是我们上面引用的组件的代码。您需要在以下位置创建此文件src/Product.tsx
:
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
钩子以键/值对的形式访问 URL 路径的动态部分。同样,我们使用解构来获取我们感兴趣的数据(productId
)。
该find
方法用于数组中data
,以搜索并返回其id
属性与productId
从 URL 参数中检索到的属性相匹配的第一个元素。
现在,当您在浏览器中访问该应用程序并选择Products时,您将看到呈现的子菜单,该子菜单又显示产品数据。
在继续之前,先尝试一下演示。确保一切正常并且您了解代码中发生的情况。
保护路线
许多现代 Web 应用程序的一个常见要求是确保只有登录用户才能访问网站的某些部分。在下一节中,我们将了解如何实现受保护的路由,以便如果有人尝试访问/admin
,他们将被要求登录。
然而,我们需要首先介绍 React Router 的几个方面。
在 React Router v6 中以编程方式导航
在版本 6 中,通过挂钩实现以编程方式重定向到新位置useNavigate
。该挂钩提供了一个可用于以编程方式导航到不同路线的函数。它可以接受一个对象作为第二个参数,用于指定各种选项。例如:
const navigate = useNavigate();
navigate('/login', {
state: { from: location },
replace: true
});
这会将用户重定向到/login
,并传递一个location
值以存储在历史状态中,然后我们可以通过钩子在目标路由上访问该值useLocation
。指定replace: true
还将替换历史堆栈中的当前条目,而不是添加新条目。<Redirect>
这模仿了v5 中现已失效的组件的行为。
/admin
总结一下:如果有人在注销时尝试访问该路线,他们将被重定向到该/login
路线。有关当前位置的信息通过state
传递,因此如果身份验证成功,用户可以重定向回他们最初尝试访问的页面。
自定义路线
接下来我们需要考虑的是自定义路由。React Router 中的自定义路由是用户定义的组件,允许在路由过程中实现附加功能或行为。它可以封装特定的路由逻辑,例如身份验证检查,并根据某些条件渲染不同的组件或执行操作。
在目录src新建文件PrivateRoute.tsx
,添加以下内容:
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
prop。<PrivateRoute>
这将是组件在调用时包裹的受保护内容。例如:
<PrivateRoute>
<Admin /> <-- children -->
</PrivateRoute>
在组件中,useNavigate
和useLocation
钩子分别用于获取navigate
函数和当前location
对象。如果用户未经过身份验证(如 所检查)!fakeAuth.isAuthenticated
,navigate
则调用该函数将用户重定向到/login
路由,如上一节中所述。
组件的返回语句再次检查身份验证状态。如果用户通过身份验证,则会呈现其子组件。如果用户未通过身份验证,null
则返回,不呈现任何内容。
另请注意,我们将调用包装navigate
在 React 的useEffect hook中。这是因为该navigate
函数不应在组件体内直接调用,因为它会在渲染期间导致状态更新。将其包装在useEffect
钩子内可确保在渲染组件后调用它。
重要安全通知
在实际应用程序中,您需要验证对服务器上受保护资源的任何请求。
在实际应用程序中,您需要验证对服务器上受保护资源的任何请求。
这是因为在客户端上运行的任何内容都可能被逆向工程和篡改。例如,在上面的代码中,我们可以打开 React 的开发工具并更改isAuthenticated
to的值true
,从而获得对受保护区域的访问权限。
React 应用程序中的身份验证值得单独编写一篇教程,但实现它的一种方法是使用JSON Web Tokens。例如,您的服务器上可能有一个接受用户名和密码组合的端点。当它收到这些(通过 Ajax)时,它会检查凭据是否有效。如果是,它会使用 JWT 进行响应,React 应用程序会保存该 JWT(例如,在 中sessionStorage
),如果不是,它会将响应发送401 Unauthorized
回客户端。
假设登录成功,客户端会将 JWT 作为标头以及对受保护资源的任何请求发送。然后,服务器会在发送响应之前对其进行验证。
当存储密码时,服务器不会以明文形式存储它们。相反,它会对它们进行加密——例如,使用bcryptjs。
实施保护路由
现在让我们实现受保护的路由。App.tsx
像这样改变:
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>
组件添加到文件顶部,并将我们的内容包含<PrivateRoute>
在该<Routes>
组件中。如前所述,<Admin>
如果用户登录,此自定义路由将呈现该组件。否则,用户将被重定向到/login
。
最后,创建Login.tsx
并添加以下代码:
// src/Login.tsx
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 钩子redirectToReferrer
将属性初始化为false
. 根据此属性的值,用户要么被重定向到他们要去的地方(即用户已登录),要么向用户显示一个用于登录的按钮。
单击按钮后,fakeAuth.authenticate
将执行该方法,该方法设置fakeAuth.isAuthenticated
并true
(在回调函数中)更新redirectToReferrer
to的值true
。这会导致组件重新呈现并且用户被重定向。
React 路由器版本 6.4
从版本 6.4 开始,您可以loader
为每个路由定义一个函数,该函数负责获取该路由所需的数据。在组件内部,您可以使用钩子useLoaderData
来访问加载器函数加载的数据。当用户导航到某个路线时,React Router 会自动调用关联的加载器函数,获取数据,并通过钩子将数据传递给组件useLoaderData
,而无需useEffect
查看。这促进了一种数据获取直接与路由相关的模式。
还有一个新<Form>
组件可以阻止浏览器将请求发送到服务器并将其发送到您的路由action
。然后,React Router 在操作完成后自动重新验证页面上的数据,这意味着所有useLoaderData
挂钩都会更新,并且 UI 会自动与您的数据保持同步。
要使用这些新的 APIS,您需要使用新<RouterProvider />
组件。这需要一个使用新的createBrowserRouter router
函数创建的 prop 。
详细讨论所有这些更改超出了本文的范围,但如果您热衷于了解更多信息,我鼓励您遵循官方React Router 教程。
概括
在本教程中相关内容:
- 如何创建用于导航和嵌套路线的最小路由器
- 如何使用路径参数构建动态路由
- 如何使用 React Router 的钩子及其更新的路由渲染模式