网站首页 > 基础教程 正文
作者:DYBOY
在 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 快速创建路由组件,不再像之前那些写大量的组件,这里直接做了官方的封装和“路由配置”的定义:
BrowserRouter
BrowserRouter 确定了是 Web 运行环境,然后利用工具方法 createBrowserHistory 创建了对 Window.history API 的自定义封装实例。
同时向自定义 history 实例上注册监听器,当路由发生变化时,会回调执行 setState 方法更新 action 和 location 信息,然后触发组件的更新和重新渲染。
Router
Router 是一个提供 Location 和 Navigation 的 Context 组件,不会参与实际的 DOM 渲染,只是存储相关路由的规格化数据。
useRoutes
以前我们总要写大段的配置,以及自行编写路由组件,各个业务甚至都定义了自己的路由配置(树状结构),这种通用化的代码实际是可以做统一封装。
useRoutes 功能上等同于 <Routes>,但它使用 JS 对象而不是 <Route> 元素来定义路由,useRoutes 的返回值是可用于呈现路由树的有效 React 元素,或因无匹配路由返回 null 。
路由配置
因此 react-router-dom 参考相关 issue 定义了 RouteObject 类型:
/**
* A route object represents a logical route, with (optionally) its child
* routes organized in a tree-like structure.
*/
export interface RouteObject {
caseSensitive?: boolean; // 大小写敏感
children?: RouteObject[]; // 嵌套路由
element?: React.ReactNode; // 组件 or 页面
index?: boolean; // 是否作为 outlet 的默认索引/渲染
path?: string; // 匹配路径
}
路由 Context
export interface RouteMatch<ParamKey extends string = string> {
/** URL 上的 query 参数 Key => value */
params: Params<ParamKey>;
pathname: string;
pathnameBase: string;
/** 用于匹配的路由对象 */
route: RouteObject;
}
interface RouteContextObject {
outlet: React.ReactElement | null;
matches: RouteMatch[];
}
// 路由 Context
export const RouteContext = React.createContext<RouteContextObject>({
outlet: null,
matches: [],
});
路由匹配
借助 React Hooks 定义了 useRoutes 方法,功能上等同于 <Routes> 组件,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 组件中使用了 <Outlet /> 组件,useRoutes 的执行过程如下:
第一阶段:获取 pathname
第二阶段:获取匹配的路由 & 组件
第三阶段:渲染
其他常用 Hooks
useLocation
这个 Hooks 比较简单,从 LocationContext 中获取 location 对象:
因此可以通过该 Hooks 感知 location 的变化。
useNavigate
useNavigate() Hooks 会返回如下两种函数调用方式:
interface NavigateOptions {
replace?: boolean;
state?: any;
}
interface NavigateFunction {
(to: To, options?: NavigateOptions): void;
(delta: number): void;
}
function useNavigate(): NavigateFunction
第一种是跳转指定路由,第二个参数可以设置 replace(是否使用 history.replace) 和 state(状态数据);
第二种是如果第一个参数是数字,等同于 window.history.go()[4] 方法。
useNavigate() 的实现主要是从 NavigationContext、RouteContext 以及 LocationContext 中获取相关路由数据、Location 和 navigator 实例,然后根据不同的入参调用相应的执行跳转逻辑。
useParams
useParams Hooks 从当前 URL 返回与 <Route path> 匹配的动态参数的键/值对对象。 子路由继承父路由的所有参数。
也就是说从 path 路径中按照规则获取对应的 Key/Value。
useOutlet
该 Hooks 通过 RouteContext 获取当前路由下的 outlet,如果存在则返回由 OutletContext 包裹的子路由 React 组件。
其他常用组件
Link
类比网页中的 <a href="xxx" /> 标签。
其实现如下:
有个疑惑是,不知道 reloadDocument 这个参数的实际作用,顾名思义的角度就是是否重载文档。
但是从 <Link /> 组件内 handleClick() 方法的实现上看,其似乎只是一个是否调用默认 click 事件的开关,不过实际生产的时候,倒是没怎么用到。
NavLink
<NavLink /> 组件(导航链接)用于导航栏,例如管理后台的顶部菜单,或者是左侧的菜单。
其内部主要是对 className 和 style 两个属性做了注入,如果传递的是函数,则会注入 isActive 变量,用于确定当前路由是否激活。当匹配到的路由激活时,默认是 className 会拼接 active 类名。
Navigate
<Navigate /> 组件功能是“路由跳转”,可以理解为,当渲染该组件时,则立即跳转到指定路由。
其内部实现依赖 useNavigate() Hooks,换句话说,这个组件只是跳转事件的一个 JSX 封装形式。
Outlet
<Outlet /> 组件用于嵌套路由场景,在父路由元素(组件)中使用 <Outlet /> 来显式表明它们的子路由元素的渲染位置,在子路由匹配时显示嵌套 UI。
父路由使用精准匹配的情况下,但子路由没有显式声明索引的话(RouteObject.index),将不会渲染任何内容。
/**
* Renders the child route's element, if there is one.
*
* @see https://reactrouter.com/docs/en/v6/api#outlet
*/
export function Outlet(props: OutletProps): React.ReactElement | null {
return useOutlet(props.context);
}
<Outlet /> 组件的实现是基于 useOutlet() Hooks。
Routes
Routes 组件内的实现还是使用了 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
关注「字节前端 ByteFE」公众号,追更不迷路!
猜你喜欢
- 2024-12-16 达·芬奇名画离奇失踪,凡·高自杀手枪将拍卖
- 2024-12-16 Posts - 7, Articles - 0, Comments - 2
- 2024-12-16 史上最全 vue-router 讲解 !!! vue $route $router
- 2024-12-16 一文让你彻底搞懂 vue-Router vue-router的方法
- 2024-12-16 直接拿来用!盘点一些拯救头发的 JS 单行代码,网友:摸鱼必备啊
- 2024-12-16 「源码解析 」这一次彻底弄懂react-router路由原理
- 2024-12-16 web前端入门到实战:js刷新当前页面的5种方式
- 2024-12-16 面试被问及 hash 与 history 的区别该怎么回答?
- 2024-12-16 从模特到摄影师,他是我的传奇恋人
- 2024-12-16 路由的两种模式:hash模式和 history模式
- 最近发表
- 标签列表
-
- gitpush (61)
- pythonif (68)
- location.href (57)
- tail-f (57)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- css3动画 (57)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- canvasfilltext (58)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- node教程 (59)
- console.table (62)
- c++time_t (58)
- phpcookie (58)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)