ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

React Router 客户端路由实战:为 React 应用构建多页面导航

React Router 客户端路由实战:为 React 应用构建多页面导航 React Router 客户端路由实战为 React 应用构建多页面导航【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum本文是 The Odin Project 开源全栈课程react_router.md的技术指南。它系统讲解如何在 React 应用中引入 React Router 实现客户端路由从安装接入、Link导航、嵌套路由与Outlet、动态段URL 参数、404 兜底页到受保护路由、Outlet上下文传值与路由组件测试。学完本文你将能独立为一个 SPA 项目搭建完整的多页面导航体系并正确编写、测试和部署带路由的 React 应用。客户端路由为什么需要 React Router课程进行到这里我们构建的都是单页应用SPA。但对于任何稍具规模的应用页面不止一个。幸运的是浏览器允许客户端 JavaScript 通过History API接管用户的导航行为而 React Router 正是把这一底层能力封装成 React 组件与 Hook 的成熟路由库。什么是客户端路由客户端路由Client-side routing是指由 JavaScript 承担应用路由处理职责的路由方式。它让 SPA 在用户导航时无需刷新页面当用户点击导航栏元素时URL 发生变化页面视图在客户端内被相应地替换。原文用一个烤鸡的比喻来说明传统多页应用MPA与客户端路由的差别MPA 像传统烤箱访问任何 URL设置烤箱→ 等待加载烹饪→ 页面可用开吃。如果发现问题需要改配方你得从座位上站起来——每次点击链接导航浏览器都会重新加载整个页面重新经历一遍完整的请求—响应流程。客户端路由像桌上的微波炉你永远不需要离开当前页面。链接请求被你所写的 JavaScript 拦截而不是直接发给服务器因此导航切换不再触发整页刷新。交互体验与可访问性代价客户端路由让应用拥有更接近原生 App 的交互体验——由于路由由你控制你可以跨路由切换制作流畅的 CSS 动画。但也要注意一个关键代价浏览器整页刷新时会自动通知屏幕阅读器朗读新内容而在客户端路由场景下你需要手动通知屏幕阅读器路由已经更新。所幸借助成熟的路由库这些问题通常可以被妥善解决。快速上手创建项目并接入路由让我们从一个最小应用开始理解 React Router 的接入方式。先创建两个模拟页面组件。创建Profile.jsxconst Profile () { return ( div h1Hello from profile page!/h1 pSo, how are you?/p /div ); }; export default Profile;同时把App.jsx替换为带导航的主页const App () { return ( div h1Hello from the main page of the app!/h1 pHere are some examples of links to other pages/p nav ul li a hrefprofileProfile page/a /li /ul /nav /div ); }; export default App;安装 React Router 并创建路由配置安装依赖本课程使用基于对象的路由配置方式即 data router 风格npm install react-router然后在入口文件main.jsx中创建浏览器路由并渲染import { StrictMode } from react; import { createRoot } from react-dom/client; import { createBrowserRouter, RouterProvider } from react-router; import App from ./App; import Profile from ./Profile; const router createBrowserRouter([ { path: /, element: App /, }, { path: profile, element: Profile /, }, ]); createRoot(document.getElementById(root)).render( StrictMode RouterProvider router{router} / /StrictMode );运行npm run dev分别访问首页/和/profile两条路由都能正常渲染。这段代码背后发生了什么从 React Router 导入createBrowserRouter和RouterProvidercreateBrowserRouter接收一个路由数组生成路由配置配置数组中的每个对象包含两个必填键path路径与对应的element要渲染的组件生成的路由配置被传给RouterProvider组件完成渲染。用 Link 替代 a 标签实现无刷新导航细心的读者会发现上一步中点击导航栏链接时浏览器仍然会为了跳转 URL 而整页重载——这显然不是我们想要的效果。React Router 为此导出了自定义的Link组件用它替代常规的a标签import { Link } from react-router; const App () { return ( div h1Hello from the main page of the app!/h1 pHere are some examples of links to other pages/p nav ul li Link toprofileProfile page/Link /li /ul /nav /div ); }; export default App;Link的核心区别在于它以to属性声明目标路径点击时由 React Router 拦截导航行为不再触发浏览器整页刷新从而让导航体验变得平滑。嵌套路由、Outlet 与动态段嵌套路由让子组件随父组件一起渲染如果希望根据不同的 URL 在页面某一区域渲染不同内容就需要嵌套路由。把子路由放进父路由的children数组中子组件就会和父组件一起被渲染。先创建两个组件Popeye.jsx与Spinach.jsximport { Link } from react-router; const Popeye () { return ( pHi, I am Popeye! I love to eat Spinach!/p Link to/Click here to go back/Link / ); }; export default Popeye;import { Link } from react-router; const Spinach () { return ( pHi, I am Spinach! Popeye loves to eat me!/p Link to/Click here to go back/Link / ); }; export default Spinach;重写路由配置import { StrictMode } from react import { createRoot } from react-dom/client; import { createBrowserRouter, RouterProvider } from react-router; import App from ./App; import Profile from ./Profile; import Spinach from ./Spinach; import Popeye from ./Popeye; const router createBrowserRouter([ { path: /, element: App /, }, { path: profile, element: Profile /, children: [ { path: spinach, element: Spinach / }, { path: popeye, element: Popeye / }, ], }, ]); createRoot(document.getElementById(root)).render( StrictMode RouterProvider router{router} / /StrictMode, );Outlet子路由的渲染插槽要让子组件真正出现在父组件页面中需要在父组件里放置Outlet组件。Outlet是一个占位插槽当访问对应子路径时它会被替换为匹配的子组件。重写Profile.jsximport { Outlet } from react-router; const Profile () { return ( div h1Hello from profile page!/h1 pSo, how are you?/p hr / h2The profile visited is here:/h2 Outlet / /div ); }; export default Profile;现在访问/profile页面并在 URL 末尾手动加上/popeye或/spinachOutlet /就会分别被替换为对应的子组件。索引路由子路由的默认内容如果希望在访问/profile且未附加任何子路径时渲染一个默认组件可以给 children 添加一条索引路由index: true。创建DefaultProfile.jsxconst DefaultProfile () { return pOh, nothing to see here!/p; }; export default DefaultProfile;把它作为/profile的子路由加入配置import { StrictMode } from react; import { createRoot } from react-dom/client; import { createBrowserRouter, RouterProvider } from react-router; import App from ./App; import Profile from ./Profile; import DefaultProfile from ./DefaultProfile; import Spinach from ./Spinach; import Popeye from ./Popeye; const router createBrowserRouter([ { path: /, element: App /, }, { path: profile, element: Profile /, children: [ { index: true, element: DefaultProfile / }, { path: spinach, element: Spinach / }, { path: popeye, element: Popeye / }, ], }, ]); createRoot(document.getElementById(root)).render( StrictMode RouterProvider router{router} / /StrictMode, );访问/profile你会看到Outlet位置渲染出了默认内容。动态段URL 参数与 useParams硬编码每一条子路由显然不够灵活。更多时候我们希望根据 URL 中的变化值动态渲染内容这就要用到动态段。把路径中的某一段写成:name形式import { StrictMode } from react; import { createRoot } from react-dom/client; import { createBrowserRouter, RouterProvider } from react-router; import App from ./App; import Profile from ./Profile; const router createBrowserRouter([ { path: /, element: App /, }, { path: profile/:name, element: Profile /, }, ]); createRoot(document.getElementById(root)).render( StrictMode RouterProvider router{router} / /StrictMode, );冒号:会把后面的路径段变成动态段它会匹配 URL 中该位置不断变化的值比如name这些值也被称为URL 参数URL params简称 params。通过useParamsHook 即可在组件中读取它们。重写Profile.jsximport { useParams } from react-router; import DefaultProfile from ./DefaultProfile; import Spinach from ./Spinach; import Popeye from ./Popeye; const Profile () { const { name } useParams(); return ( div h1Hello from profile page!/h1 pSo, how are you?/p hr / h2The profile visited is here:/h2 {name popeye ? ( Popeye / ) : name spinach ? ( Spinach / ) : ( DefaultProfile / )} /div ); }; export default Profile;useParams返回一个包含所有动态段键值对的对象这里解构出name再根据其值条件渲染不同组件——注意之前那条index: true索引路由此时已不再生效因为/profile本身不再匹配任何动态段未传参数时会走到兜底分支或报错。处理无效 URLerrorElement 与 404 页动态段方案下访问/profile不带任何名字已经没有实际意义——不指明是哪个 profile页面该显示谁呢此时应用会报错。为了让用户访问错误或未使用的路径时看到友好的默认页面可以在路由配置中传入errorElement。创建基础的页面不存在组件import { Link } from react-router; const ErrorPage () { return ( div h1Oh no, this route doesnt exist!/h1 Link to/ You can go back to the home page by clicking here, though! /Link /div ); }; export default ErrorPage;把errorElement加入配置然后访问/profile或任何未声明的路径验证效果import { StrictMode } from react; import { createRoot } from react-dom/client; import { createBrowserRouter, RouterProvider } from react-router; import App from ./App; import Profile from ./Profile; import ErrorPage from ./ErrorPage; const router createBrowserRouter([ { path: /, element: App /, errorElement: ErrorPage /, }, { path: profile/:name, element: Profile /, }, ]); createRoot(document.getElementById(root)).render( StrictMode RouterProvider router{router} / /StrictMode, );这样任何未匹配的 URL 都会渲染ErrorPage用户可以通过其中的Link返回首页而不会再看到裸奔的报错界面。重构把路由配置抽到独立文件路由配置继续膨胀之前把它抽到独立文件routes.jsx中可以让main.jsx更清爽import App from ./App; import Profile from ./Profile; import ErrorPage from ./ErrorPage; const routes [ { path: /, element: App /, errorElement: ErrorPage /, }, { path: profile/:name, element: Profile /, }, ]; export default routes;main.jsx只需导入并创建路由import { StrictMode } from react; import { createRoot } from react-dom/client; import { createBrowserRouter, RouterProvider } from react-router; import routes from ./routes; const router createBrowserRouter(routes); createRoot(document.getElementById(root)).render( StrictMode RouterProvider router{router} / /StrictMode );这个重构还有一个重要收益路由数组可以被测试文件直接导入测试中可以基于同一份配置创建测试专用的 router详见下文测试章节保证测试环境与生产环境的路由行为一致。通过 Outlet 传递状态context 与 useOutletContext嵌套路由时如果父组件持有数据比如 state需要传给Outlet渲染出的任意子组件就需要用到context。Outlet内置了一个contextprop可以向其中传入任意值——包括数组或对象。在任何由该Outlet渲染的组件包括孙组件内部都可以调用useOutletContext()Hook 取回传给contextprop 的值如果传入的是数组或对象还能直接解构使用。例如// 父组件把 state 通过 context 传给子组件 Outlet context{{ user, setUser }} / // 子组件中读取 const { user, setUser } useOutletContext();这与 React 自带的 Context APIcreateContext/useContext见本仓库课程 managing_state_with_context_api.md思路一脉相承但useOutletContext是 React Router 针对路由插槽场景提供的专用 Hook更适合把数据顺着路由层级向下传递的需求。在本课程的后续项目中如购物车项目这类跨页面共享状态的需求会很常见。受保护路由与编程式导航实际应用中经常需要判断某条路由该不该渲染。最典型的场景是认证根据用户是否登录来决定渲染哪些路由。若已登录展示用户信息否则重定向到登录页。实现方式有很多种其中最简单的一种是根据条件动态构建路由配置——在调用createBrowserRouter之前用if判断或三元表达式把需要保护的路由有条件地加入路由数组或替换element从而实现登录才渲染的效果。与此同时你还会经常需要编程式地把用户重定向到另一个 URL例如登录成功后跳回原页面。这时可以使用useNavigateHook它既能导航到任意 URL也能在用户历史记录中前进或后退对应navigate(-1)等用法。它与Link的分工是Link面向声明式的用户点击useNavigate面向代码逻辑中的命令式跳转。测试使用 React Router 的组件测试这类组件时有个关键前提要牢记你的应用从不会直接渲染这些组件而是通过 router例如RouterProvider来渲染它们。因此测试中渲染这些组件时也必须把它们放进路由上下文里否则useNavigate、useParams这类 Hook 或Link组件会直接抛错。具体选择哪种方式取决于被测组件的依赖程度仅包含Link之类的轻量组件不测试导航、不依赖其他路由特性用轻量的MemoryRouter包裹即可。MemoryRouter把路由状态保存在内存中无需浏览器地址栏非常适合测试环境。依赖路由行为的组件如使用 outlet context、参数匹配、errorElement或重定向更合理的做法是像应用本身那样渲染一个RouterProvider。由于测试运行在 Node/jsdom 环境中而非浏览器可以使用createMemoryRouter并直接复用从routes.jsx导出的同一份路由配置。这与本仓库 React 测试课程introduction_to_react_testing.md的实践一致测试依赖jsdom在内存中模拟 DOM配合testing-library/react的render/screen查询与testing-library/user-event模拟用户交互。值得注意的是课程作业特别提醒不要直接去测试react-router本身它是外部库其维护者已经充分测试过了你只需在正确的路由上下文里测试自己的组件。部署提醒SPA 路由与服务器重写客户端路由还有一个常被忽略的实战细节——部署时的服务器配置。SPA 的所有路由在客户端解析但服务器上并不存在/profile/popeye这样的真实文件。如果用户直接访问或刷新该 URL服务器会返回 404除非你把所有请求重写回index.html让 React Router 接管后续解析。本仓库的购物车项目project_shopping_cart.md给出了各主流平台的配置示例Netlify在项目public/目录添加_redirects文件内容为/* /index.html 200将所有路由重定向到首页Vercel在项目根目录添加vercel.json通过 rewrites 把所有路径指向/index.html{ rewrites: [ { source: /(.*), destination: /index.html } ] }Cloudflare Pages截至撰写时无需额外配置默认行为即可让react-router正确处理 SPA 重定向。总结与知识检查到这里你已经掌握了用 React Router 构建 React 多页面应用的核心基础客户端路由的原理、createBrowserRouterRouterProvider的接入方式、Link无刷新导航、嵌套路由与Outlet、索引路由、动态段与useParams、errorElement兜底、useOutletContext传值、受保护路由与useNavigate以及如何在测试与部署场景下正确处理路由。React Router 还有大量超出本课范围的高级特性如 history/match 对象等但掌握以上基础足以支撑你完成整个 React 课程的学习。建议的巩固练习为上文构建的应用新增几条路由动手实践如果觉得本课信息量较大可以删掉重写一遍加深印象同时浏览 React Router 官方文档中本课涉及的概念并留意其他实用特性作为日后查阅的参考。最后用以下知识检查问题自测回答不出的部分可回到对应小节复习客户端路由意味着什么见客户端路由一节如何搭建一个基础路由见快速上手一节为了启用客户端路由应该用什么替代a标签见Link 替代 a 标签一节如何创建嵌套路由见嵌套路由一节动态段或 URL 参数是什么意思见动态段与 useParams一节如何处理无效 URL 导致的错误见errorElement 与 404 页一节如何通过Outlet /从父组件向子组件传数据见Outlet 传递状态一节如何创建受保护路由见受保护路由一节如何测试使用 React Router 的组件见路由组件测试一节【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表