react-router|react-router 源码阅读

这次的版本是 6.2.1
使用 相比较 5.x 版本, 元素升级为了
简单的 v6 例子:
function App(){ return }/> }

context 在 react-router 中, 他创建了两个 context 供后续的使用, 当然这两个 context 是在内部的, 并没有 API 暴露出来
NavigationContext
/** * 一个路由对象的基本构成 */ export interface RouteObject { caseSensitive?: boolean; children?: RouteObject[]; element?: React.ReactNode; index?: boolean; path?: string; }// 常用的参数类型 export type Params = { readonly [key in Key]: string | undefined; }; /** * 一个 路由匹配 接口 */ export interface RouteMatch { /** * 动态参数的名称和值的URL */ params: Params; /** * 路径名 */ pathname: string; /** * 之前匹配的路径名 */ pathnameBase: string; /** * 匹配到的路由对象 */ route: RouteObject; }interface RouteContextObject { outlet: React.ReactElement | null; matches: RouteMatch[]; }const RouteContext = React.createContext({ outlet: null, matches: [] });

LocationContext
import type { Location, Action as NavigationType } from "history"; interface LocationContextObject { location: Location; // 原生的 location 对象, window.location/** * enum Action 一个枚举, 他有三个参数, 代表路由三种动作 * Pop = "POP", * Push = "PUSH", * Replace = "REPLACE" */ navigationType: NavigationType; }const LocationContext = React.createContext(null!);

MemoryRouter 在 react-router-dom 的源码解析中我们说到了 BrowserRouterHashRouter, 那么这个 MemoryRouter又是什么呢
他是将 URL 的历史记录保存在内存中的 (不读取或写入地址栏)。在测试和非浏览器环境中很有用,例如 React Native。
他的源码和其他两个 Router 最大的区别就是一个 createMemoryHistory 方法, 此方法也来自于 history 库中
export function MemoryRouter({ basename, children, initialEntries, initialIndex }: MemoryRouterProps): React.ReactElement { let historyRef = React.useRef(); if (historyRef.current == null) { historyRef.current = createMemoryHistory({ initialEntries, initialIndex }); }let history = historyRef.current; let [state, setState] = React.useState({ action: history.action, location: history.location }); React.useLayoutEffect(() => history.listen(setState), [history]); return ( ); }

那我们现在来看一看这个方法, 这里只讲他与 createHashHistory 不同的地方:
export function createMemoryHistory( options: MemoryHistoryOptions = {} ): MemoryHistory { let { initialEntries = ['/'], initialIndex } = options; // 不同的初始值 initialEntries let entries: Location[] = initialEntries.map((entry) => { let location = readOnly({ pathname: '/', search: '', hash: '', state: null, key: createKey(), // 通过 random 生成唯一值 ...(typeof entry === 'string' ? parsePath(entry) : entry) }); // 这里的 location 属于是直接创建, HashHistory 中是使用的 window.location // readOnly方法 可以看做 (obj)=>obj, 并没有太大作用 return location; }); function push(to: To, state?: any) { let nextAction = Action.Push; let nextLocation = getNextLocation(to, state); function retry() { push(to, state); }// 忽略其他类似的代码if (allowTx(nextAction, nextLocation, retry)) { index += 1; // 别处是调用原生 API, history.pushState entries.splice(index, entries.length, nextLocation); applyTx(nextAction, nextLocation); } }// 与 push 类似, 忽略 replacefunction go(delta: number) { // 与HashHistory不同, 也是走的类似 push let nextIndex = clamp(index + delta, 0, entries.length - 1); let nextAction = Action.Pop; let nextLocation = entries[nextIndex]; function retry() { go(delta); }if (allowTx(nextAction, nextLocation, retry)) { index = nextIndex; applyTx(nextAction, nextLocation); } }let history: MemoryHistory = { // 基本相同 }; return history; }

Navigate 用来改变 当然 location 的方法, 是一个 react-router 抛出的 API
使用方式:
function App() { // 一旦 user 是有值的, 就跳转至 `/dashboard` 页面了 // 算是跳转路由的一种方案 return {user && ( )}
this.handleSubmit(event)}>
}

源码
export function Navigate({ to, replace, state }: NavigateProps): null { // 直接调用 useNavigate 来获取 navigate 方法, 并且useEffect 每次都会触发 // useNavigate 源码在下方会讲到 let navigate = useNavigate(); React.useEffect(() => { navigate(to, { replace, state }); }); return null; }

Outlet 用来渲染子路由的元素, 简单来说就是一个路由的占位符
代码很简单, 使用的逻辑是这样
使用方式:
function App(props) { return ( }> }/> ); }// 其中外层的Dashboard:function Dashboard() { return (Dashboard // 这里就会渲染他的子路由了 // 和以前 children 差不多); }

