React-Router v6
Browser路由
先看官方例子:
-
App.tsx
export default function App() {
return (
<div>
<h1>Basic Example</h1>
<p>
This example demonstrates some of the core features of React Router
including nested <code><Route></code>s,{" "}
<code><Outlet></code>s, <code><Link></code>s, and using a
"*" route (aka "splat route") to render a "not found" page when someone
visits an unrecognized URL.
</p>
{/* 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>
);
}
-
main.tsx
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
可以看到 BrowserRouter是入口,那么就先从BrowserRouter开始看
BrowserRouter
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,
});
// 当history更改时重新监听
// 当切换路由时会触发setState
React.useLayoutEffect(() => history.listen(setState), [history]);
return (
<Router
basename={basename}
children={children}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
}
很短很好理解,主要就是通过 history库中 createBrowserHistory函数创建history实例,然后通过historyRef保存,返回Router组件
接下来看 Router组件
Router
packages/react-router/lib/components.tsx#L172
// line:172
export function Router({
basename: basenameProp = "/",
children = null,
location: locationProp,
navigationType = NavigationType.Pop,
navigator,
static: staticProp = false,
}: RouterProps): React.ReactElement | null {
invariant(
// 不能嵌套<Router>
!useInRouterContext(),
`You cannot render a <Router> inside another <Router>.` +
` You should never have more than one in your app.`
);
// 规范basename
let basename = normalizePathname(basenameProp);
// 设置navigationContext
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(() => {
// 将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]);
warning(
location != null,
`<Router basename="${basename}"> is not able to match the URL ` +
`"${pathname}${search}${hash}" because it does not start with the ` +
`basename, so the <Router> won't render anything.`
);
if (location == null) {
// 因为pathname没有以basename开头,所以无法匹配,不会渲染任何东西
return null;
}
return (
// 一共注入两个context——NavigationContext和LocationContext
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
);
}
-
normalizePathname:
const normalizePathname = (pathname: string): string =>
pathname.replace(/\/+$/, "").replace(/^\/*/, "/");
// '/a/b/c' => '/a/b/c'
// '/a/b/c/' => '/a/b/c'
// 'a/b/c////' => '/a/b/c'
// 给第一位的零或多个'/'替换成'/' ,去掉最后一位的多个'/'
-
parsePath:
let pathPieces = parsePath("/the/path?the=query#the-hash");
// pathPieces = {
// pathname: '/the/path',
// search: '?the=query',
// hash: '#the-hash'
// }
-
stripBasename
export function stripBasename(
pathname: string,
basename: string
): string | null {
if (basename === "/") return pathname;
if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
// 判断是否以basename开头
return null;
}
let nextChar = pathname.charAt(basename.length);
if (nextChar && nextChar !== "/") {
// pathname不能以 basename/ 开头
return null;
}
// 截取basename后面一段
return pathname.slice(basename.length) || "/";
}
Router组件同样很好理解,注入navigationContext,对location做处理,之后当做context注入。相当于一个外层容器,初始化好一些数据供内部组件使用。<Router>它是一个上下文提供者,为应用程序的其余部分提供路由信息。
接下来我们来看 Routes组件
Routes
packages/react-router/lib/components.tsx#L252
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
return useRoutes(createRoutesFromChildren(children), location);
}
很短很简单,主要是引用了两个函数,使用useRoutes这个hooks来渲染,通过createRoutesFromChildren生成hooks接受的routes数据结构
createRoutesFromChildren
packages/react-router/lib/components.tsx#L270
export function createRoutesFromChildren(
children: React.ReactNode
): RouteObject[] {
let routes: RouteObject[] = [];
React.Children.forEach(children, (element) => {
if (!React.isValidElement(element)) {
// 忽略非有效元素。可以更容易地在路由配置中内联条件。
return;
}
if (element.type === React.Fragment) {
// 支持展开Fragment
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,
};
// 如果有children就递归
if (element.props.children) {
route.children = createRoutesFromChildren(element.props.children);
}
routes.push(route);
});
// 最后返回出这个数组
return routes;
}
就是一个遍历的过程,很简单。最后返回的是一个数组,符合useRoutes第一个参数数据结构的数组
useRoutes
packages/react-router/lib/hooks.tsx#L266
export function useRoutes(
routes: RouteObject[],
locationArg?: Partial<Location> | string
): React.ReactElement | null {
invariant(
// 首先检测是否在LocationContext.provider下
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.`
);
// 获取RouteContext,因为上文并没有RouteContext,
// 所以可以看出Routes和Route都是是可以嵌套的
let { matches: parentMatches } = React.useContext(RouteContext);
// 获取matches最后一个,获取对应参数
let routeMatch = parentMatches[parentMatches.length - 1];
let parentParams = routeMatch ? routeMatch.params : {};
let parentPathname = routeMatch ? routeMatch.pathname : "/";
let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
let parentRoute = routeMatch && routeMatch.route;
// 相当于 locationFromContext = React.useContext(LocationContext).location;
let locationFromContext = useLocation();
let location;
if (locationArg) {
// 这个操作跟Router中的location处理类似
let parsedLocationArg =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
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 {
// 如果没传这个参数就默认取LocationContext中de
location = locationFromContext;
}
let pathname = location.pathname || "/";
// 获取剩余pathname
let remainingPathname =
parentPathnameBase === "/"
? pathname
// 截取从父路径开始的后半段pathname
: pathname.slice(parentPathnameBase.length) || "/";
// matchRoutes 路由匹配的核心算法
let matches = matchRoutes(routes, { pathname: remainingPathname });
// 渲染函数
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
);
}
-
RouteContext
export const RouteContext = React.createContext<RouteContextObject>({
outlet: null,
matches: [],
});
matchRoutes
packages/react-router/lib/router.ts#L141
matchRoutes针对给定的一组路由运行路由匹配算法,location以查看哪些路由(如果有)匹配。如果找到匹配项,RouteMatch则返回一个数组,每个匹配的路由对应一个对象。
这是 React-Router 匹配算法的核心。useRoutes在内部使用它来确定哪些路径与当前位置匹配。在想要手动匹配一组路由的某些情况下,它也很有用。
export function matchRoutes(
routes: RouteObject[],
locationArg: Partial<Location> | string,
basename = "/"
): RouteMatch[] | null {
// 相同的操作上文已有叙述
let location =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
let pathname = stripBasename(location.pathname || "/", basename);
if (pathname == null) {
return null;
}
// routes数组拍平
let branches = flattenRoutes(routes);
// 降序排列
rankRouteBranches(branches);
let matches = null;
for (let i = 0; matches == null && i < branches.length; ++i) {
// 注意这里的 matches==null
// 只要其中一个未匹配到则进入下一轮循环,匹配到则结束循环
matches = matchRouteBranch(branches[i], pathname);
}
return matches;
}
-
rankRouteBranches
function rankRouteBranches(branches: RouteBranch[]): void {
// 降序排列
branches.sort((a, b) =>
a.score !== b.score
? b.score - a.score // 分数高的优先
: compareIndexes(
a.routesMeta.map((meta) => meta.childrenIndex),
b.routesMeta.map((meta) => meta.childrenIndex)
)
);
}
-
compareIndexes
function compareIndexes(a, b) {
// 判断是否是同级
let siblings =
a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
return siblings
? // 同级则判断最后一位
a[a.length - 1] - b[b.length - 1]
: // 否则,按索引对非同级进行排序是没有意义的,所以它们的排序是相等的
0;
}
-
只看代码可能不太直观,举个例子
{
path: "/courses",
element: " <Courses />",
children: [
// 同级
// 上文 a[a.length - 1] - b[b.length - 1] 指的是
// childrenIndex的对比 ,/a index为0 /b index为1
{ path: "/courses/a", element: "<Course />" },
{ path: "/courses/b", element: "<Course />" },
],
},
flattenRoutes
packages/react-router/lib/router.ts#L179
关键部分
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("/")) {
// 如果路径以 / 开头并且没有包含父路径则报错
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.`
);
// 将父路径截掉,保留后半段
// 例1
meta.relativePath = meta.relativePath.slice(parentPath.length);
}
// 与父路径合并路径
let path = joinPaths([parentPath, meta.relativePath]);
// 类似于把meta push到了parentsMeta数组,但是不会改变原数组(parentsMeta)
let routesMeta = parentsMeta.concat(meta);
// Add the children before adding this route to the array so we traverse the
// route tree depth-first and child routes appear before their parents in
// the "flattened" version.
if (route.children && route.children.length > 0) {
// 索引路由,也称默认子路由,该路由不能有子路由
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;
}
branches.push({ path, score: computeScore(path, route.index), routesMeta });
});
return branches;
}
-
例1
{
path:'/', // 父路径为'',相当于 '/'.slice(0) => '/'
children:[
{
path:'/a' // => a
}
]
}
// 如果子路径带/ 则要写父路径
-
joinPaths
export const joinPaths = (paths: string[]): string =>
// 路径通过/拼接,并且将多个 // 替换为 /
paths.join("/").replace(/\/\/+/g, "/");
-
computeScore
packages/react-router/lib/router.ts#L251
const paramRe = /^:\w+$/;
const dynamicSegmentValue = 3;
const indexRouteValue = 2;
const emptySegmentValue = 1;
const staticSegmentValue = 10;
const splatPenalty = -2;
const isSplat = (s: string) => s === "*";
function computeScore(path: string, index: boolean | undefined): number {
let segments = path.split("/");
// 初始分数为路径深度
let initialScore = segments.length;
if (segments.some(isSplat)) {
// 只要有*,则
initialScore += splatPenalty;
}
if (index) {
// 如果是索引路由
initialScore += indexRouteValue;
}
return segments
// 挑出不带星号的
.filter((s) => !isSplat(s))
// 计算分数
.reduce(
(score, segment) =>
score +
// 动态路径,例如: /:id
(paramRe.test(segment)
? dynamicSegmentValue
: segment === ""
// 空字符串
? emptySegmentValue
: staticSegmentValue),
initialScore
);
}
-
例如:
path='/' // => 分数:4 segments=['','']
path='/abc' // => 分数:13 segments=['','abc']
-
举个例子:
let routes = [
{
path: "/",
element:' <Layout />',// branches第6次push
children: [
{ index: true, element: '<Home />' }, // branches第1次push 因为是索引路由,所以score多加了2
{
path: "/courses",
element:' <Courses />',// branches第4次push
children: [
{ index: true, element: '<CoursesIndex />' }, // branches第2次push
{ path: "/courses/:id", element: '<Course />' },// branches第3次push
],
},
{ path: "*", element: '<NoMatch />' },// branches第5次push
],
},
];
flattenRoutes(routes)
输出结果

