找回密码
 会员注册
查看: 18|回复: 0

通过ReactRouterV6源码,掌握前端路由

[复制链接]

16

主题

0

回帖

49

积分

新手上路

积分
49
发表于 2024-10-2 17:01:40 | 显示全部楼层 |阅读模式
在 React 前端项目中,涉及到前端路由,想必大家都用过了 react-router-dom[1] 这个包,因为常用,所以有必要弄清楚其中的实现细节,对前端路由会有一个更深入的认识,另外也有助于提升工作效率。此文不赘述使用方法,相关内容可以参考tutorial 官方的指导手册[2]。客户端里的路由模式相较于“服务端路由”每次从服务端获取 CSS、JS、HTML 资源,客户端路由即是在客户端内自行控制,与服务端解耦,页面数据异步获取,浏览器无刷新切换页面,能为用户提供更快的页面切换体验,同时也为前端 SPA 应用发展提供了基础。在浏览器 Web 环境里有 “Hash” 和 “History” 两种客户端路由模式。Hash 模式Hash 模式点击会跳转定位到指定 DOM 位置,同时触发 hashchange 事件,支持在浏览器中操作前进后退,其本质还是在同一文档中操作,Server 端无感知前端路由变化。Hash 值在 window.location.hash 中存储,因此 Hash 变化时,同时可看到浏览器的 window.location.pathname 不变。History 模式在 Window 对象中提供了 history 实例,同时可以通过 history 暴露的 API 操作路由历史堆栈。也就是说,我们可以通过控制 history 对象来控制页面的路由跳转,浏览器不会刷新,但浏览器里的 URL 会变更,SEO 更友好。History 的 API 具体用法可参阅:History API - MDN[3]React Router v6 的架构设计react-router-dom 是一个封装浏览器客户端路由方案的优质工具模块,基于 React 的应用开发者,可借助其快速开发实现“客户端路由”,同时提升用户体验。react-router-dom 作为一款优秀的前端模块,更新到了 V6 版本,全面拥抱 React hooks 功能设计,通过阅读其源码,了解其设计思想,相信可以给大家在 路由设计 和 Hooks 实践上带来一些收获。文件结构在项目管理上采用了基于 Yarn 的 Monorepo 方案:项目设计react-router-dom 是浏览器环境中的桥接层,react-router-native 则是 Hybrid 开发的桥接层,其核心实现都在 react-router 模块中,层层递进。此外,react-router-dom-v5-compat 是用于 react-router-dom v5 版本兼容迁移到 v6 版本的处理方案,但个人更建议是直接使用/切换到 v6 版本,直接冲 !因此项目设计可以简单分为两层:架构设计因为我们常用 History 模式的前端路由,也就是 BrowserRouter,与此同时,可以理解为 HashRouter 只是调用的 Browser API 不一样,因此下面仅分析了 BrowserRouter 模式下的架构和设计。从 react-router-dom@6.4 版本开始支持数据 API,即根据路规则预先获取网络数据,数据预加载和路由做了绑定。虽然该功能是可选,但个人感觉大部分业务应该还是会自行在页面内控制,或者采用自有的一套灵活的预加载方案,目前无法定量评估方案好坏,因此,我们阅读的源码版本为 react-router-dom@6.3.0。react-router-dom 整体的功能架构设计如下图:虽然还有 StaticRouter、MemoryRouter、NativeRouter,但是掌握了 BrowserRouter,其它的应当也很容易理解。核心实现 & 组件react-router-dom 的实践案例要使用 react-router-dom,如下例举了一个简单的实践案例。顶层组件使用 BrowserRouter 包裹:借助 useRoutes Hooks 快速创建路由组件,不再像之前那些写大量的组件,这里直接做了官方的封装和“路由配置”的定义:BrowserRouterBrowserRouter 确定了是 Web 运行环境,然后利用工具方法 createBrowserHistory 创建了对 Window.history API 的自定义封装实例。同时向自定义 history 实例上注册监听器,当路由发生变化时,会回调执行 setState 方法更新 action 和 location 信息,然后触发组件的更新和重新渲染。RouterRouter 是一个提供 Location 和 Navigation 的 Context 组件,不会参与实际的 DOM 渲染,只是存储相关路由的规格化数据。useRoutes以前我们总要写大段的配置,以及自行编写路由组件,各个业务甚至都定义了自己的路由配置(树状结构),这种通用化的代码实际是可以做统一封装。useRoutes 功能上等同于 ,但它使用 JS 对象而不是 元素来定义路由,useRoutes 的返回值是可用于呈现路由树的有效 React 元素,或因无匹配路由返回 null 。路由配置因此 react-router-dom 参考相关 issue 定义了 RouteObject 类型:/***Arouteobjectrepresentsalogicalroute,with(optionally)itschild*routesorganizedinatree-likestructure.*/exportinterfaceRouteObject{caseSensitive:boolean;//大小写敏感children:RouteObject[];//嵌套路由element:React.ReactNode;//组件or页面index:boolean;//是否作为outlet的默认索引/渲染path:string;//匹配路径}路由 ContextexportinterfaceRouteMatch {/**URL上的query参数Key=>value*/paramsarams ;pathname:string;pathnameBase:string;/**用于匹配的路由对象*/route:RouteObject;}interfaceRouteContextObject{outlet:React.ReactElement|null;matches:RouteMatch[];}//路由ContextexportconstRouteContext=React.createContext({outlet:null,matches:[],});路由匹配借助 React Hooks 定义了 useRoutes 方法,功能上等同于 组件,useRoutes 能够依据“路由配置对象”和当前路由做匹配,然后按匹配规则渲染对应的“组件”。该 hooks 文件位置:packages/react-router/lib/hooks.tsx其中 matchRoutes() 函数返回一个对象数组,每个匹配的路由对应一个对象,是 React Router 的 核心算法 函数,不难理解。渲染_renderMatches() 函数将 matchRoutes() 的结果渲染为 React 元素:这个函数为每个匹配组路由组(嵌套路由)建立 RouteContext,children 即为需要渲染的 React 元素。其中比较巧妙的设计是利用 reduceRight() 方法,从右往左开始遍历,也就是从子到父的嵌套路由顺序,将前一项的 React 节点作为下一个 React 节点的 outlet。其中 outlet 是一个非常核心的概念,其用于嵌套路由场景,outlet 的渲染实现方式可参考下文中的 useOutlet() Hooks。举例一个嵌套路由配置如图:在 HomePage 组件中使用了 组件,useRoutes 的执行过程如下:第一阶段:获取 pathname第二阶段:获取匹配的路由 & 组件第三阶段:渲染其他常用 HooksuseLocation这个 Hooks 比较简单,从 LocationContext 中获取 location 对象:因此可以通过该 Hooks 感知 location 的变化。useNavigateuseNavigate() Hooks 会返回如下两种函数调用方式:interfaceNavigateOptions{replace:boolean;state:any;}interfaceNavigateFunction{(to:To,options:NavigateOptions):void;(delta:number):void;}functionuseNavigate():NavigateFunction第一种是跳转指定路由,第二个参数可以设置 replace(是否使用 history.replace) 和 state(状态数据);第二种是如果第一个参数是数字,等同于 window.history.go()[4] 方法。useNavigate() 的实现主要是从 NavigationContext、RouteContext 以及 LocationContext 中获取相关路由数据、Location 和 navigator 实例,然后根据不同的入参调用相应的执行跳转逻辑。useParamsuseParams Hooks 从当前 URL 返回与 匹配的动态参数的键/值对对象。子路由继承父路由的所有参数。也就是说从 path 路径中按照规则获取对应的 Key/Value。useOutlet该 Hooks 通过 RouteContext 获取当前路由下的 outlet,如果存在则返回由 OutletContext 包裹的子路由 React 组件。其他常用组件Link类比网页中的 标签。其实现如下:有个疑惑是,不知道 reloadDocument 这个参数的实际作用,顾名思义的角度就是是否重载文档。但是从 组件内 handleClick() 方法的实现上看,其似乎只是一个是否调用默认 click 事件的开关,不过实际生产的时候,倒是没怎么用到。NavLink 组件(导航链接)用于导航栏,例如管理后台的顶部菜单,或者是左侧的菜单。其内部主要是对 className 和 style 两个属性做了注入,如果传递的是函数,则会注入 isActive 变量,用于确定当前路由是否激活。当匹配到的路由激活时,默认是 className 会拼接 active 类名。Navigate 组件功能是“路由跳转”,可以理解为,当渲染该组件时,则立即跳转到指定路由。其内部实现依赖 useNavigate() Hooks,换句话说,这个组件只是跳转事件的一个 JSX 封装形式。Outlet 组件用于嵌套路由场景,在父路由元素(组件)中使用 来显式表明它们的子路由元素的渲染位置,在子路由匹配时显示嵌套 UI。父路由使用精准匹配的情况下,但子路由没有显式声明索引的话(RouteObject.index),将不会渲染任何内容。/***Rendersthechildroute'selement,ifthereisone.**@seehttps://reactrouter.com/docs/en/v6/api#outlet*/exportfunctionOutlet(props:OutletProps):React.ReactElement|null{returnuseOutlet(props.context);} 组件的实现是基于 useOutlet() Hooks。RoutesRoutes 组件内的实现还是使用了 useRoutes() Hooks,因此在生产实践中还是推荐大家用“配置化路由”方式,来实现渲染路由组件,能提升路由的可维护性。总结React Router 目前更新到了 6.6.x 版本,其中的数据预加载和路由绑定方案,确实也是一个不错的方案,但在实际生产过程中,想要快速实现“大一统”也确实会遇到各种问题,因此大家还是需要辩证看待,按需取舍。此外 @remix-run/router 这个模块是对 History 和 Navigator 的封装,部分实现细节也是值得借鉴。阅读源码可能确实比较枯燥,但是如果能够潜心阅读,仔细推敲每一个让你疑惑的问题点,并学习其精妙的设计与实现,相信能够对我们的编码技能有一定的促进作用。点击上方关注 · 我们下期再见参考资料[1]react-router-dom: https://www.npmjs.com/package/react-router-dom[2]tutorial 官方的指导手册: https://reactrouter.com/en/main/start/tutorial[3]History API - MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/History_API[4]window.history.go(): https://developer.mozilla.org/en-US/docs/Web/API/History/go[5]React Router Docs: https://reactrouter.com/en/main
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-12 08:47 , Processed in 0.505659 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表