React Router基于monorepo的架包(指在一个项目仓库(repo)中管理多个模块/包(package))。
- react-router:React Router的核心基本功能,为react-router-dom和react-router-native服务;
- react-router-dom:在web应用使用React Router的方法;
- react-router-native:在RN中使用React Router的方法;
- react-router-dom-v5-compat:V5迁移至V6的垫片;
这里主要总结react-router核心库
1. react-router
与运行环境无关,几乎所有运行平台无关的方法、组件和hooks都是在这里定义的
- index.ts:入口文件,且标识了三个不安全的API,要使用的话,不要单独从lib/context.ts引入,要从react-router的入口文件引入(虽然一般开发中用不到 )。
/** @internal */
export {
NavigationContext as UNSAFE_NavigationContext,
LocationContext as UNSAFE_LocationContext,
RouteContext as UNSAFE_RouteContext,
};
1.1 router
Router在react-router内部主要用于提供全局的路由导航对象(一般由history库提供)以及当前的路由导航状态,在项目中使用时一般是必须并且唯一的,不过一般不会直接使用,更多会使用已经封装好的路由导航对象的BrowserRouter(react-router-dom包引入)、HashRouter(react-router-dom包引入)和MemoryRouter(react-router包引入)
- router context
import React from 'react'
import type {
History,
Location,
} from "history";
import {
Action as NavigationType,
} from "history";
// 只包含,go、push、replace、createHref 四个方法的 History 对象,用于在 react-router 中进行路由跳转
export type Navigator = Pick<History, "go" | "push" | "replace" | "createHref">;
interface NavigationContextObject {
basename: string;
navigator: Navigator;
static: boolean;
}
/**
* 内部含有 navigator 对象的全局上下文,官方不推荐在外直接使用
*/
const NavigationContext = React.createContext<NavigationContextObject>(null!);
interface LocationContextObject {
location: Location;
navigationType: NavigationType;
}
/**
* 内部含有当前 location 与 action 的 type,一般用于在内部获取当前 location,官方不推荐在外直接使用
*/
const LocationContext = React.createContext<LocationContextObject>(null!);
// 这是官方对于上面两个 context 的导出,可以看到都是被定义为不安全的,并且可能会有着重大更改,强烈不建议使用
/** @internal */
export {
NavigationContext as UNSAFE_NavigationContext,
LocationContext as UNSAFE_LocationContext,
};
- Hooks:基于LocationContext的三个 hooks
-
- useInRouterContext
- useNavigationType
- useLocation
/**
* 断言方法
*/
function invariant(cond: any, message: string): asserts cond {
if (!cond) throw new Error(message);
}
/**
* 判断当前组件是否在一个 Router 中
*/
export function useInRouterContext(): boolean {
return React.useContext(LocationContext) != null;
}
/**
* 获取当前的跳转的 action type
*/
export function useNavigationType(): NavigationType {
return React.useContext(LocationContext).navigationType;
}
/**
* 获取当前跳转的 location
*/
export function useLocation(): Location {
// useLocation 必须在 Router 提供的上下文中使用
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useLocation() may be used only in the context of a <Router> component.`
);
return React.useContext(LocationContext).location;
}
- 定义Router组件
传入Context与外部传入的location
// 接上面,这里额外还从 history 中引入了 parsePath 方法
import {
parsePath
} from "history";
export interface RouterProps {
// 路由前缀
basename?: string;
children?: React.ReactNode;
// 必传,当前 location
/*
interface Location {
pathname: string;
search: string;
hash: string;
state: any;
key: string;
}
*/
location: Partial<Location> | string;
// 当前路由跳转的类型,有 POP,PUSH 与 REPLACE 三种
navigationType?: NavigationType;
// 必传,history 中的导航对象,我们可以在这里传入统一外部的 history
navigator: Navigator;
// 是否为静态路由(ssr)
static?: boolean;
}
/**
* 提供渲染 Route 的上下文,但是一般不直接使用这个组件,会包装在 BrowserRouter 等二次封装的路由中
* 整个应用程序应该只有一个 Router
* Router 的作用就是格式化传入的原始 location 并渲染全局上下文 NavigationContext、LocationContext
*/
export function Router({
basename: basenameProp = "/",
children = null,
location: locationProp,
navigationType = NavigationType.Pop,
navigator,
static: staticProp = false
}: RouterProps): React.ReactElement | null {
// 断言,Router 不能在其余 Router 内部,否则抛出错误
invariant(
!useInRouterContext(),
`You cannot render a <Router> inside another <Router>.` +
` You should never have more than one in your app.`
);
// 格式化 basename,去掉 url 中多余的 /,比如 /a//b 改为 /a/b
let basename = normalizePathname(basenameProp);
// 全局的导航上下文信息,包括路由前缀,导航对象等
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
// 转换 location,传入 string 将转换为对象
if (typeof locationProp === "string") {
// parsePath 用于将 locationProp 转换为 Path 对象,都是 history 库引入的
/*
interface Path {
pathname: string;
search: string;
hash: string;
}
*/
locationProp = parsePath(locationProp);
}
let {
pathname = "/",
search = "",
hash = "",
state = null,
key = "default"
} = locationProp;
// 经过抽离 base 后的真正的 location,如果抽离 base 失败返回 null
let location = React.useMemo(() => {
// stripBasename 用于去除 pathname 前面 basename 部分
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 (
// 唯一传入 location 的地方
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
);
}
// 格式化方法
/**
* 格式化 pathname
* @param pathname
* @returns
*/
const normalizePathname = (pathname: string): string =>
pathname.replace(//+$/, "").replace(/^/*/, "/");
/**
*
* 抽离 basename,获取纯粹的 path,如果没有匹配到则返回 null
* @param pathname
* @param basename
* @returns
*/
function stripBasename(pathname: string, basename: string): string | null {
if (basename === "/") return pathname;
// 如果 basename 与 pathname 不匹配,返回 null
if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
return null;
}
// 上面只验证了是否 pathname 包含 basename,这里还需要验证包含 basename 后第一个字母是否为 /,不为 / 证明并不是该 basename 下的路径,返回 null
let nextChar = pathname.charAt(basename.length);
if (nextChar && nextChar !== "/") {
return null;
}
// 返回去除掉 basename 的 path
return pathname.slice(basename.length) || "/";
}
- memory router封装
其实就是将history库与我们声明的Router组件绑定起来,当history.listen 监听到路由改变后重新设置当前的location 与 action。
import type { InitialEntry, MemoryHistory } from 'history';
import { createMemoryHistory } from 'history';
export interface MemoryRouterProps {
// 路由前缀
basename?: string;
children?: React.ReactNode;
// 与 createMemoryHistory 返回的 history 对象参数相对应,代表的是自定义的页面栈与索引
initialEntries?: InitialEntry[];
initialIndex?: number;
}
/**
* react-router 里面只有 MemoryRouter,其余的 router 在 react-router-dom 里
*/
export function MemoryRouter({
basename,
children,
initialEntries,
initialIndex
}: MemoryRouterProps): React.ReactElement {
// history 对象的引用
let historyRef = React.useRef<MemoryHistory>();
if (historyRef.current == null) {
// 创建 memoryHistory
historyRef.current = createMemoryHistory({ initialEntries, initialIndex });
}
let history = historyRef.current;
let [state, setState] = React.useState({
action: history.action,
location: history.location
});
// 监听 history 改变,改变后重新 setState
React.useLayoutEffect(() => history.listen(setState), [history]);
// 简单的初始化并将相应状态与 React 绑定
return (
<Router
basename={basename}
children={children}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
}
- 总结:
-
- Router组件是react-router应用中必不可少的,一般直接写在应用最外层,它提供了一系列关于路由跳转和状态的上下文属性和方法;
- 一般不会直接使用Router组件,而是使用react-router内部提供的高阶Router组件,而这些高阶组件实际上就是将history库中提供的导航对象与Router组件连接起来,进而控制应用的导航状态;
1.2 route
举个例子:
import { render } from "react-dom";
import {
BrowserRouter,
Routes,
Route
} from "react-router-dom";
// 这几个页面不用管它
import App from "./App";
import Expenses from "./routes/expenses";
import Invoices from "./routes/invoices";
const rootElement = document.getElementById("root");
render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/expenses" element={<Expenses />} />
<Route path="/invoices" element={<Invoices />} />
</Routes>
</BrowserRouter>,
rootElement
);
- props
route在react-router中只是提供命令式的路由配置的方式
// Route 有三种 props 类型,这里先了解内部参数的含义,下面会细讲
export interface PathRouteProps {
caseSensitive?: boolean;
// children 代表子路由
children?: React.ReactNode;
element?: React.ReactNode | null;
index?: false;
path: string;
}
export interface LayoutRouteProps {
children?: React.ReactNode;
element?: React.ReactNode | null;
}
export interface IndexRouteProps {
element?: React.ReactNode | null;
index: true;
}
/**
* Route 组件内部没有进行任何操作,仅仅只是定义 props,而我们就是为了使用它的 props
*/
export function Route(
_props: PathRouteProps | LayoutRouteProps | IndexRouteProps
): React.ReactElement | null {
// 这里可以看出 Route 不能够被渲染出来,渲染会直接抛出错误,证明 Router 拿到 Route 后也不会在内部操作
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>.`
);
}
- 总结
-
- Route可以被看作一个挂载用户传入参数的对象,它不会在页面中渲染,而是会被Routes接受并解析;
1.3 routes
export interface RoutesProps {
children?: React.ReactNode;
// 用户传入的 location 对象,一般不传,默认用当前浏览器的 location
location?: Partial<Location> | string;
}
/**
* 所有的 Route 都需要 Routes 包裹,用于渲染 Route(拿到 Route 的 props 的值,不渲染真实的 DOM 节点)
*/
export function Routes({
children,
location
}: RoutesProps): React.ReactElement | null {
return useRoutes(createRoutesFromChildren(children), location);
}
- createRoutesFromChildren
// 路由配置对象
export interface RouteObject {
// 路由 path 是否匹配大小写
caseSensitive?: boolean;
// 子路由
children?: RouteObject[];
// 要渲染的组件
element?: React.ReactNode;
// 是否是索引路由
index?: boolean;
path?: string;
}
/**
* 将 Route 组件转换为 route 对象,提供给 useRoutes 使用
*/
export function createRoutesFromChildren(
children: React.ReactNode
): RouteObject[] {
let routes: RouteObject[] = [];
// 内部逻辑很简单,就是递归遍历 children,获取 <Route /> props 上的所有信息,然后格式化后推入 routes 数组中
React.Children.forEach(children, element => {
if (!React.isValidElement(element)) {
// Ignore non-elements. This allows people to more easily inline
// conditionals in their route config.
return;
}
// 空节点,忽略掉继续往下遍历
if (element.type === React.Fragment) {
// Transparently support React.Fragment and its children.
routes.push.apply(
routes,
createRoutesFromChildren(element.props.children)
);
return;
}
// 不要传入其它组件,只能传 Route
invariant(
element.type === Route,
`[${
typeof element.type === "string" ? element.type : element.type.name
}] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>`
);
let route: RouteObject = {
caseSensitive: element.props.caseSensitive,
element: element.props.element,
index: element.props.index,
path: element.props.path
};
// 递归
if (element.props.children) {
route.children = createRoutesFromChildren(element.props.children);
}
routes.push(route);
});
return routes;
}
- useRoutes:声明式配置路由,下面详细介绍
- 总结:
- react-router在路由定义时包含两种方式
- 指令式:
<routes><route /></routes> - 声明式:useRoutes
- 指令式:
- Routes与Route强绑定,有Routes则必定要传入且只能传入Route;
1.4 useRoutes
import { useRoutes } from "react-router-dom";
// 此时 App 返回的就是已经渲染好的路由元素了
function App() {
let element = useRoutes([
{
path: "/",
element: <Dashboard />,
children: [
{
path: "/messages",
element: <DashboardMessages />
},
{ path: "/tasks", element: <DashboardTasks /> }
]
},
{ path: "/team", element: <AboutPage /> }
]);
return element;
}
- RouteContext
/**
* 动态参数的定义
*/
export type Params<Key extends string = string> = {
readonly [key in Key]: string | undefined;
};
export interface RouteMatch<ParamKey extends string = string> {
// params 参数,比如 :id 等
params: Params<ParamKey>;
// 匹配到的 pathname
pathname: string;
/**
* 子路由匹配之前的路径 url,这里可以把它看做是只要以 /* 结尾路径(这是父路由的路径)中 /* 之前的部分
*/
pathnameBase: string;
// 定义的路由对象
route: RouteObject;
}
interface RouteContextObject {
// 一个 ReactElement,内部包含有所有子路由组成的聚合组件,其实 Outlet 组件内部就是它
outlet: React.ReactElement | null;
// 一个成功匹配到的路由数组,索引从小到大层级依次变深
matches: RouteMatch[];
}
/**
* 包含全部匹配到的路由,官方不推荐在外直接使用
*/
const RouteContext = React.createContext<RouteContextObject>({
outlet: null,
matches: []
});
/** @internal */
export {
RouteContext as UNSAFE_RouteContext
};
- 拆解useRoutes
/**
* 1.该 hooks 不是只调用一次,每次重新匹配到路由时就会重新调用渲染新的 element
* 2.当多次调用 useRoutes 时需要解决内置的 route 上下文问题,继承外层的匹配结果
* 3.内部通过计算所有的 routes 与当前的 location 关系,经过路径权重计算,得到 matches 数组,然后将 matches 数组重新渲染为嵌套结构的组件
*/
export function useRoutes(
routes: RouteObject[],
locationArg?: Partial<Location> | string
): React.ReactElement | null {
// useRoutes 必须最外层有 Router 包裹,不然报错
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useRoutes() may be used only in the context of a <Router> component.`
);
// 1.当此 useRoutes 为第一层级的路由定义时,matches 为空数组(默认值)
// 2.当该 hooks 在一个已经调用了 useRoutes 的渲染环境中渲染时,matches 含有值(也就是有 Routes 的上下文环境嵌套)
let { matches: parentMatches } = React.useContext(RouteContext);
// 最后 match 到的 route(深度最深),该 route 将作为父 route,我们后续的 routes 都是其子级
let routeMatch = parentMatches[parentMatches.length - 1];
// 下面是父级 route 的参数,我们会基于以下参数操作,如果项目中只在一个地方调用了 useRoutes,一般都会是默认值
let parentParams = routeMatch ? routeMatch.params : {};
// 父路由的完整 pathname,比如路由设置为 /foo/*,当前导航是 /foo/1,那么 parentPathname 就是 /foo/1
let parentPathname = routeMatch ? routeMatch.pathname : "/";
// 同上面的 parentPathname,不过是 /* 前的部分,也就是 /foo
let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
let parentRoute = routeMatch && routeMatch.route;
// 获取上下文环境中的 location
let locationFromContext = useLocation();
// 判断是否手动传入了 location,否则用默认上下文的 location
let location;
if (locationArg) {
// 格式化为 Path 对象
let parsedLocationArg =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
// 如果传入了 location,判断是否与父级路由匹配(作为子路由存在)
invariant(
parentPathnameBase === "/" ||
parsedLocationArg.pathname?.startsWith(parentPathnameBase),
`When overriding the location using `<Routes location>` or `useRoutes(routes, location)`, ` +
`the location pathname must begin with the portion of the URL pathname that was ` +
`matched by all parent routes. The current pathname base is "${parentPathnameBase}" ` +
`but pathname "${parsedLocationArg.pathname}" was given in the `location` prop.`
);
location = parsedLocationArg;
} else {
location = locationFromContext;
}
let pathname = location.pathname || "/";
// 剩余的 pathname,整体 pathname 减掉父级已经匹配的 pathname,才是本次 routes 要匹配的 pathname(适用于 parentMatches 匹配不为空的情况)
let remainingPathname =
parentPathnameBase === "/"
? pathname
: pathname.slice(parentPathnameBase.length) || "/";
// 匹配当前路径,注意是移除了 parentPathname 的相关路径后的匹配
// 通过传入的 routes 配置项与当前的路径,匹配对应渲染的路由
let matches = matchRoutes(routes, { pathname: remainingPathname });
// 参数为当前匹配到的 matches 路由数组和外层 useRoutes 的 matches 路由数组
// 返回的是 React.Element,渲染所有的 matches 对象
return _renderMatches(
// 没有 matches 会返回 null
matches &&
matches.map(match =>
// 合并外层调用 useRoutes 得到的参数,内部的 Route 会有外层 Route(其实这也叫父 Route) 的所有匹配属性。
Object.assign({}, match, {
params: Object.assign({}, parentParams, match.params),
// joinPaths 函数用于合并字符串
pathname: joinPaths([parentPathnameBase, match.pathname]),
pathnameBase:
match.pathnameBase === "/"
? parentPathnameBase
: joinPaths([parentPathnameBase, match.pathnameBase])
})
),
// 外层 parentMatches 部分,最后会一起加入最终 matches 参数中
parentMatches
);
}
/**
* 将多个 path 合并为一个
* @param paths path 数组
* @returns
*/
const joinPaths = (paths: string[]): string =>
paths.join("/").replace(///+/g, "/");
总结:
- 获取上下文中调用useRoutes后的信息,如果证明此次调用时作为子路由使用的,需要合并父路由的匹配信息;
- 移除父路由已经匹配完毕的pathname前缀后,调用matchRoutes与当前传入的routes配置相匹配,返回匹配到的matches数组;
- 调用_renderMathces方法,渲染上一步得到的matches数组;
也就对应着:路由上下文解析阶段,路由匹配阶段(matchRoutes),路由渲染阶段(_renderMatches)
- matchRoutes
/**
* 通过 routes 与 location 得到 matches 数组
*/
export function matchRoutes(
// 用户传入的 routes 对象
routes: RouteObject[],
// 当前匹配到的 location,注意这在 useRoutes 内部是先有过处理的
locationArg: Partial<Location> | string,
// 这个参数在 useRoutes 内部是没有用到的,但是该方法是对外暴露的,用户可以使用这个参数来添加统一的路径前缀
basename = "/"
): RouteMatch[] | null {
// 先格式化为 Path 对象
let location =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
// 之前提到过,抽离 basename,获取纯粹的 pathname
let pathname = stripBasename(location.pathname || "/", basename);
// basename 匹配失败,返回 null
if (pathname == null) {
return null;
}
// 1.扁平化 routes,将树状的 routes 对象根据 path 扁平为一维数组,同时包含当前路由的权重值
let branches = flattenRoutes(routes);
// 2.传入扁平化后的数组,根据内部匹配到的权重排序
rankRouteBranches(branches);
let matches = null;
// 3.这里就是权重比较完成后的解析顺序,权重高的在前面,先进行匹配,然后是权重低的匹配
// branches 中有一个匹配到了就终止循环,或者全都没有匹配到
for (let i = 0; matches == null && i < branches.length; ++i) {
// 遍历扁平化的 routes,查看每个 branch 的路径匹配规则是否能匹配到 pathname
matches = matchRouteBranch(branches[i], pathname);
}
return matches;
}
主要方面:
- flattenRoutes:扁平化
- rankRouteBranches:排序
- matchRouteBranch:路由匹配
- flattenRoutes:将树形结构转为一维数组
// 保存在 branch 中的路由信息,后续路由匹配时会用到
interface RouteMeta {
/**
* 路由的相对路径(刨除与父路由重复部分)
*/
relativePath: string;
caseSensitive: boolean;
/**
* 用户在 routes 数组中定义的索引位置(相对其兄弟 route 而言)
*/
childrenIndex: number;
route: RouteObject;
}
// 扁平化的路由对象,包含当前路由对象对应的完整 path,权重得分与用于匹配的路由信息
interface RouteBranch {
/**
* 完整的 path(合并了父路由的,下面会引入相对路由的概念)
*/
path: string;
/**
* 权重,用于排序
*/
score: number;
/**
* 路径 meta,依次为从父级到子级的路径规则,最后一个是路由自己
*/
routesMeta: RouteMeta[];
}
/**
* 扁平化路由,会将所有路由扁平为一个数组,用于比较权重
* @param routes 第一次在外部调用只需要传入该值,用于转换的 routes 数组
* @param branches
* @param parentsMeta
* @param parentPath
* @returns
*/
function flattenRoutes(
routes: RouteObject[],
// 除了 routes,下面三个都是递归的时候使用的
branches: RouteBranch[] = [],
parentsMeta: RouteMeta[] = [],
parentPath = ""
): RouteBranch[] {
routes.forEach((route, index) => {
// 当前 branch 管理的 route meta
let meta: RouteMeta = {
// 只保存相对路径,这里的值下面会进行处理
relativePath: route.path || "",
caseSensitive: route.caseSensitive === true,
// index 是用户给出的 routes 顺序,会一定程度影响 branch 的排序(当为同一层级 route 时)
childrenIndex: index,
// 当前 route 对象
route
};
// 如果 route 以 / 开头,那么它应该完全包含父 route 的 path,否则报错
if (meta.relativePath.startsWith("/")) {
invariant(
meta.relativePath.startsWith(parentPath),
`Absolute route path "${meta.relativePath}" nested under path ` +
`"${parentPath}" is not valid. An absolute child route path ` +
`must start with the combined path of all its parent routes.`
);
// 把父路由前缀去除,只要相对路径
meta.relativePath = meta.relativePath.slice(parentPath.length);
}
// 完整的 path,合并了父路由的 path
let path = joinPaths([parentPath, meta.relativePath]);
// 第一次使用 parentsMeta 为空数组,从外到内依次推入 meta 到该数组中
let routesMeta = parentsMeta.concat(meta);
// 开始递归
if (route.children && route.children.length > 0) {
// 如果是 index route,报错,因为 index route 不能有 children
invariant(
route.index !== true,
`Index routes must not have child routes. Please remove ` +
`all child routes from route path "${path}".`
);
flattenRoutes(route.children, branches, routesMeta, path);
}
// 没有路径的路由(之前提到过的布局路由)不参与路由匹配,除非它是索引路由
/*
注意:递归是在前面进行的,也就是说布局路由的子路由是会参与匹配的
而子路由会有布局路由的路由信息,这也是布局路由能正常渲染的原因。
*/
if (route.path == null && !route.index) {
return;
}
// routesMeta,包含父 route 到自己的全部 meta 信息
// computeScore 是计算权值的方法,我们后面再说
branches.push({ path, score: computeScore(path, route.index), routesMeta });
});
return branches;
}
- rankRouteBranches
// 动态路由权重,比如 /foo/:id
const dynamicSegmentValue = 3;
// 索引路由权重,也就是加了 index 为 true 属性的路由
const indexRouteValue = 2;
// 空路由权重,当一段路径值为空时匹配,只有最后的路径以 / 结尾才会用到它
const emptySegmentValue = 1;
// 静态路由权重
const staticSegmentValue = 10;
// 路由通配符权重,为负的,代表当我们写 * 时实际会降低权重
const splatPenalty = -2;
// 判断是否有动态参数,比如 :id 等
const paramRe = /^:\w+$/;
// 判断是否为 *
const isSplat = (s: string) => s === "*";
/**
* 计算路由权值,根据权值大小匹配路由
* 静态值 > params 动态参数
* @param path 完整的路由路径,不是相对路径
* @param index
* @returns
*/
function computeScore(path: string, index: boolean | undefined): number {
let segments = path.split("/");
// 初始化权重值,有几段路径就是几,路径多的初始权值高
let initialScore = segments.length;
// 有一个 * 权重减 2
if (segments.some(isSplat)) {
initialScore += splatPenalty;
}
// 用户传了 index,index 是布尔值,代表 IndexRouter,权重 +2
if (index) {
initialScore += indexRouteValue;
}
// 在过滤出非 * 的部分
return segments
.filter(s => !isSplat(s))
.reduce(
(score, segment) =>
score +
// 如果有动态参数
(paramRe.test(segment)
? // 动态参数权重 3
dynamicSegmentValue
: segment === ""
? // 空值权重为 1,这个其实只有一种情况,path 最后面多一个 /,比如 /foo 与 /foo/ 的区别
emptySegmentValue
: // 静态值权重最高为 10
staticSegmentValue),
initialScore
);
}
/**
* 排序,比较权重值
* @param branches
*/
function rankRouteBranches(branches: RouteBranch[]): void {
branches.sort((a, b) =>
a.score !== b.score
// 排序,权值大的在前面
? b.score - a.score
: // 如果 a.score === b.score
compareIndexes(
// routesMeta 是一个从最外层路由到子路由的数组
// childrenIndex 是按照 routes 中 route 传入的顺序传值的,写在后面的 index 更大(注意是同级)
a.routesMeta.map(meta => meta.childrenIndex),
b.routesMeta.map(meta => meta.childrenIndex)
)
);
}
/**
* 比较子 route 的 index,判断是否为兄弟 route,如果不是则返回 0,比较没有意义,不做任何操作
* @param a
* @param b
* @returns
*/
function compareIndexes(a: number[], b: number[]): number {
// 是否为兄弟 route
let siblings =
// 这里是比较除了最后一个 route 的 path,需要全部一致才是兄弟 route
a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
return siblings
?
// 如果是兄弟节点,按照传入的顺序排序 a.length - 1 和 b.length - 1 是相等的,只是内部的值不同
a[a.length - 1] - b[b.length - 1]
:
// 只比较兄弟节点,如果不是兄弟节点,则权重相同
0;
}
- matchRouteBranch
/**
* 通过 branch 和当前的 pathname 得到真正的 matches 数组
* @param branch
* @param routesArg
* @param pathname
* @returns
*/
function matchRouteBranch<ParamKey extends string = string>(
branch: RouteBranch,
pathname: string
): RouteMatch<ParamKey>[] | null {
let { routesMeta } = branch;
// 初始化匹配到的值
let matchedParams = {};
let matchedPathname = "/";
// 最终的 matches 数组
let matches: RouteMatch[] = [];
// 遍历 routesMeta 数组,最后一项是自己的 route,前面是 parentRoute
for (let i = 0; i < routesMeta.length; ++i) {
let meta = routesMeta[i];
// 是否为最后一个 route
let end = i === routesMeta.length - 1;
// pathname 匹配过父 route 后的剩余的路径名
let remainingPathname =
matchedPathname === "/"
? pathname
: pathname.slice(matchedPathname.length) || "/";
// 使用的相对路径规则匹配剩余的值
let match = matchPath(
// 在匹配时只有最后一个 route 的 end 才会是 true,其余都是 false,这里的 end 意味路径最末尾的 /
{ path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
remainingPathname
);
// 没匹配上,直接返回 null,整个 route 都匹配失败
if (!match) return null;
// 匹配上了合并 params,注意这里是改变的 matchedParams,所以所有 route 的 params 都是同一个
Object.assign(matchedParams, match.params);
let route = meta.route;
// 匹配上了就把路径再补全
matches.push({
params: matchedParams,
pathname: joinPaths([matchedPathname, match.pathname]),
pathnameBase: joinPaths([matchedPathname, match.pathnameBase]),
route
});
// 更改 matchedPathname,已经匹配上的 pathname 前缀,用作后续子 route 的循环
if (match.pathnameBase !== "/") {
matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
}
}
return matches;
}
- _renderMatches
/**
* 其实就是渲染 RouteContext.Provider 组件(包括多个嵌套的 Provider)
*/
function _renderMatches(
matches: RouteMatch[] | null,
// 如果在已有 match 的 route 内部调用,会合并父 context 的 match
parentMatches: RouteMatch[] = []
): React.ReactElement | null {
if (matches == null) return null;
// 生成 outlet 组件,注意这里是从后往前 reduce,所以索引在前的 match 是最外层,也就是父路由对应的 match 是最外层
/**
* 可以看到 outlet 是通过不断递归生成的组件,最外层的 outlet 递归层数最多,包含有所有的内层组件,
* 所以我们在外层使用的 <Outlet /> 是包含有所有子组件的聚合组件
* */
return matches.reduceRight((outlet, match, index) => {
return (
<RouteContext.Provider
// 如果有 element 就渲染 element,如果没有填写 element,则默认是 <Outlet />,继续渲染内嵌的 <Route />
children={
match.route.element !== undefined ? match.route.element : <Outlet />
}
// 代表当前 RouteContext 匹配到的值,matches 并不是全局状态一致的,会根据层级不同展示不同的值,最后一个层级是完全的 matches,这也是之前提到过的不要在外部使用 RouteContext 的原因
value={{
outlet,
matches: parentMatches.concat(matches.slice(0, index + 1))
}}
/>
);
// 最内层的 outlet 为 null,也就是最后的子路由
}, null as React.ReactElement | null);
}
- 总结
-
- useRoutes是react-router中的核心,用户不管是直接使用useRoutes还是用Routes与Route组件结合最终都会转换为它。
- useRoutes在上下文解析阶段会解析在外层是否已经调用过useRoutes,如果调用过会先获取外层的上下文数据,最后将外层数据与用户传入的routes数组结合,生成最终结果;
- useRoutes在匹配阶段会将传入的routes与当前的location(可手动传入,但内部会做校验)做一层匹配,通过对route中声明的path的权重计算,拿到当前pathname所能匹配到的最佳matches数组,索引从小到大层数关系从到外到内;
- useRoutes在渲染阶段会将matches数组渲染为一个聚合的React Element,该元素整体是许多RouteContext.Provider的嵌套,从外到内依次是【父 => 子 => 孙子】这样的关系,每个Provider包含两个值,与该级别对应的matches数组(最后的元素是该级别的route自身)与outlet元素,outlet元素就是嵌套RouteContext.Provider 存放的地方,每个RouteContext.Provider的children就是route的element属性;
- 每次使用outlet实际上都是渲染的内置的路由关系(如果当前route没有element属性,则默认渲染outlet,这也是为什么可以直接写不带element组件嵌套的原因),我们可以在当前级别route的element中任意地方使用outlet来渲染子路由;
1.5 Navigate
// useNavigate 返回的 navigate 函数定义,可以传入 to 或者传入数字控制浏览器页面栈的显示
export interface NavigateFunction {
(to: To, options?: NavigateOptions): void;
(delta: number): void;
}
export interface NavigateOptions {
// 是否替换当前栈
replace?: boolean;
// 当前导航的 state
state?: any;
}
/**
* 返回的 navigate 函数可以传和文件夹相同的路径规则
*/
export function useNavigate(): NavigateFunction {
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useNavigate() may be used only in the context of a <Router> component.`
);
// Router 提供的 navigator,本质是 history 对象
let { basename, navigator } = React.useContext(NavigationContext);
// 当前路由层级的 matches 对象(我们在前面说了,不同的 RouteContext.Provider 层级不同该值不同)
let { matches } = React.useContext(RouteContext);
let { pathname: locationPathname } = useLocation();
// 依次匹配到的子路由之前的路径(/* 之前)
let routePathnamesJson = JSON.stringify(
matches.map(match => match.pathnameBase)
);
// 是否已经初始化完毕(useEffect),这里是要让页面不要在一渲染的时候就跳转,应该在 useEffect 后才能跳转,也就是说如果一渲染就要跳转页面应该写在 useEffect 中
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
);
// 有 basename,加上 basename
if (basename !== "/") {
path.pathname = joinPaths([basename, path.pathname]);
}
(!!options.replace ? navigator.replace : navigator.push)(
path,
options.state
);
},
[basename, navigator, routePathnamesJson, locationPathname]
);
return navigate;
}
import type { To } from 'history';
export interface NavigateProps {
// To 从 history 中引入
/*
export declare type To = string | PartialPath;
*/
to: To;
replace?: boolean;
state?: any;
}
/**
* 组件式导航,当页面渲染后立刻调用 navigate 方法,很简单的封装
*/
export function Navigate({ to, replace, state }: NavigateProps): null {
// 必须在 Router 上下文中
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of
// the router loaded. We can help them understand how to avoid that.
`<Navigate> may be used only in the context of a <Router> component.`
);
let navigate = useNavigate();
React.useEffect(() => {
navigate(to, { replace, state });
});
return null;
}
- 总结:navigate内部还是调用的useNavigate,而useNavigate内部则是对用户传入的路径做处理,获取到最终的路径值,再传递给NavigationContext提供navigator对象;
2. react-router-dom
这里主要介绍在react-router-dom中引用的BrowserRouter、hashRouter以及historyRouter
BrowserRouter 和 HashRouter的区别,是区分链接还是hash,从history库中取到
import { createBrowserHistory, createHashHistory } from "history";
2.1 BrowserRouter
export interface BrowserRouterProps {
basename?: string;
children?: React.ReactNode;
window?: Window;
}
/**
* A `<Router>` for use in web browsers. Provides the cleanest URLs.
*/
export function BrowserRouter({
basename,
children,
window,
}: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
historyRef.current = createBrowserHistory({ 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}
/>
);
}
2.2 hashRouter
export interface HashRouterProps {
basename?: string;
children?: React.ReactNode;
window?: Window;
}
/**
* A `<Router>` for use in web browsers. Stores the location in the hash
* portion of the URL so it is not sent to the server.
*/
export function HashRouter({ basename, children, window }: HashRouterProps) {
let historyRef = React.useRef<HashHistory>();
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}
/>
);
}
2.3 HistoryRouter
export interface HistoryRouterProps {
basename?: string;
children?: React.ReactNode;
history: History;
}
/**
* A `<Router>` that accepts a pre-instantiated history object. It's important
* to note that using your own history object is highly discouraged and may add
* two versions of the history library to your bundles unless you use the same
* version of the history library that React Router uses internally.
*/
function HistoryRouter({ basename, children, history }: HistoryRouterProps) {
const [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}
/>
);
}
if (__DEV__) {
HistoryRouter.displayName = "unstable_HistoryRouter";
}
export { HistoryRouter as unstable_HistoryRouter };