排序之后

routesMeta为父-子的顺序,在后面匹配时会遍历 routesMeta
matchRouteBranch
packages/react-router/lib/router.ts#L291
查找匹配的路由分支
function matchRouteBranch<ParamKey extends string = string>(
branch: RouteBranch,
pathname: string
): RouteMatch<ParamKey>[] | null {
let { routesMeta } = branch;
let matchedParams = {};
let matchedPathname = "/";
let matches: RouteMatch[] = [];
for (let i = 0; i < routesMeta.length; ++i) {
// 一层层路径遍历匹配
let meta = routesMeta[i];
let end = i === routesMeta.length - 1;
// 剩余路径
let remainingPathname =
// 匹配路径
// pathname = `${matchedPathname}xxx`,remainingPathname = 'xxx'
matchedPathname === "/"
? pathname
: pathname.slice(matchedPathname.length) || "/";
let match = matchPath(
{ path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
remainingPathname
);
// 只要有一层未匹配到则跳出当前循环,routesMeta是一个数组,从0到最后一项顺序为父->子
if (!match) return null;
Object.assign(matchedParams, match.params);
let route = meta.route;
matches.push({
params: matchedParams,
pathname: joinPaths([matchedPathname, match.pathname]),
pathnameBase: normalizePathname(
joinPaths([matchedPathname, match.pathnameBase])
),
route,
});
if (match.pathnameBase !== "/") {
// 匹配到对应层级的路径之后,要把这个层级算上,以便下次循环匹配remainingPathname
// 例如 /a/b/c matchedPathname=> / => /a => /a/b => /a/b/c
matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
}
}
return matches;
}
matchPath
packages/react-router/lib/router.ts#L388
matchPath将路由路径与 URL 路径名匹配并返回有关匹配的信息。当您需要手动运行路由器的匹配算法以确定路由路径是否匹配时,这很有用。如果模式与给定的路径名不匹配,则返回 null
useMatch钩子在内部使用此函数来匹配相对于当前位置的路由路径。
export function matchPath<
ParamKey extends ParamParseKey<Path>,
Path extends string
>(
pattern: PathPattern<Path> | Path,
pathname: string
): PathMatch<ParamKey> | null {
if (typeof pattern === "string") {
pattern = { path: pattern, caseSensitive: false, end: true };
}
// 编译路径,根据对应的路径生成对应的匹配正则
let [matcher, paramNames] = compilePath(
pattern.path,
pattern.caseSensitive,
pattern.end
);
// 与当前路径匹配
let match = pathname.match(matcher);
// 没匹配到不作处理
if (!match) return null;
let matchedPathname = match[0];
// 其实就是去掉结尾任意数量的 /
let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
// 捕获组 其实就是参数(动态参数或者*)
let captureGroups = match.slice(1);
let params: Params = paramNames.reduce<Mutable<Params>>(
(memo, paramName, index) => {
// 直接根据index在捕获组中匹配,参数有可能被编码,所以先decode
if (paramName === "*") {
let splatValue = captureGroups[index] || "";
// 去掉参数后的路径
pathnameBase = matchedPathname
.slice(0, matchedPathname.length - splatValue.length)
.replace(/(.)\/+$/, "$1");
}
// 解码
memo[paramName] = safelyDecodeURIComponent(
captureGroups[index] || "",
paramName
);
return memo;
},
{}
);
return {
params,
pathname: matchedPathname,
pathnameBase,
pattern,
};
}
_renderMatches
packages/react-router/lib/hooks.tsx#L377
export function _renderMatches(
matches: RouteMatch[] | null,
parentMatches: RouteMatch[] = []
): React.ReactElement | null {
if (matches == null) return null;
return matches.reduceRight((outlet, match, index) => {
// 使用reduceRight,遍历渲染
return (
<RouteContext.Provider
children={
match.route.element !== undefined ? match.route.element : outlet
}
value={{
outlet,
matches: parentMatches.concat(matches.slice(0, index + 1)),
}}
/>
);
}, null as React.ReactElement | null);
}
React-Router v6
Browser路由
先看官方例子:
App.tsx
main.tsx
可以看到
BrowserRouter是入口,那么就先从BrowserRouter开始看BrowserRouter
很短很好理解,主要就是通过
history库中createBrowserHistory函数创建history实例,然后通过historyRef保存,返回Router组件接下来看
Router组件Router
normalizePathname:
parsePath:
stripBasename
Router组件同样很好理解,注入navigationContext,对location做处理,之后当做context注入。相当于一个外层容器,初始化好一些数据供内部组件使用。<Router>它是一个上下文提供者,为应用程序的其余部分提供路由信息。接下来我们来看
Routes组件Routes
很短很简单,主要是引用了两个函数,使用
useRoutes这个hooks来渲染,通过createRoutesFromChildren生成hooks接受的routes数据结构createRoutesFromChildren
就是一个遍历的过程,很简单。最后返回的是一个数组,符合
useRoutes第一个参数数据结构的数组useRoutes
RouteContext
matchRoutes
matchRoutes针对给定的一组路由运行路由匹配算法,location以查看哪些路由(如果有)匹配。如果找到匹配项,RouteMatch则返回一个数组,每个匹配的路由对应一个对象。这是
React-Router匹配算法的核心。useRoutes在内部使用它来确定哪些路径与当前位置匹配。在想要手动匹配一组路由的某些情况下,它也很有用。rankRouteBranches
compareIndexes
只看代码可能不太直观,举个例子
flattenRoutes
例1
joinPaths
computeScore
例如:
举个例子:
输出结果
排序之后
routesMeta为父-子的顺序,在后面匹配时会遍历routesMetamatchRouteBranch
查找匹配的路由分支
matchPath
matchPath将路由路径与URL路径名匹配并返回有关匹配的信息。当您需要手动运行路由器的匹配算法以确定路由路径是否匹配时,这很有用。如果模式与给定的路径名不匹配,则返回nulluseMatch钩子在内部使用此函数来匹配相对于当前位置的路由路径。conpilePath
全是正则匹配,不了解的可以先恶补下正则,主要有这几种
(?:)(?=)\b_renderMatches
为什么使用
reduceRight,还是上面的例子,例如当前路由为/courses,传入的matches的数据结构为