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;
}

一个经常用到的小组件, 常用来做跳转

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]
);
}

等于是对 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 的再度包装, 给开发提供了良好的基础设施

引用

相关知识点汇总

posted @   Grewer  阅读(304)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· 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吧
点击右上角即可分享
微信分享提示