react-router-dom 源码阅读
这次的版本是 6.0.2
这里只讲 react-router-dom 提供的 API, 像是 Routes, Router 这些都是 react-router 提供的
源码阅读
BrowserRouter, HashRouter
BrowserRouter 和 hashRouter 的主要区别就在于使用的路由 API
简单解释
BrowserRouter
它使用了 history 库 的API,也就是说,浏览器(IE 9和更低版本以及同时代的浏览器)是不可用的。
客户端React应用程序能够维护干净的路由,如 example.com/react/route
,但需要得到Web服务器的支持。
这需要Web服务器应该被配置为单页应用程序,即为/react/route路径或服务器上的任何其他路由提供相同的index.html。
HashRouter
它使用URL哈希,对支持的浏览器或网络服务器没有限制, 如 example.com/#/react/route.
效果是所有后续的URL路径内容在服务器请求中被忽略(即你发送 "www.mywebsite.com/#/person/john",服务器得到 "www.mywebsite.com"。
因此,服务器将返回前#URL响应,然后后#路径将由你的客户端反应程序进行解析处理。
代码解析
先说 hashRouter , 他的依赖度是最低的, 代码也很简单:
import {createHashHistory} from "history"; function HashRouter({basename, children, window}: HashRouterProps) { let historyRef = React.useRef<HashHistory>(); // 用来存储 createHashHistory 结果 if (historyRef.current == null) { historyRef.current = createHashHistory({window}); } let history = historyRef.current; let [state, setState] = React.useState({ action: history.action, location: history.location }); React.useLayoutEffect(() => history.listen(setState), [history]); return ( <Router basename={basename} children={children} location={state.location} navigationType={state.action} navigator={history} /> ); }
这里需要了解的一个 API 是 createHashHistory
, 他来自于 history 仓库, 这里我们需要解析一下这个方法:
/** * 此方法里并不是全部的源码, 省略了部分不太 core 的代码 */ function createHashHistory( options: HashHistoryOptions = {} ): HashHistory { let {window = document.defaultView!} = options; // window 是传递的参数 let globalHistory = window.history; // 全局的 history 对象 // 获取当前 state.idx 和 location 对象 function getIndexAndLocation(): [number, Location] { let { pathname = '/', search = '', hash = '' } = parsePath(window.location.hash.substr(1)); // 解析 hash let state = globalHistory.state || {}; return [ state.idx, readOnly<Location>({ pathname, search, hash, state: state.usr || null, key: state.key || 'default' }) ]; } let blockedPopTx: Transition | null = null; // pop 的操作 最终调用的是 go() 函数 function handlePop() { // 省略 } // popstate 事件监听 window.addEventListener(PopStateEventType, handlePop); // hashchange 事件监听 ie11 中存在问题 window.addEventListener(HashChangeEventType, () => { let [, nextLocation] = getIndexAndLocation(); // 忽略外部的 hashchange 事件 createPath = pathname + search + hash if (createPath(nextLocation) !== createPath(location)) { handlePop(); } }); // Action 是一个 枚举, Pop = 'POP' let action = Action.Pop; let [index, location] = getIndexAndLocation(); /** * createEvents 方法 * 一个闭包方法, 维护一个数组,类似观察者模式, 返回 push, call 两个方法 */ let listeners = createEvents<Listener>(); let blockers = createEvents<Blocker>(); // 常用的 push 方法 function push(to: To, state?: any) { let nextAction = Action.Push; // 枚举 Action.Push = 'PUSH' let nextLocation = getNextLocation(to, state); // 生成一个新的 location 对象 function retry() { push(to, state); } // blockers 为空的时候 if (allowTx(nextAction, nextLocation, retry)) { // 根据 location 生成需要的对象, 只是数据格式更改了下 /* historyState = { usr: nextLocation.state, key: nextLocation.key, idx: index }*/ let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1); try { // 调用原生 API, history.pushState globalHistory.pushState(historyState, '', url); } catch (error) { // 不兼容就使用这个 window.location.assign(url); } applyTx(nextAction); // listeners 中添加回调 nextAction } } function replace(to: To, state?: any) { // 同 push, 只不过调用的原生改成了这个 globalHistory.replaceState(historyState, '', url); } function go(delta: number) { // 原生 go 方法 globalHistory.go(delta); } let history: HashHistory = { // 定义的局部 history 对象, 最后要返回的 get action() { return action; }, get location() { return location; }, createHref, push, replace, go, back() { go(-1); }, forward() { go(1); }, listen(listener) { return listeners.push(listener); }, block(blocker) { let unblock = blockers.push(blocker); if (blockers.length === 1) { window.addEventListener(BeforeUnloadEventType, promptBeforeUnload); } return function () { // 在页面 UnMount 的时候调用 unblock(); if (!blockers.length) { window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); } }; } }; return history; }
Link
一个经常用到的小组件, 常用来做跳转
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>( function LinkWithRef( { onClick, reloadDocument, replace = false, state, target, to, ...rest }, ref ) { // useHref 来自于 react-router 中, 用来 parse URL let href = useHref(to); // 真实点击跳转调用的函数, 具体源码在下面给出 let internalOnClick = useLinkClickHandler(to, { replace, state, target }); // 点击 a 标签的句柄, 如果有 onClick 事件 则优先 function handleClick( event: React.MouseEvent<HTMLAnchorElement, MouseEvent> ) { if (onClick) onClick(event); if (!event.defaultPrevented && !reloadDocument) { internalOnClick(event); } } return ( <a {...rest} href={href} onClick={handleClick} ref={ref} target={target} /> ); } );
useLinkClickHandler
来看看这个 hooks 的具体构成
export function useLinkClickHandler<E extends Element = HTMLAnchorElement>( to: To, { target, replace: replaceProp, state }: { target?: React.HTMLAttributeAnchorTarget; replace?: boolean; state?: any; } = {} ): (event: React.MouseEvent<E, MouseEvent>) => void { // 来源于 react-router, 获取 navigate 函数, 可以用来跳转 let navigate = useNavigate(); // 获取当前的 location 对象(非 window.location) let location = useLocation(); // 同样来源于react-router, 解析 to, 获取 path let path = useResolvedPath(to); return React.useCallback( (event: React.MouseEvent<E, MouseEvent>) => { if ( event.button === 0 && // 忽略除了左键点击以外的, 参考: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button (!target || target === "_self") && !isModifiedEvent(event) // 忽略各类键盘按钮, 参考 https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent // isModifiedEvent = !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); ) { event.preventDefault(); // 对比是否有变化 let replace = !!replaceProp || createPath(location) === createPath(path); navigate(to, { replace, state }); } }, [location, navigate, path, replaceProp, state, target, to] ); }
NavLink
等于是对 Link 组件的包装
const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>( function NavLinkWithRef( { "aria-current": ariaCurrentProp = "page", caseSensitive = false, className: classNameProp = "", end = false, style: styleProp, to, ...rest }, ref ) { // 这两个 hooks 上述已经说过 let location = useLocation(); let path = useResolvedPath(to); let locationPathname = location.pathname; let toPathname = path.pathname; if (!caseSensitive) { // 支持字符串大小写不敏感 locationPathname = locationPathname.toLowerCase(); toPathname = toPathname.toLowerCase(); } // 是否 active // /user => /user/name let isActive = locationPathname === toPathname || (!end && locationPathname.startsWith(toPathname) && locationPathname.charAt(toPathname.length) === "/"); // aria 是帮助残障人士辅助阅读的 let ariaCurrent = isActive ? ariaCurrentProp : undefined; // class 样式计算 let className: string; if (typeof classNameProp === "function") { className = classNameProp({ isActive }); } else { className = [classNameProp, isActive ? "active" : null] .filter(Boolean) .join(" "); } let style = typeof styleProp === "function" ? styleProp({ isActive }) : styleProp; return ( <Link {...rest} aria-current={ariaCurrent} className={className} ref={ref} style={style} to={to} /> ); } );
useSearchParams
用来获取/设置 query 的 hooks
export function useSearchParams(defaultInit?: URLSearchParamsInit) { // createSearchParams 的源码下面会讲, 大体是包装了 URLSearchParams // 相关知识点: https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit)); // 获取 location let location = useLocation(); // 解析, 通过对比 更新 let searchParams = React.useMemo(() => { let searchParams = createSearchParams(location.search); for (let key of defaultSearchParamsRef.current.keys()) { if (!searchParams.has(key)) { defaultSearchParamsRef.current.getAll(key).forEach(value => { searchParams.append(key, value); }); } } return searchParams; }, [location.search]); let navigate = useNavigate(); // 通过 navigate 方法 实现 location.search 的变更 let setSearchParams = React.useCallback( ( nextInit: URLSearchParamsInit, navigateOptions?: { replace?: boolean; state?: any } ) => { // URLSearchParams toString 就成了 query 格式 navigate("?" + createSearchParams(nextInit), navigateOptions); }, [navigate] ); return [searchParams, setSearchParams] as const; }
createSearchParams
function createSearchParams( init: URLSearchParamsInit = "" ): URLSearchParams { // 通过原生 api 创建, 数组的话 就类似于 tuple return new URLSearchParams( typeof init === "string" || Array.isArray(init) || init instanceof URLSearchParams ? init : Object.keys(init).reduce((memo, key) => { let value = init[key]; return memo.concat( Array.isArray(value) ? value.map(v => [key, v]) : [[key, value]] ); }, [] as ParamKeyValuePair[]) ); }
总结
react-router-dom 其实就像 react-router 的再度包装, 给开发提供了良好的基础设施
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