一个React Router教程,教你如何使用React Router 6。这个React Router v6教程的代码可以在这里找到。为了让你开始,创建一个新的React项目(例如:create-react-app)。之后,按照React Router文档中的官方说明来安装它。
第一个实施细节将是告诉我们的React应用程序,我们要使用React Router。因此,在你的React项目的顶层文件(如index.js)中导入Router组件,React通过使用ReactDOM API钩住了HTML。
import React from 'react';import ReactDOM from 'react-dom';import { BrowserRouter } from 'react-router-dom';
import App from './App';
ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root'));
从这里,我们将继续在App.js文件中实现。当需要时,可以通过自己想出项目结构或按照本指南中关于常见的React项目结构,自由地将组件提取到它们自己的文件夹和文件中。
React 路由器。匹配路由
首先,我们将通过使用React Router的Link组件在我们的App组件中实现导航,以促进React中的路由。我不建议像我一样使用内联样式,所以请随意为你的React项目选择合适的样式策略和样式方法。
import { Link } from 'react-router-dom';
const App = () => { return ( <> <h1>React Router</h1>
<Navigation /> </> );};
const Navigation = () => { return ( <nav style={{ borderBottom: 'solid 1px', paddingBottom: '1rem', }} > <Link to="/home">Home</Link> <Link to="/users">Users</Link> </nav> );};
当你在浏览器中启动你的React应用程序时,你应该能够点击这两个Link组件,它们应该能将你导航到各自的路由。通过检查点击这些链接时浏览器的当前URL来确认这一点。接下来,我们需要通过使用React Router的Route组件将路由映射到实际渲染。
import { Routes, Route, Link } from 'react-router-dom';
const App = () => { return ( <> <h1>React Router</h1>
<Navigation />
<Routes> <Route path="home" element={<Home />} /> <Route path="users" element={<Users />} /> </Routes> </> );};
const Navigation = () => { return ( <nav style={{ borderBottom: 'solid 1px', paddingBottom: '1rem', }} > <Link to="/home">Home</Link> <Link to="/users">Users</Link> </nav> );};
通过检查它们各自的to 和path 属性,你可以看到链接和路由组件之间的直接匹配。当路由匹配时,每个路由组件会渲染一个React元素。既然我们在这里渲染一个React元素,我们也可以传递React道具。缺少的是相应的函数组件的声明。
const Home = () => { return ( <main style={{ padding: '1rem 0' }}> <h2>Home</h2> </main> );};
const Users = () => { return ( <main style={{ padding: '1rem 0' }}> <h2>Users</h2> </main> );};
当回到浏览器时,你应该能够从一个页面浏览到另一个页面(这里:从/home 到/users 路由),同时看到首页和用户组件。基本上这就是React Router的本质:设置Link组件并将其与Route组件相匹配。链接与路由有多对一的关系,因此在你的应用程序中可以有多个链接到同一个路由。
布局路由、索引路由、无匹配路由
接下来你会看到新的主页和用户组件是如何共享同一个布局的。作为React开发者,直觉上我们会从Home和Users组件中提取一个新的组件,以避免重复。在这个新的组件中,我们将使用React的children道具,将组件相互组合。第一步,将样式提取到自己的组件中。
const Home = () => { return ( <> <h2>Home</h2> </> );};
const Users = () => { return ( <> <h2>Users</h2> </> );};
const Layout = ({ children }) => { return <main style={{ padding: '1rem 0' }}>{children}</main>;};
第二,在App组件中渲染。通过使用React的children,Layout组件应该渲染匹配的包围的子路线。
const App = () => { return ( <> ...
<Routes> <Layout> <Route path="home" element={<Home />} /> <Route path="users" element={<Users />} /> </Layout> </Routes> </> );};
但你会看到,这在React Router中是不允许的,你会得到一个异常说。 *<Routes> 的所有组件的子节点必须是<Route> 或<React.Fragment> 。*一个常见的方法是在每个组件中单独使用 Layout 组件(类似于我们之前的做法)或在每个路由组件中使用(就像下面的例子)。
const App = () => { return ( <> ...
<Routes> <Route path="home" element={<Layout><Home /></Layout>} /> <Route path="users" element={<Layout><Users /></Layout>} /> </Routes> </> );};
然而,这给React应用程序增加了不必要的冗余。因此,我们将使用一个所谓的Layout Route来代替重复的Layout组件,它不是一个实际的路由,而只是一种给一组Routes中的每个Route组件的element 相同的周围风格的方法。
const App = () => { return ( <> ...
<Routes> <Route element={<Layout />}> <Route path="home" element={<Home />} /> <Route path="users" element={<Users />} /> </Route> </Routes> </> );};
正如你所看到的,可以将Route组件嵌套在另一个Route组件中--而前者成为所谓的Nested Routes。现在,不要在Layout组件中使用React的儿童,而是使用React Router的Outlet组件作为等价物。
import { Routes, Route, Outlet, Link } from 'react-router-dom';
...
const Layout = () => { return ( <main style={{ padding: '1rem 0' }}> <Outlet /> </main> );};
本质上,Layout组件中的Outlet组件插入了父路由(这里:Home或Users组件)的匹配的子路由(这里:Layout组件)。毕竟,使用Layout路由可以帮助你在一个集体中给每个路由组件以相同的布局(例如,用CSS的样式,用HTML的结构)。
从这里开始,你甚至可以更进一步,将所有App组件的实现细节(标题、导航)移到这个新的Layout组件中。此外,我们可以用NavLink组件来交换链接,以实现所谓的主动链接--向用户显示当前的活动路线。因此,当新的NavLink组件与一个函数一起使用时,我们可以在其style (和className )道具中访问一个isActive 标志。
import { ... NavLink,} from 'react-router-dom';
const App = () => { return ( <Routes> <Route element={<Layout />}> <Route path="home" element={<Home />} /> <Route path="users" element={<Users />} /> </Route> </Routes> );};
const Layout = () => { const style = ({ isActive }) => ({ fontWeight: isActive ? 'bold' : 'normal', });
return ( <> <h1>React Router</h1>
<nav style={{ borderBottom: 'solid 1px', paddingBottom: '1rem', }} > <NavLink to="/home" style={style}>Home</NavLink> <NavLink to="/users" style={style}>Users</NavLink> </nav>
<main style={{ padding: '1rem 0' }}> <Outlet /> </main> </> );};
接下来你可能已经注意到,这个React应用程序缺乏一个基本的路由。虽然我们有一个/home 和/users 路由,但没有/ 路由。你在浏览器的开发工具中也会看到这个警告。没有匹配位置"/"的路由。因此,我们将创建一个所谓的索引路由,作为用户访问/ 路由的回退。这个后备路由的元素可以是一个新的组件,也可以是任何已经匹配的路由(例如,首页应该为路由/ 和/home 呈现,如下面的例子所示)。
const App = () => { return ( <Routes> <Route element={<Layout />}> <Route index element={<Home />} /> <Route path="home" element={<Home />} /> <Route path="users" element={<Users />} /> </Route> </Routes> );};
你可以把索引路由看作是一个默认路由,当父路由匹配,但其子路由都不匹配时。接下来,如果用户导航到一个不匹配的路由(例如:/about ),我们将添加一个所谓的无匹配路由(也叫未找到路由),它相当于一个网站的404页面。
const App = () => { return ( <Routes> <Route element={<Layout />}> <Route index element={<Home />} /> <Route path="home" element={<Home />} /> <Route path="users" element={<Users />} /> <Route path="*" element={<NoMatch />} /> </Route> </Routes> );};
const NoMatch = () => { return (<p>There's nothing here: 404!</p>);};
到目前为止,在使用Routes组件作为Route组件集合的容器时,通过使用Layout Routes、Index Routes和No Match Routes展示了React Router的其他最佳实践。正如你所看到的,也可以将Route组件嵌套到Route组件中。我们将在下一节中学习更多关于嵌套路由的知识。最后但并非最不重要的是,只要我们想显示Link组件的活动状态,我们就可以在Link组件上使用NavLink组件。基本上这就是使用React Router时的基本概念了。
React Router:动态和嵌套路由
接下来我们要用实现细节来装饰Users组件。首先,我们将在我们的App组件中初始化一个项目列表(这里:users )。这个列表只是样本数据,但它也可以在React中从一个远程API中获取。其次,我们将把用户作为道具传递给Users组件。
const App = () => { const users = [ { id: '1', fullName: 'Robin Wieruch' }, { id: '2', fullName: 'Sarah Finnley' }, ];
return ( <Routes> <Route element={<Layout />}> <Route index element={<Home />} /> <Route path="home" element={<Home />} /> <Route path="users" element={<Users users={users} />} /> <Route path="*" element={<NoMatch />} /> </Route> </Routes> );};
Users组件在React中成为一个列表组件,因为它迭代每个用户并返回JSX。在这种情况下,它不仅仅是一个简单的列表,因为我们在其中加入了React Router的Link组件。链接组件中的相对路径提示了各自的动态(这里是:/${user.id} )和嵌套(这里是:/${user.id} 嵌套在/users )路由。
const Users = ({ users }) => { return ( <> <h2>Users</h2>
<ul> {users.map((user) => ( <li key={user.id}> <Link to={`/users/${user.id}`}> {user.fullName} </Link> </li> ))} </ul> </> );};
有了这个新的动态而又嵌套的路由,我们需要在App组件中为它创建一个匹配的嵌套路由组件。首先,由于它是/users 路由的一个所谓的嵌套路由(或子路由),我们可以将它嵌套在这个相应的父路由组件中。/users/1此外,由于它是一个所谓的动态路由,它使用一个定义为:userId 的动态路由,而用户的标识符是动态匹配的(例如,id 的用户将被匹配到'1' )。
const App = () => { const users = [ { id: '1', fullName: 'Robin Wieruch' }, { id: '2', fullName: 'Sarah Finnley' }, ];
return ( <Routes> <Route element={<Layout />}> <Route index element={<Home />} /> <Route path="home" element={<Home />} /> <Route path="users" element={<Users users={users} />}> <Route path=":userId" element={<User />} /> </Route> <Route path="*" element={<NoMatch />} /> </Route> </Routes> );};
以前,当我们引入父布局路由时,我们已经了解了嵌套路由,该路由的子路由是/home 和/users 。当我们做出这个改变时,我们不得不使用父路由中的Outlet组件来渲染匹配的子路由。同样的情况也发生在这里,因为Users组件也要渲染它的Nested Route。
const Users = ({ users }) => { return ( <> <h2>Users</h2>
<ul>...</ul>
<Outlet /> </> );};
接下来,我们要声明缺失的用户组件,只要有用户的标识符在URL中匹配,它就会通过Outlet嵌套到用户组件中。因此,我们可以使用React Router的useParams Hook来从URL中获取相应的userId (相当于:userId )。
import { ... useParams,} from 'react-router-dom';
...
const User = () => { const { userId } = useParams();
return ( <> <h2>User: {userId}</h2>
<Link to="/users">Back to Users</Link> </> );};
我们再次看到了如何通过将一个Route组件(或多个Route组件)嵌套在另一个Route组件中来创建嵌套路由。前者是嵌套的子路由,而后者是父路由,它渲染的包围组件必须利用Outlet组件来渲染实际匹配的子路由。
继续阅读。嵌套路由的详细介绍
我们还看到,我们如何通过在路由的path 道具中使用冒号来创建动态路由(例如::userId )。基本上,:userId 作为任何标识符的星号。在我们的例子中,我们使用一个链接组件将用户导航到一个/users/:userId 路线,其中:userId 代表实际用户的标识符。最后,我们总是可以通过使用React Router的useParams Hook从URL中获得动态路径(称为参数或params)。
React Router中的相对链接
最新版本的React Router带有所谓的相对链接。我们将通过查看Users组件和它的绝对/users/${user.id} 路径来研究这个概念,该路径被用于Link组件。在以前的React Router版本中,有必要指定整个路径。然而,在这个版本中,你可以只使用嵌套路径作为相对路径。
const Users = ({ users }) => { return ( <> <h2>Users</h2>
<ul> {users.map((user) => ( <li key={user.id}> <Link to={user.id}> {user.fullName} </Link> </li> ))} </ul> </> );};
由于Users组件被用于/users 路径,Users组件中的Link知道其当前位置,不需要创建整个绝对路径的顶层部分。相反,它知道/users ,并只是将:userId 作为相对路径附加到它上面。
声明性和程序性导航
到目前为止,我们只在使用Link或NavLink组件时使用声明式导航。然而,在某些情况下,你希望能够通过JavaScript以编程方式对用户进行导航。我们将通过实现一个可以在用户组件中删除一个用户的功能来展示这个场景。删除后,用户应该被从用户组件导航到用户组件(从/users/:userId 到/users )。
继续阅读。使用React Router的懒惰加载
我们将通过React的useState Hook创建一个有状态的users ,然后实现一个事件处理程序,通过使用标识符从users 删除一个用户。
import * as React from 'react';...
const App = () => { const [users, setUsers] = React.useState([ { id: '1', fullName: 'Robin Wieruch' }, { id: '2', fullName: 'Sarah Finnley' }, ]);
const handleRemoveUser = (userId) => { setUsers((state) => state.filter((user) => user.id !== userId)); };
return ( <Routes> <Route element={<Layout />}> <Route index element={<Home />} /> <Route path="home" element={<Home />} /> <Route path="users" element={<Users users={users} />}> <Route path=":userId" element={<User onRemoveUser={handleRemoveUser} />} /> </Route> <Route path="*" element={<NoMatch />} /> </Route> </Routes> );};
在我们把事件处理程序作为回调处理程序传递给User组件后,我们可以把它作为内联处理程序,通过标识符删除特定的用户。
const User = ({ onRemoveUser }) => { const { userId } = useParams();
return ( <> <h2>User: {userId}</h2>
<button type="button" onClick={() => onRemoveUser(userId)}> Remove </button>
<Link to="/users">Back to Users</Link> </> );};
一旦用户被删除,我们可以利用React Router的useNavigate Hook,它允许我们以编程方式将用户导航到另一个路由(这里:/users )。
import * as React from 'react';import { ... useNavigate,} from 'react-router-dom';
const App = () => { const navigate = useNavigate();
const [users, setUsers] = React.useState([ { id: '1', fullName: 'Robin Wieruch' }, { id: '2', fullName: 'Sarah Finnley' }, ]);
const handleRemoveUser = (userId) => { setUsers((state) => state.filter((user) => user.id !== userId));
navigate('/users'); };
return (...);};
在这种情况下,删除操作是同步进行的,因为用户在客户端只是一个有状态的值。然而,如果用户是数据库中的一个实体,你将不得不提出一个异步请求来删除它。一旦这个操作(读作:承诺)解决了,用户就会被导航到/users 路线。你可以通过在React中设置一个假的API而不使用实际的服务器来尝试这个场景。
React Router。搜索参数
浏览器中的URL不仅由路径组成(基本上是由像users 这样的段和像/ 这样的分隔符组成),还包括一个可选的查询字符串(在React Router中称为搜索参数),它以键/值对的形式出现在URL的? 分隔符之后。例如,/users?name=robin 将是一个有一个搜索参数对的URL,其中键是name ,值是robin 。下面的例子显示了它的实现。
import * as React from 'react';import { ... useSearchParams,} from 'react-router-dom';
...
const Users = ({ users }) => { const [searchParams, setSearchParams] = useSearchParams();
const searchTerm = searchParams.get('name') || '';
const handleSearch = (event) => { const name = event.target.value;
if (name) { setSearchParams({ name: event.target.value }); } else { setSearchParams({}); } };
return ( <> <h2>Users</h2>
<input type="text" value={searchTerm} onChange={handleSearch} />
<ul> {users .filter((user) => user.fullName .toLowerCase() .includes(searchTerm.toLocaleLowerCase()) ) .map((user) => ( <li key={user.id}> <Link to={user.id}>{user.fullName}</Link> </li> ))} </ul>
<Outlet /> </> );};
首先,我们使用React Router的useSearchParams Hook从URL中读取当前的搜索参数(见get() 方法上的searchParams ),同时也将搜索参数写入URL中(见setSearchParams() 函数)。虽然我们使用前者按键获取搜索参数(这里:'name' )以控制(读作:在)输入字段中显示,但我们使用后者在用户输入字段时按键在 URL 中设置搜索参数。就其核心而言,React Router的useSearchParams Hook与React的useState Hook相同,不同的是这个状态是一个URL状态,而不是React的本地状态。最后但并非最不重要的是,我们正在使用搜索参数来过滤实际的users 列表,以完成这一功能。
继续阅读。React Router搜索参数
毕竟,在你的URL中拥有搜索参数给你带来的好处是与他人分享更具体的URL。如果你在一个电子商务网站上有一个活跃的黑鞋搜索,你可能想分享整个URL(例如:myecommerce.com/shoes?color=black ),而不是只分享路径(例如:myecommerce.com/shoes )。前者为打开你的URL的人提供了过滤后的列表作为起点。
React Router是React中最常用的第三方库之一。它的核心功能是将Link组件映射到Route组件,使开发者能够实现客户端路由,而不需要向Web服务器发出请求。然而,除了这个核心功能之外,它还是一个完整的路由库,可以实现声明式嵌套路由、动态路由、导航、活动链接,以及编程式导航和通过URL搜索。