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,
};
}
-
conpilePath
全是正则匹配,不了解的可以先恶补下正则,主要有这几种
(?:)(?=)\bfunction compilePath( path: string, caseSensitive = false, end = true ): [RegExp, string[]] { warning( // 例如 '/abc*' 路径被视为 '/abc/*' path === "*" || !path.endsWith("*") || path.endsWith("/*"), `Route path "${path}" will be treated as if it were ` + `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` + `always follow a \`/\` in the pattern. To get rid of this warning, ` + `please change the route path to "${path.replace(/\*$/, "/*")}".` ); let paramNames: string[] = []; let regexpSource = "^" + path // 0次或多次 / 和 零次或者一次 * .replace(/\/*\*?$/, "") // 忽略 / and /*, 我们将在下面处理它 .replace(/^\/*/, "/") // 确保最前面有 / .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape特殊正则字符 .replace(/:(\w+)/g, (_: string, paramName: string) => { // 例:/a/:id/ // paramName = id paramNames.push(paramName); // 将:id替换 return "([^\\/]+)"; }); if (path.endsWith("*")) { // 以*结尾 paramNames.push("*"); regexpSource += path === "*" || path === "/*" ? "(.*)$" // 已匹配首字母/,只匹配其余的 : "(?:\\/(.+)|\\/*)$"; // 不要在参数[“*”]中包含/ (?:) 非捕获组 } else { regexpSource += end ? "\\/*$" // 匹配到结尾时,忽略尾部斜杠 : //否则,请匹配单词边界。单词边界限制父路由仅匹配其自己的单词,仅此而已, //例如父路由“/home”不应匹配“/home2”。 // 此外,允许以“.`、`-`、` ~`、` ~`”开头的路径, // 和url编码的实体,但不使用匹配路径中的字符,以便它们可以与嵌套路径匹配。 // 例 可匹配 /abc/d%22 "(?:(?=[.~-]|%[0-9A-F]{2})|\\b|\\/|$)"; } // 创建正则 let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i"); return [matcher, paramNames]; }
_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);
}
- 为什么使用
reduceRight,还是上面的例子,例如当前路由为/courses,传入的matches的数据结构为 matches: parentMatches.concat(matches.slice(0, index + 1)),这句解释了上文useRoutes中let routeMatch = parentMatches[parentMatches.length - 1]为什么要取最后一位