React Router是React中最常用的路由库,它提供了一种灵活且强大的方式来处理单页面应用的路由。
本文主要解决几个问题:
- React Router的源码解析
- React Router的源码结构
- React Router的核心代码解析
- React Router的工作原理
- React Router的实现
- 创建一个简单的路由组件
- 实现路由的匹配和切换
- 实现路由的嵌套
再进一步~~(这里不实现)~~
基本使用
下面是官方文档的使用案例 github.com/remix-run/r…
import { Routes, Route, Outlet, Link } from "react-router-dom";
export default function App() {
return (
<div>
<h1>Basic Example</h1>
{/* Routes nest inside one another. Nested route paths build upon
parent route paths, and nested route elements render inside
parent route elements. See the note about <Outlet> below. */}
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="dashboard" element={<Dashboard />} />
{/* Using path="*"" means "match anything", so this route
acts like a catch-all for URLs that we don't have explicit
routes for. */}
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
</div>
);
}
function Layout() {
return (
<div>
{/* A "layout route" is a good place to put markup you want to
share across all the pages on your site, like navigation. */}
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
<li>
<Link to="/nothing-here">Nothing Here</Link>
</li>
</ul>
</nav>
<hr />
{/* An <Outlet> renders whatever child route is currently active,
so you can think about this <Outlet> as a placeholder for
the child routes we defined above. */}
<Outlet />
</div>
);
}
function Home() {
return (
<div>
<h2>Home</h2>
</div>
);
}
function About() {
return (
<div>
<h2>About</h2>
</div>
);
}
function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
</div>
);
}
function NoMatch() {
return (
<div>
<h2>Nothing to see here!</h2>
<p>
<Link to="/">Go to the home page</Link>
</p>
</div>
);
}
源码分析
其中包含了以下几个组件:
- Routes
- Route
- Outlet
- Link
Routes
- 匹配对应的地址
- 渲染匹配的元素
- 将子元素的参数拿出来,组成Route配置
/**
* A container for a nested tree of <Route> elements that renders the branch
* that best matches the current location.
*
* @see https://reactrouter.com/components/routes
*/
// 用来嵌套和匹配当前的组件
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
// 通过这个函数来实现对应渲染的组件
return useRoutes(createRoutesFromChildren(children), location);
}
// createRoutesFromChildren
export function createRoutesFromChildren(
children: React.ReactNode,
parentPath: number[] = []
): RouteObject[] {
let routes: RouteObject[] = [];
// 省略....
let route: RouteObject = {
id: element.props.id || treePath.join("-"),
caseSensitive: element.props.caseSensitive,
element: element.props.element,
Component: element.props.Component,
index: element.props.index,
path: element.props.path,
loader: element.props.loader,
action: element.props.action,
errorElement: element.props.errorElement,
ErrorBoundary: element.props.ErrorBoundary,
hasErrorBoundary:
element.props.ErrorBoundary != null ||
element.props.errorElement != null,
shouldRevalidate: element.props.shouldRevalidate,
handle: element.props.handle,
lazy: element.props.lazy,
};
if (element.props.children) {
route.children = createRoutesFromChildren(
element.props.children,
treePath
);
}
routes.push(route);
});
return routes;
}
// useRoutes
export function useRoutes(
routes: RouteObject[],
locationArg?: Partial<Location> | string
): React.ReactElement | null {
return useRoutesImpl(routes, locationArg);
}
export function useRoutesImpl(
routes: RouteObject[],
locationArg?: Partial<Location> | string,
dataRouterState?: RemixRouter["state"]
): React.ReactElement | null {
// 省略代码...
let locationFromContext = useLocation();
let location;
// 省略代码...
let pathname = location.pathname || "/";
let remainingPathname =
parentPathnameBase === "/"
? pathname
: pathname.slice(parentPathnameBase.length) || "/";
let matches = matchRoutes(routes, { pathname: remainingPathname });
let renderedMatches = _renderMatches(
matches &&
matches.map((match) =>
Object.assign({}, match, {
params: Object.assign({}, parentParams, match.params),
pathname: joinPaths([
parentPathnameBase,
// Re-encode pathnames that were decoded inside matchRoutes
navigator.encodeLocation
? navigator.encodeLocation(match.pathname).pathname
: match.pathname,
]),
pathnameBase:
match.pathnameBase === "/"
? parentPathnameBase
: joinPaths([
parentPathnameBase,
// Re-encode pathnames that were decoded inside matchRoutes
navigator.encodeLocation
? navigator.encodeLocation(match.pathnameBase).pathname
: match.pathnameBase,
]),
})
),
parentMatches,
dataRouterState
);
Outlet
- 返回子路由的元素
/**
* Renders the child route's element, if there is one.
*
* @see https://reactrouter.com/components/outlet
*/
export function Outlet(props: OutletProps): React.ReactElement | null {
return useOutlet(props.context);
}
/**
* Returns the element for the child route at this level of the route
* hierarchy. Used internally by <Outlet> to render child routes.
*
* @see https://reactrouter.com/hooks/use-outlet
*/
export function useOutlet(context?: unknown): React.ReactElement | null {
let outlet = React.useContext(RouteContext).outlet;
if (outlet) {
return (
<OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
);
}
return outlet;
}
Link
- 返回一个带a标签的元素
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
function LinkWithRef()
// 省略代码....
return (
<a
{...rest}
href={absoluteHref || href}
onClick={isExternal || reloadDocument ? onClick : handleClick}
ref={ref}
target={target}
/>
)
);
通过上述的分析,我们可以知道React Router的原理:
一句话总结:React Router通过监听location的变化来触发组件更新,通过Routes的组件匹配对应的组件进行渲染。
简易实现
Router
这里以BrowserRouter为例
- 监听
url变化 - 传递下去
import React, { useLayoutEffect } from "react";
import Router from "./Router";
import { createBrowserHistory } from "history";
export default function BrowserRouter({ children }) {
// 创建一个引用,用于存储浏览器历史记录对象
let historyRef = React.useRef();
// 如果引用中的值为null,则创建一个浏览器历史记录对象,并将其赋值给引用
if (historyRef.current == null) {
historyRef.current = createBrowserHistory();
}
// 从引用中获取浏览器历史记录对象
const history = historyRef.current;
// 使用状态钩子函数创建一个名为state的状态,初始值为一个包含当前路由位置的对象
const [state, setState] = React.useState({
location: history.location,
});
// 使用useLayoutEffect钩子函数,在组件挂载或更新时监听路由变化,并更新状态
useLayoutEffect(() => {
// 监听路由变化,并在变化时调用setState函数更新状态
history.listen(setState);
}, [history]);
// 返回一个Router组件,将children、history和state.location作为props传递给Router组件
return (
<Router children={children} navigator={history} location={state.location} />
);
}
// Router
import React, { useMemo } from "react";
import { NavigationContext } from "./Context";
export default function Router({ navigator, children, location }) {
// 使用useMemo来缓存navigationContext对象
let navigationContext = useMemo(
() => ({ navigator, location }),
[navigator, location]
);
// 使用NavigationContext.Provider组件将navigationContext对象作为值传递给子组件
return (
<NavigationContext.Provider value={navigationContext}>
{children}
</NavigationContext.Provider>
);
}
Routes
- 匹配路由
- 构建子元素的Route配置
// Routes
import { createRoutesFromChildren } from "./createRoutesFromChildren";
import { useRoutes } from "./hooks";
export default function Routes({ children }) {
let routes = createRoutesFromChildren(children);
return useRoutes(routes);
}
// createRoutesFromChildren
import React from "react";
export function createRoutesFromChildren(children) {
const routes = [];
React.Children.forEach(children, (child) => {
const { path, element } = child.props;
let route = {
path,
element,
};
if (child.props.children) {
route.children = createRoutesFromChildren(child.props.children);
}
routes.push(route);
});
return routes;
}
// useRoutes
export function useRoutes(routes) {
// 获取当前页面的URL路径
const location = useLocation();
// 获取当前页面的路径名
const pathName = location.pathname;
// 遍历路由数组
return routes.map((route) => {
// 判断当前页面的路径名是否以路由的路径开头
const match = pathName.startsWith(route.path);
// 如果匹配成功
return (
match &&
// 遍历路由的子路由数组
route.children.map((child) => {
// 判断子路由的路径名是否与当前页面的路径名相等
// 规范化路由
// normalizePathname(""); // 输出: "/"
// normalizePathname("/home/"); // 输出: "/home"
// normalizePathname("////home////"); // 输出: "/home"
let m = normalizePathname(child.path) === pathName;
// 如果匹配成功
return (
m && (
// 传递子元素给outlet
<RouteContext.Provider
value={{ outlet: child.element }}
children={
// 如果路由的元素属性存在,则渲染该元素,否则渲染<Outlet />组件
route.element !== undefined ? route.element : <Outlet />
}
></RouteContext.Provider>
)
);
})
);
});
}
export function useLocation() {
const { location } = useContext(NavigationContext);
return location;
}
Link
在上述代码中,传递了history对象过来,我们可以直接调用api来实现跳转
// Link
import { useNavigate } from "./hooks";
export default function Link({ to, children }) {
const navigate = useNavigate()
const handleClick = (e) => {
// 页面不刷新,拦截默认行为
e.preventDefault();
navigate(to)
};
return (
<a href={to} onClick={handleClick}>
{children}
</a>
);
}
export function useNavigate() {
// 跳转
const { navigator } = useContext(NavigationContext);
return navigator.push;
}
Outlet
只需要取出传递过来的child.element即可
import { useOutlet } from "./hooks";
export default function Outlet(props) {
return useOutlet();
}
export function useOutlet() {
const { outlet } = useContext(RouteContext);
return outlet;
}
验证demo
import {
BrowserRouter as Router,
Routes,
Route,
Link,
Outlet,
} from "./mini-react-router";
export default function App(props) {
return (
<div className="app">
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="product" element={<Product />}>
</Route>
</Route>
</Routes>
</Router>
</div>
);
}
function Layout(props) {
return (
<div>
<h2>Layout</h2>
<Link to="/">首页</Link>
<Link to="/product">商品</Link>
<Outlet />
</div>
);
}
function Home() {
return (
<div>
<h1>Home</h1>
</div>
);
}
function Product() {
return (
<div>
<h1>Product</h1>
</div>
);
}
到目前为止,我们已经完成了路由跳转的核心功能。接下来,我们可以进一步思考如何实现动态匹配、路由守卫和懒加载等功能。
参考文章