React Router
以下源码参考自 react-router v6.3.0
项目结构
react-router-main
├─ docs
├─ examples
├─ packages
│ ├─ react-router # v6.3.0
│ ├─ react-router-dom # v6.3.0
│ └─ react-router-native
├─ scripts
│ ├─ publish.js
│ └─ version.js
├─ static
├─ tutorial
├─ contributors.yml
├─ package.json
├─ prettier.config.js
├─ rollup.config.js
├─ tsconfig.json
└─ yarn.lock
这是一个 monorepo
项目,主要分为 react-router
(核心库)、react-router-dom
(浏览器路由库)、react-router-native
(react-native 路由库) 三个项目
monorepo 的好处主要是便于管理多仓库,包括版本控制、独立维护等
咱们主要看浏览器路由库的实现
使用
v6 相比 v5 改动蛮大的,使用 Routes
组件替换 Switch
组件,移除了 Redirect
组件,还有一些属性的更改,以及加强了路径匹配,但路由原理不变
基础配置:
import { BrowserRouter, Route, Routes } from "react-router-dom";
// ...
<BrowserRouter>
<Routes>
<Route path="/home" element={<Home/>} />
<Route path="/login" element={<Login/>} />
// 重定向
<Route path="/" element={<Navigate to="/home"/>}>
</Routes>
</BrowserRouter>
路由跳转:
// NavLink
<NavLink to="/home">首页</NavLink>
// useNavigate
import { useNavigate } from "react-router-dom";
let navigate = useNavigate();
navigate(`/home`);
// 返回上一页
navigate(-1);
// 对象方式跳转
navigate({
pathname: "/home",
});
路由传参:
// search 传参
let navigate = useNavigate();
navigate(`/home?page=1&size=10`);
// ...
const [searchParams,setSearchParams] = useSearchParams()
searchParams.get('page'); // 1
searchParams.get('size'); // 10
// 动态路由传参
<Route path="/article" element={<Article />}>
<Route path=":/id" element={<ArticleDetail />} />
</Route>
// ...
const {id} = useParams()
// state传参
<NavLink to="/home" state={{ id: 1 }}>首页</NavLink>
navigate('/home',{state:{ id: 1 }})
// ...
const { state } = useLocation();
console.log(state.id) // 1
history
监听 url 变化的功能是由 history
这个库实现的,它的原理是基于浏览器的 history API
管理历史浏览记录,通过监听 popstate
或 hashchange
,抹平 history 路由和 hash 路由的差异,统一路由的对象和操作函数
history 其实不论在哪种模式下,都会优先使用 history API 来模拟路由跳转,因为 hash 改变也会触发 popstate 事件。hashchange 只是作为 popstate 的一种降级方案
要点:
- 提供 3 种类型的 history:browserHistory,hashHistory,memoryHistory,并保持统一的 api
- 修改地址栏 url
- 支持发布/订阅功能(listen),监听 url 修改事件(transitionManager)
- 提供跳转拦截(block)、跳转确认(prompt)和 basename 等实用功能
React-Router
Router
分为 BrowserRouter
和 HashRouter
,这两个组件在 react-router-dom
中,但它们都基于 react-router
的 Router
组件
BrowserRouter:
export function BrowserRouter({
basename,
children,
window,
}: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
// 创建一个 BrowserHistory 对象
historyRef.current = createBrowserHistory({ window });
}
let history = historyRef.current;
// history 对象变化会触发 Router 组件重渲染,以此达到切换路由页面的效果
let [state, setState] = React.useState({
action: history.action,
location: history.location,
});
// history 对象发生变化会触发回调
React.useLayoutEffect(() => history.listen(setState), [history]);
return (
<Router
basename={basename}
children={children}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
}
HashRouter 组件类似,只是将 createBrowserHistory
改为 createHashHistory
Router:
export function Router({
basename: basenameProp = "/",
children = null,
location: locationProp,
navigationType = NavigationType.Pop,
navigator,
static: staticProp = false,
}: RouterProps): React.ReactElement | null {
let basename = normalizePathname(basenameProp);
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
if (typeof locationProp === "string") {
locationProp = parsePath(locationProp);
}
let {
pathname = "/",
search = "",
hash = "",
state = null,
key = "default",
} = locationProp;
let location = React.useMemo(() => {
let trailingPathname = stripBasename(pathname, basename);
if (trailingPathname == null) {
return null;
}
return {
pathname: trailingPathname,
search,
hash,
state,
key,
};
}, [basename, pathname, search, hash, state, key]);
if (location == null) {
return null;
}
return (
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
);
}
Route
这个组件只是挂载下真实的路由组件和路径信息等
export function Route(
_props: PathRouteProps | LayoutRouteProps | IndexRouteProps
): React.ReactElement | null {
invariant(
false,
`A <Route> is only ever to be used as the child of <Routes> element, ` +
`never rendered directly. Please wrap your <Route> in a <Routes>.`
);
}
Routes
Routes
组件中,createRoutesFromChildren
会遍历 Route
组件,形成一个 routes 列表,然后 useRoutes
统一处理路由匹配逻辑
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
return useRoutes(createRoutesFromChildren(children), location);
}
NavLink
Link
组件加强版,有一些新增属性和功能,主要看下 Link
组件的实现
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
function LinkWithRef(
{ onClick, reloadDocument, replace = false, state, target, to, ...rest },
ref
) {
let href = useHref(to);
// useLinkClickHandler 会处理路由跳转逻辑
let internalOnClick = useLinkClickHandler(to, { replace, state, target });
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}
/>
);
}
);
useNavigate
这是个路由跳转的 Hook,分为 replace
和 push
两种方式,通过参数区分后,调用对象的路由操作方法
export function useNavigate(): NavigateFunction {
let { basename, navigator } = React.useContext(NavigationContext);
let { matches } = React.useContext(RouteContext);
let { pathname: locationPathname } = useLocation();
let routePathnamesJson = JSON.stringify(
matches.map((match) => match.pathnameBase)
);
// 活动路由锁
let activeRef = React.useRef(false);
React.useEffect(() => {
activeRef.current = true;
});
let navigate: NavigateFunction = React.useCallback(
(to: To | number, options: NavigateOptions = {}) => {
if (!activeRef.current) return;
if (typeof to === "number") {
navigator.go(to);
return;
}
let path = resolveTo(
to,
JSON.parse(routePathnamesJson),
locationPathname
);
if (basename !== "/") {
path.pathname = joinPaths([basename, path.pathname]);
}
(!!options.replace ? navigator.replace : navigator.push)(
path,
options.state
);
},
// 这里的 navigator 其实就是路由对象(history)
[basename, navigator, routePathnamesJson, locationPathname]
);
return navigate;
}
useLocation
export function useLocation(): Location {
return React.useContext(LocationContext).location;
}
示例
以基础的路由跳转为例
const navigate = useNavigate();
navigate("/counter", {
state: {
name: "lucas",
},
});
先进入 useNavigate
最后会执行 navigator.push(path, options.state),navigator 其实就是 history 对象
function push(path, state) {
const action = "PUSH";
const location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
(ok) => {
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
globalHistory.pushState({ key, state }, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
const prevIndex = allKeys.indexOf(history.location.key);
const nextKeys = allKeys.slice(
0,
prevIndex === -1 ? 0 : prevIndex + 1
);
nextKeys.push(location.key);
allKeys = nextKeys;
setState({ action, location });
}
} else {
// 降级措施是直接跳转
window.location.href = href;
}
}
);
}
setState 做了啥?
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
// 遍历执行每个监听器上注册的事件
transitionManager.notifyListeners(history.location, history.action);
}
createTransitionManager 源码参考
const createTransitionManager = () => {
let prompt = null;
const setPrompt = (nextPrompt) => {
prompt = nextPrompt;
return () => {
if (prompt === nextPrompt) prompt = null;
};
};
const confirmTransitionTo = (
location,
action,
getUserConfirmation,
callback
) => {
if (prompt != null) {
const result =
typeof prompt === "function" ? prompt(location, action) : prompt;
if (typeof result === "string") {
if (typeof getUserConfirmation === "function") {
getUserConfirmation(result, callback);
} else {
callback(true);
}
} else {
// 取消事务
callback(result !== false);
}
} else {
callback(true);
}
};
let listeners = [];
const appendListener = (fn) => {
let isActive = true;
const listener = (...args) => {
if (isActive) fn(...args);
};
listeners.push(listener);
return () => {
isActive = false;
// 防止重复事件
listeners = listeners.filter((item) => item !== listener);
};
};
// 通知监听器
const notifyListeners = (...args) => {
listeners.forEach((listener) => listener(...args));
};
return {
setPrompt,
confirmTransitionTo,
appendListener,
notifyListeners,
};
};
export default createTransitionManager;
路由传参
- history 借助 pushState 可以传参,参考 https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
- hash 基于 url 传参,有体积限制