源码
export function Outlet(props: OutletProps): React.ReactElement | null { return useOutlet(props.context); }export function useOutlet(context?: unknown): React.ReactElement | null { let outlet = React.useContext(RouteContext).outlet; if (outlet) { return ( {outlet} ); } return outlet; }

useParams 从当前URL所匹配的路径中, 返回一个对象的键/值对的动态参数。
function useParams< ParamsOrKey extends string | Record = string >(): Readonly< [ParamsOrKey] extends [string] ? Params : Partial> { // 直接获取了 RouteContext 中 matches 数组的最后一个对象, 如果没有就是空对象 let { matches } = React.useContext(RouteContext); let routeMatch = matches[matches.length - 1]; return routeMatch ? (routeMatch.params as any) : {}; }

useResolvedPath 将给定的`to'值的路径名与当前位置进行比较
这个组件中使用到
function useResolvedPath(to: To): Path { let { matches } = React.useContext(RouteContext); let { pathname: locationPathname } = useLocation(); // 合并成一个 json 字符, 至于为什么又要解析, 是为了添加字符层的缓存, 如果是一个对象, 就不好浅比较了 let routePathnamesJson = JSON.stringify( matches.map(match => match.pathnameBase) ); // TODO resolveTo return React.useMemo( () => resolveTo(to, JSON.parse(routePathnamesJson), locationPathname), [to, routePathnamesJson, locationPathname] ); }

useRoutes useRoutes钩子的功能等同于,但它使用JavaScript对象而不是元素来定义路由。
相当于是一种 schema 版本, 更好的配置性
使用方式:
如果使用过 umi, 是不是会感觉到一模一样
function App() { let element = useRoutes([ { path: "/", element:}, { path: "dashboard", element: }, { path: "invoices", element: , children: [ { path: ":id", element: }, { path: "sent", element:} ] }, { path: "*", element: } ]); return element; }

源码
// 具体的 routes 对象是如何生成的, 下面的 Routes-createRoutesFromChildren 会讲到export function useRoutes( routes: RouteObject[], locationArg?: Partial | string ): React.ReactElement | null {let { matches: parentMatches } = React.useContext(RouteContext); let routeMatch = parentMatches[parentMatches.length - 1]; // 获取匹配的 routelet parentParams = routeMatch ? routeMatch.params : {}; let parentPathname = routeMatch ? routeMatch.pathname : "/"; let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/"; let parentRoute = routeMatch && routeMatch.route; // 这里上面都是一些参数, 没有就是默认值//等于 React.useContext(LocationContext).location, 约等于原生的 location let locationFromContext = useLocation(); let location; if (locationArg) { // 对于配置项参数的一些判断 let parsedLocationArg = typeof locationArg === "string" ? parsePath(locationArg) : locationArg; location = parsedLocationArg; } else { location = locationFromContext; } // 如果参数里有则使用参数里的, 如果没有使用 context 的let pathname = location.pathname || "/"; let remainingPathname = parentPathnameBase === "/" ? pathname : pathname.slice(parentPathnameBase.length) || "/"; // matchRoutes 大概的作用是通过pathname遍历寻找,匹配到的路由具体源码放在下面讲 let matches = matchRoutes(routes, { pathname: remainingPathname }); // 最后调用渲染函数首先对数据进行 map // joinPaths的作用约等于 paths.join("/") 并且去除多余的斜杠 return _renderMatches( matches && matches.map(match => Object.assign({}, match, { params: Object.assign({}, parentParams, match.params), pathname: joinPaths([parentPathnameBase, match.pathname]), pathnameBase: match.pathnameBase === "/" ? parentPathnameBase : joinPaths([parentPathnameBase, match.pathnameBase]) }) ), parentMatches ); }

useRoutes-matchRoutes
function matchRoutes( routes: RouteObject[], locationArg: Partial | string, basename = "/" ): RouteMatch[] | null { let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg; // 获取排除 basename 的 pathname let pathname = stripBasename(location.pathname || "/", basename); if (pathname == null) { return null; }// flattenRoutes 函数的主要作用, 压平 routes, 方便遍历 // 源码见下方 let branches = flattenRoutes(routes); // 对路由进行排序 // rankRouteBranches 源码见下方 rankRouteBranches(branches); // 筛选出匹配到的路由 matchRouteBranch源码在下面讲 let matches = null; for (let i = 0; matches == null && i < branches.length; ++i) { matches = matchRouteBranch(branches[i], pathname); }return matches; }

useRoutes-matchRoutes-stripBasename 拆分 basename, 代码很简单, 这里就直接贴出来了
function stripBasename(pathname: string, basename: string): string | null { if (basename === "/") return pathname; if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) { return null; }let nextChar = pathname.charAt(basename.length); if (nextChar && nextChar !== "/") { return null; }return pathname.slice(basename.length) || "/"; }

useRoutes-matchRoutes-flattenRoutes 递归处理 routes, 压平 routes
function flattenRoutes( routes: RouteObject[], branches: RouteBranch[] = [], parentsMeta: RouteMeta[] = [], parentPath = "" ): RouteBranch[] { routes.forEach((route, index) => { let meta: RouteMeta = { relativePath: route.path || "", caseSensitive: route.caseSensitive === true, childrenIndex: index, route }; if (meta.relativePath.startsWith("/")) { meta.relativePath = meta.relativePath.slice(parentPath.length); }// joinPaths 源码: (paths)=>paths.join("/").replace(/\/\/+/g, "/") // 把数组转成字符串, 并且清除重复斜杠 let path = joinPaths([parentPath, meta.relativePath]); let routesMeta = parentsMeta.concat(meta); // 如果有子路由则递归 if (route.children && route.children.length > 0) { flattenRoutes(route.children, branches, routesMeta, path); }// 匹配不到就 return if (route.path == null && !route.index) { return; } // 压平后组件添加的对象, TODO computeScore branches.push({ path, score: computeScore(path, route.index), routesMeta }); }); return branches; }

useRoutes-matchRoutes-rankRouteBranches 对路由进行排序, 这里可以略过,不管排序算法如何, 只需要知道, 知道输入的值是经过一系列排序的就行
function rankRouteBranches(branches: RouteBranch[]): void { branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first : compareIndexes( a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex) ) ); }

useRoutes-matchRoutes-matchRouteBranch 匹配函数, 接受参数 branch 就是某一个 rankRouteBranches
function matchRouteBranch( branch: RouteBranch, pathname: string ): RouteMatch[] | null { let { routesMeta } = branch; let matchedParams = {}; let matchedPathname = "/"; let matches: RouteMatch[] = []; //routesMeta 详细来源可以查看 上面的flattenRoutes for (let i = 0; i < routesMeta.length; ++i) { let meta = routesMeta[i]; let end = i === routesMeta.length - 1; let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/"; // 比较, matchPath 源码在下方 let match = matchPath( { path: meta.relativePath, caseSensitive: meta.caseSensitive, end }, remainingPathname ); // 如果返回是空 则直接返回 if (!match) return null; // 更换对象源 Object.assign(matchedParams, match.params); let route = meta.route; // push 到最终结果上, joinPaths 不再赘述 matches.push({ params: matchedParams, pathname: joinPaths([matchedPathname, match.pathname]), pathnameBase: joinPaths([matchedPathname, match.pathnameBase]), route }); if (match.pathnameBase !== "/") { matchedPathname = joinPaths([matchedPathname, match.pathnameBase]); } }return matches; }

useRoutes-matchRoutes-matchRouteBranch-matchPath 对一个URL路径名进行模式匹配,并返回有关匹配的信息。
他也是一个保留在外的可用 API
export function matchPath< ParamKey extends ParamParseKey, Path extends string >( pattern: PathPattern | Path, pathname: string ): PathMatch | null { // pattern 的重新赋值 if (typeof pattern === "string") { pattern = { path: pattern, caseSensitive: false, end: true }; }// 通过正则匹配返回匹配到的正则表达式matcher 为 RegExp let [matcher, paramNames] = compilePath( pattern.path, pattern.caseSensitive, pattern.end ); // 正则对象的 match 方法 let match = pathname.match(matcher); if (!match) return null; // 取 match 到的值 let matchedPathname = match[0]; let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1"); let captureGroups = match.slice(1); // params 转成对象{ param:value, ... } let params: Params = paramNames.reduce( (memo, paramName, index) => { // 如果是*号转换 if (paramName === "*") { let splatValue = https://www.it610.com/article/captureGroups[index] ||""; pathnameBase = matchedPathname .slice(0, matchedPathname.length - splatValue.length) .replace(/(.)\/+$/, "$1"); }// safelyDecodeURIComponent等于 decodeURIComponent + try_catch memo[paramName] = safelyDecodeURIComponent( captureGroups[index] || "", paramName ); return memo; }, {} ); return { params, pathname: matchedPathname, pathnameBase, pattern }; }

useRoutes-matchRoutes-matchRouteBranch-matchPath-compilePath
function compilePath( path: string, caseSensitive = false, end = true ): [RegExp, string[]] { let paramNames: string[] = []; // 正则匹配替换 let regexpSource = "^" + path // 忽略尾随的 / 和 /* .replace(/\/*\*?$/, "") // 确保以 / 开头 .replace(/^\/*/, "/") // 转义特殊字符 .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars .replace(/:(\w+)/g, (_: string, paramName: string) => { paramNames.push(paramName); return "([^\\/]+)"; }); // 对于*号的特别判断 if (path.endsWith("*")) { paramNames.push("*"); regexpSource += path === "*" || path === "/*" ? "(.*)$" // Already matched the initial /, just match the rest : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"] } else { regexpSource += end ? "\\/*$" // 匹配到末尾时,忽略尾部斜杠 : "(?:\\b|\\/|$)"; }let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i"); // 返回匹配结果 return [matcher, paramNames]; }

useRoutes-_renderMatches
渲染匹配到的路由
function _renderMatches( matches: RouteMatch[] | null, parentMatches: RouteMatch[] = [] ): React.ReactElement | null {if (matches == null) return null; // 通过 context 传递数据 return matches.reduceRight((outlet, match, index) => { return ( } value=https://www.it610.com/article/{{ outlet, matches: parentMatches.concat(matches.slice(0, index + 1)) }} /> ); }, null as React.ReactElement | null); }

Router 为应用程序的其他部分提供context信息
通常不会使用此组件, 他是 MemoryRouter 最终渲染的组件
在 react-router-dom 库中, 也是 BrowserRouter 和 HashRouter 的最终渲染组件
export function Router({ basename: basenameProp = "/", children = null, location: locationProp, navigationType = NavigationType.Pop, navigator, static: staticProp = false }: RouterProps): React.ReactElement | null {// 格式化 baseName let basename = normalizePathname(basenameProp); // memo context value let navigationContext = React.useMemo( () => ({ basename, navigator, static: staticProp }), [basename, navigator, staticProp] ); // 如果是字符串则解析根据 #, ? 特殊符号解析 url if (typeof locationProp === "string") { locationProp = parsePath(locationProp); }let { pathname = "/", search = "", hash = "", state = null, key = "default" } = locationProp; // 同样的缓存 let location = React.useMemo(() => { // 这还方法在 useRoutes-matchRoutes-stripBasename 讲过这里就不多说 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; }// 提供 context 的 provider, 传递 children return ( ); }

parsePath 此源码来自于 history 仓库
function parsePath(path: string): Partial { let parsedPath: Partial = {}; // 首先确定 path if (path) { // 是否有#号 , 如果有则截取 let hashIndex = path.indexOf('#'); if (hashIndex >= 0) { parsedPath.hash = path.substr(hashIndex); path = path.substr(0, hashIndex); }// 再判断 ? , 有也截取 let searchIndex = path.indexOf('?'); if (searchIndex >= 0) { parsedPath.search = path.substr(searchIndex); path = path.substr(0, searchIndex); }// 最后就是 path if (path) { parsedPath.pathname = path; } } // 返回结果 return parsedPath; }

Routes 用来包裹 route 的元素, 主要是通过 useRoutes 的逻辑
function Routes({ children, location }: RoutesProps): React.ReactElement | null { return useRoutes(createRoutesFromChildren(children), location); }

Routes-createRoutesFromChildren 接收到的参数一般都是 Route children, 可能是多层嵌套的, 最后得的我们定义的 route 组件结构,
它将被传递给 useRoutes 函数
function createRoutesFromChildren( children: React.ReactNode ): RouteObject[] { let routes: RouteObject[] = []; // 使用官方函数循环 React.Children.forEach(children, element => { if (element.type === React.Fragment) { // 如果是 React.Fragment 组件 则直接push 递归函数 routes.push.apply( routes, createRoutesFromChildren(element.props.children) ); return; }let route: RouteObject = { caseSensitive: element.props.caseSensitive, element: element.props.element, index: element.props.index, path: element.props.path }; // route 对象具有的属性// 同样地递归 if (element.props.children) { route.children = createRoutesFromChildren(element.props.children); }routes.push(route); }); return routes; }

useHref 返回完整的链接
export function useHref(to: To): string { let { basename, navigator } = React.useContext(NavigationContext); // useResolvedPath 在上面讲过 let { hash, pathname, search } = useResolvedPath(to); let joinedPathname = pathname; if (basename !== "/") { let toPathname = getToPathname(to); let endsWithSlash = toPathname != null && toPathname.endsWith("/"); joinedPathname = pathname === "/" ? basename + (endsWithSlash ? "/" : "") : joinPaths([basename, pathname]); }// 可以看做, 路由的拼接, 包括 ? , # return navigator.createHref({ pathname: joinedPathname, search, hash }); }

resolveTo 解析toArg, 返回对象
function resolveTo( toArg: To, routePathnames: string[], locationPathname: string ): Path { // parsePath上面已经分析过了 let to = typeof toArg === "string" ? parsePath(toArg) : toArg; let toPathname = toArg === "" || to.pathname === "" ? "/" : to.pathname; let from: string; 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 复制 from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/"; }// 解析, 返回对象 let path = resolvePath(to, from); if ( toPathname && toPathname !== "/" && toPathname.endsWith("/") && !path.pathname.endsWith("/") ) { path.pathname += "/"; } // 确保加上末尾 /return path; }

resolveTo-resolvePath
返回一个相对于给定路径名的解析路径对象, 这里的函数也基本都讲过
function resolvePath(to: To, fromPathname = "/"): Path { let { pathname: toPathname, search = "", hash = "" } = typeof to === "string" ? parsePath(to) : to; let pathname = toPathname ? toPathname.startsWith("/") ? toPathname // resolvePathname : resolvePathname(toPathname, fromPathname) : fromPathname; return { pathname, search: normalizeSearch(search), hash: normalizeHash(hash) }; }

resolveTo-resolvePath-resolvePathname
function resolvePathname(relativePath: string, fromPathname: string): string { // 去除末尾斜杠, 再以斜杠分割成数组 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("/") : "/"; }

useLocation useNavigationType
function useLocation(): Location { // 只是获取 context 中的数据 return React.useContext(LocationContext).location; }

同上
function useNavigationType(): NavigationType { return React.useContext(LocationContext).navigationType; }

useMatch
function useMatch< ParamKey extends ParamParseKey, Path extends string >(pattern: PathPattern | Path): PathMatch | null { // 获取 location.pathname let { pathname } = useLocation(); // matchPath在 useRoutes-matchRoutes-matchRouteBranch-matchPath 中讲到过 // 对一个URL路径名进行模式匹配,并返回有关匹配的信息。 return React.useMemo( () => matchPath(pattern, pathname), [pathname, pattern] ); }

useNavigate 此 hooks 是用来获取操作路由对象的
function useNavigate(): NavigateFunction { // 从 context 获取数据 let { basename, navigator } = React.useContext(NavigationContext); let { matches } = React.useContext(RouteContext); let { pathname: locationPathname } = useLocation(); // 转成 json, 方便 memo 对比 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; // 控制渲染 // 如果 go 是数字, 则结果类似于 go 方法 if (typeof to === "number") { navigator.go(to); return; } // 解析go let path = resolveTo( to, JSON.parse(routePathnamesJson), locationPathname ); if (basename !== "/") { path.pathname = joinPaths([basename, path.pathname]); } // 这一块 就是 前一个括号产生函数, 后一个括号传递参数 // 小小地转换下: // !!options.replace ? //navigator.replace( //path, //options.state //) //: navigator.push( //path, //options.state //) // (!!options.replace ? navigator.replace : navigator.push)( path, options.state ); }, [basename, navigator, routePathnamesJson, locationPathname] ); // 最后返回 return navigate; }

generatePath 返回一个有参数插值的路径。 原理还是通过正则替换
function generatePath(path: string, params: Params = {}): string { return path .replace(/:(\w+)/g, (_, key) => { return params[key]!; }) .replace(/\/*\*$/, _ => params["*"] == null ? "" : params["*"].replace(/^\/*/, "/") ); }

他的具体使用:
generatePath("/users/:id", { id: 42 }); // "/users/42" generatePath("/files/:type/*", { type: "img", "*": "cat.jpg" }); // "/files/img/cat.jpg"

【react-router|react-router 源码阅读】这里的代码可以说是覆盖整个 react-router 80%以上, 有些简单的, 用处小的这里也不再过多赘述了
参考文档:
  • https://reactrouter.com/docs/...
  • https://github.com/remix-run/...
  • https://github.com/remix-run/...

    推荐阅读