思维导图
demo
// import {
// BrowserRouter as Router,
// // HashRouter as Router,
// // MemoryRouter as Router,
// Routes,
// Route,
// Link,
// Outlet,
// useNavigate,
// useParams,
// useResolvedPath,
// } from "react-router-dom";
import {
BrowserRouter as Router,
Routes,
Route,
Link,
Outlet,
useParams,
useNavigate,
useResolvedPath,
} from "./mini-react-router";
export default function App(props) {
return (
<div className="app">
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
{/* <Route path="product" element={<Product />} /> */}
<Route path="product" element={<Product />}>
<Route path=":id" element={<ProductDetail />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
</Router>
</div>
);
}
function Layout() {
return (
<div>
<Link to="/">首页</Link>
<Link to="product">商品</Link>
<Outlet />
</div>
);
}
function Home() {
return (
<div>
<h1>Home</h1>
</div>
);
}
function Product() {
const path = useResolvedPath("123");
console.log("path", path); //sy-log
return (
<div>
<h1>Product</h1>
<Link to="123">详情</Link>
<Outlet />
</div>
);
}
function ProductDetail() {
const params = useParams();
const navigate = useNavigate();
return (
<div>
<h1>ProductDetail: {params.id}</h1>
<button
onClick={() => {
navigate("/");
}}
>
go home
</button>
</div>
);
}
function NoMatch() {
return (
<div>
<h1>NoMatch</h1>
</div>
);
}
BrowserRouter
import { createBrowserHistory } from "history";
import { useRef, useState, useLayoutEffect } from "react";
import Router from "./Router";
export default function BrowserRouter({ children }) {
// 存值,在组件卸载前,这个值在组件任何一个生命周期都指向同一个地址
let historyRef = useRef();
if (historyRef.current == null) {
historyRef.current = createBrowserHistory();
}
let history = historyRef.current;
//location是为了实现useLocation用的
const [state, setState] = useState({ location: history.location });
useLayoutEffect(() => {
const unlisten = history.listen(setState);
// history.listen((location) => {
// setState({ location });
// });
return unlisten;
}, [history]);
return (
<Router children={children} naviagtor={history} location={state.location} />
);
}
- history是h5的api,可提供replace、push等路由跳转api
- historyRef保存了history的值,在组件卸载前,这个值在任何一个生命周期都指向同一个地址
- navigator的作用是给后代传history值,比如可以实现useNavigate这个自定义hook。location主要是监听路由地址的变化,用于实现useLocation
Router
import { NavigationContext } from "./Context";
// 跨组件层级传递数据 context
export default function Router({ naviagtor, children, location }) {
let naviagtionContext = { naviagtor, location };
return (
<NavigationContext.Provider value={naviagtionContext}>
{children}
</NavigationContext.Provider>
);
}
单纯作为一个中间封装,到后来HashRouter也会用到这个组件
Routes
import React, { isValidElement } from "react";
import { useRoutes } from "./hooks";
export function createRoutesFromChildren(children) {
let routes = [];
React.Children.forEach(children, (child) => {
if (!isValidElement(child)) {
return;
}
let route = {
element: child.props.element,
path: child.props.path,
index: child.props.index,
};
if (child.props.children) {
route.children = createRoutesFromChildren(child.props.children);
}
routes.push(route);
});
return routes;
}
export default function Routes({ children }) {
let routes = createRoutesFromChildren(children);
return useRoutes(routes);
}
- createRoutesFromChildren借助虚拟dom转化为fiber的实现,从child里获取route,最终得到routes
- useRoutes里的实现是路由匹配渲染的核心,具体可见useRoutes的实现
hooks
import { useContext, useMemo } from "react";
import { matchRoutes } from "react-router-dom";
import { parsePath } from "history";
import Outlet from "./Outlet";
import { NavigationContext, RouteContext } from "./Context";
import {
normalizePathname,
// matchRoutes
} from "./utils";
// todo 1.监听location,准确渲染路由
// todo 2. 渲染子路由 实现Outlet
export function useRoutes(routes) {
// 遍历routes,渲染匹配的route
const location = useLocation();
let pathname = location.pathname;
console.log(routes, "routes000");
const matches = matchRoutes(routes, { pathname });
console.log(matches, "matches000");
return _renderMatches(matches);
}
function _renderMatches(matches, parentMatches = []) {
if (matches == null) {
return null;
}
return matches.reduceRight((outlet, match, index) => {
return (
<RouteContext.Provider
children={match.route.element}
value={{
outlet,
matches: parentMatches.concat(matches.slice(0, index + 1)),
}}
/>
);
}, null);
}
export function useLocation() {
const { location } = useContext(NavigationContext);
return location;
}
// 路由跳转函数
export function useNavigate() {
const { naviagtor } = useContext(NavigationContext);
return naviagtor.push;
}
// 获取outlet
export function useOutlet() {
const { outlet } = useContext(RouteContext);
return outlet;
}
export function useParams() {
const { matches } = useContext(RouteContext);
const rootMatch = matches[matches.length - 1];
return rootMatch ? rootMatch.params : {};
}
export function useResolvedPath(to) {
const { matches } = useContext(RouteContext);
const { pathname: locationPathname } = useLocation();
let routePathenameJoson = JSON.stringify(
matches.map((match) => match.pathnameBase)
);
return useMemo(
() => resolveTo(to, JSON.parse(routePathenameJoson), locationPathname),
[to, routePathenameJoson, locationPathname]
);
}
function resolveTo(toArg, routePathnames, locationPathname): Path {
let to = typeof toArg === "string" ? parsePath(toArg) : toArg;
let toPathname = toArg === "" || to.pathname === "" ? "/" : to.pathname;
let from;
if (toPathname == null) {
from = locationPathname;
} else {
let routePathnameIndex = routePathnames.length - 1;
if (toPathname.startsWith("..")) {
let toSegments = toPathname.split("/");
while (toSegments[0] === "..") {
toSegments.shift();
routePathnameIndex -= 1;
}
to.pathname = toSegments.join("/");
}
from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
}
let path = resolvePath(to, from);
if (
toPathname &&
toPathname !== "/" &&
toPathname.endsWith("/") &&
!path.pathname.endsWith("/")
) {
path.pathname += "/";
}
return path;
}
export function resolvePath(to, fromPathname = "/"): Path {
let {
pathname: toPathname,
search = "",
hash = "",
} = typeof to === "string" ? parsePath(to) : to;
let pathname = toPathname
? toPathname.startsWith("/")
? toPathname
: resolvePathname(toPathname, fromPathname)
: fromPathname;
return {
pathname,
};
}
function resolvePathname(relativePath, fromPathname) {
let segments = fromPathname.replace(/\/+$/, "").split("/");
let relativeSegments = relativePath.split("/");
relativeSegments.forEach((segment) => {
if (segment === "..") {
if (segments.length > 1) segments.pop();
} else if (segment !== ".") {
segments.push(segment);
}
});
return segments.length > 1 ? segments.join("/") : "/";
}
Link
import { useNavigate, useResolvedPath } from "./hooks";
export default function Link({ to, children }) {
const { pathname } = useResolvedPath(to);
const navigate = useNavigate();
const handleClick = (e) => {
e.preventDefault();
// 跳转
navigate(pathname);
};
return (
<a href={pathname} onClick={handleClick}>
{children}
</a>
);
}
Outlet
import { useOutlet } from "./hooks";
export default function Outlet() {
return useOutlet();
}
utils
// 1. 删除结尾的多个/
// 2. 删除开头的多少/
import { matchPath } from "react-router-dom";
// 如 ///product/detail/// -> /product/detail
export const normalizePathname = (pathname) =>
pathname.replace(/\/+$/, "").replace(/^\/*/, "/");
// [/, /product] -> ///product -> /product
const joinPaths = (paths) => paths.join("/").replace(/\/\/+/g, "/");
function stripBasename(pathname, basename) {
if (basename === "/") return pathname;
if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
return null;
}
let nextChar = pathname.charAt(basename.length);
if (nextChar && nextChar !== "/") {
// pathname does not start with basename/
return null;
}
return pathname.slice(basename.length) || "/";
}
export function matchRoutes(routes, location) {
let pathname = location.pathname;
let branches = flatternRoutes(routes);
let matches = null;
for (let i = 0; matches == null && i < branches.length; i++) {
matches = matchRouteBranch(branches[i], pathname);
}
return matches;
}
function flatternRoutes(
routes,
branches = [],
parentMeta = [],
parentPath = ""
) {
routes.forEach((route) => {
let meta = {
relativePath: route.path || "",
route,
};
let path = joinPaths([parentPath, meta.relativePath]);
let routesMeta = parentMeta.concat(meta);
if (route.children && route.children.length > 0) {
flatternRoutes(route.children, branches, routesMeta, path);
}
if (route.path == null && !route.index) {
return;
}
branches.push({
path,
routesMeta,
});
});
return branches;
}
// /prodcut/123
function matchRouteBranch(branch, pathname) {
const { routesMeta } = branch;
let matches = [];
for (let i = 0; i < routesMeta.length; i++) {
let meta = routesMeta[i];
let end = routesMeta.length - 1 === i;
let match = matchPath({ path: meta.relativePath, end }, pathname);
if (!match) {
return null;
}
matches.push({
params: match.params,
pathname: match.pathname,
route: meta.route,
});
}
return matches;
}