本篇记录react-router6
的核心内容:从Router
到Routes
到Route
再到渲染为React组件的过程。
具体的记录都体现在代码的注释上,每一块代码前的记录为总结。
在此之前有必要先阅读history库的源码。
BrowserRouter
创建browserHistory对象,给Router组件提供来自该对象的值。然后在history.listen中更新所提供的值,触发路由组件的更新。
/**
* 用于Web浏览器的<Router>,提供最干净的URL。
*/
export function BrowserRouter({
basename,
children,
window,
}: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
// 可以保证在整个过程都使用同一个history
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}
/>
);
}
Router
计算出路由组件顶层上下文的内容
- 处理basename,生成navigationContext
- 解析history中的location,将其与basename进行一些对比处理,最后生成路由组件中的location,它与history的location的区别只在于pathname是去掉basename的。生成locationContext
/**
* 为应用程序的其余部分提供位置上下文。
* 注意:您通常不会直接渲染 <Router>,相反您将渲染一个
* 更特定于您的环境的路由器,例如 <BrowserRouter>
* 在 Web 浏览器或 <StaticRouter> 中用于服务器渲染。
*
* @see https://reactrouter.com/docs/en/v6/api#router
*/
export function Router({
basename: basenameProp = "/",
children = null,
location: locationProp,
navigationType = NavigationType.Pop,
navigator,
static: staticProp = false,
}: RouterProps): React.ReactElement | null {
invariant(
!useInRouterContext(),
`You cannot render a <Router> inside another <Router>.` +
` You should never have more than one in your app.`
);
// 标准化basename,例如//base转为/base,base转为/base
let basename = normalizePathname(basenameProp);
// navigationContext内容
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
// parsePath来自history库
// 将字符串URL路径解析为其单独的由pathname、search和hash组成的对象
if (typeof locationProp === "string") {
locationProp = parsePath(locationProp);
}
let {
pathname = "/",
search = "",
hash = "",
state = null,
key = "default",
} = locationProp;
// location关于basename的一些判断
// 若当前location(来自history的)的pathname不以basename开头,返回null
// 若满足条件,则将新建的location的pathname置为去掉basename的部分
let location = React.useMemo(() => {
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) {
return null;
}
return (
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
);
}
Routes
匹配逻辑还是调用useRoutes
/**
* 渲染在<Route>嵌套树中与当前位置最匹配的的分支对应的element
* @see https://reactrouter.com/docs/en/v6/api#routes
*/
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
// createRoutesFromChildren将<Route />解析成js对象
return useRoutes(createRoutesFromChildren(children), location);
}
useRoutes
获取已经命中的路由组件的相关数据,计算出要使用的location对象,使用matchRoutes,传入所有的route配置和当前pathname去匹配命中的组件。然后渲染为React组件。
/**
* 返回与当前位置匹配的路由组件
* 准备好正确的上下文以呈现路由树的其余部分
* 树中的路由组件必须渲染一个 <Outlet> 来渲染其子路由的组件。
*
* @see https://reactrouter.com/docs/en/v6/api#useroutes
*/
export function useRoutes(
routes: RouteObject[],
locationArg?: Partial<Location> | string
): React.ReactElement | null {
// 取到已经命中的组件的相关数据
// 因为在已命中的路由组件中仍然可以使用<Routes />或useRoutes
let { matches: parentMatches } = React.useContext(RouteContext);
let routeMatch = parentMatches[parentMatches.length - 1];
let parentParams = routeMatch ? routeMatch.params : {};
let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
// 从LocationContext中拿到location
let locationFromContext = useLocation();
let location;
// 以下使用的location是来自与useRoutes传入的location
// 还是来自LocationContext的
// parsePath来自history库
if (locationArg) {
let parsedLocationArg =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
location = parsedLocationArg;
} else {
location = locationFromContext;
}
let pathname = location.pathname || "/";
// 截取出除去父组件路径的其余pathname
let remainingPathname =
parentPathnameBase === "/"
? pathname
: pathname.slice(parentPathnameBase.length) || "/";
// matchRoutes针对给定的一组路由匹配规则匹配命中的组件
// 这是react-router匹配算法的核心
// matches就是返回的命中的路由组件
let matches = matchRoutes(routes, { pathname: remainingPathname });
// 根据matches渲染组件
// 将父组件的params、pathname和pathnameBase拼到每个组件对应的地方
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
);
}
_renderMatches
将上面匹配到的matches渲染为React组件。
利用reduceRight的特性,因为此时的matches是排过序的,从0到n依次是从父到子再到孙。
这样就可以按照层级一层一层把组件放在对应父组件的outlet里,而且每一个组件都有一个独立的provider,所以每一个组件的outlet里都只存着自己的子组件。最后返回的就是最上层的命中组件。
这个结构供useOutlet使用,这也是新组件<Outlet />
的核心逻辑。
export function _renderMatches(
matches: RouteMatch[] | null,
parentMatches: RouteMatch[] = []
): React.ReactElement | null {
if (matches == null) return null;
return matches.reduceRight((outlet, match, index) => {
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);
}
matchRoutes
将routes转换成被拉平且排过序的分支列表,这个分支列表中的每一条分支都可能被命中渲染,最后在渲染时是以分支为单位渲染的。
因为存在渲染嵌套组件的情况,比如可能有父路由和子路由都被命中的情况。详情见flattenRoutes
形成分支列表之后,遍历去进行匹配
/**
* 将给定路由匹配到某个位置并返回匹配数据。
* @see https://reactrouter.com/docs/en/v6/api#matchroutes
*/
export function matchRoutes(
routes: RouteObject[],
locationArg: Partial<Location> | 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;
}
// 将routes的属性结构拉平,形成分支列表
// 每个分支都是可能渲染到的分支
// {path, score, routesMeta}
let branches = flattenRoutes(routes);
// 给拉平的数组根据score和index排序(这两个指标有先后顺序)
rankRouteBranches(branches);
let matches = null;
for (let i = 0; matches == null && i < branches.length; ++i) {
// 去尝试匹配每一条分支
matches = matchRouteBranch(branches[i], pathname);
}
return matches;
}
flattenRoutes
分支列表就是routes配置分层级的处理,在routesMeta里存入其父route,并压入分支列表,子在父之前被压入。
例如:
// 模拟routes
{
path: 'parent',
...
children: {
path: 'son'
...
}
}
// 转换后的branches
[
{
path: 'son',
...
routesMeta: [
{
path: 'son'
...
},
{
path: 'parent',
...
}
],
},
{
path: 'parent',
...
routesMeta: [
{
path: 'parent',
...
}
]
}
]
换句话说,分支就是渲染时的渲染数组,比如上面当path: 'son’被命中时,routesMeta里的两个route都将被渲染为React组件。
每个分支中包含:
- path:当前分支命中的路径规则
- score:优先级
- routesMeta:命中后要渲染的所有组件数组,包含以下信息
- relativePath:相对路径
- caseSensitive:是否区分大小写
- childrenIndex:route的index
- route:就是routes配置中的route
function flattenRoutes(
routes: RouteObject[],
branches: RouteBranch[] = [],
parentsMeta: RouteMeta[] = [],
parentPath = ""
): RouteBranch[] {
routes.forEach((route, index) => {
// 先形成自己的meta
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.`
);
// 截掉父路径,计算出相对路径
meta.relativePath = meta.relativePath.slice(parentPath.length);
}
let path = joinPaths([parentPath, meta.relativePath]);
// routesMeta为父路由接上自己的meta
// 此行为的原因是若自己被命中,则说明父组件也被命中
// 渲染时根据此来渲染
let routesMeta = parentsMeta.concat(meta);
// 在将此路由添加到数组之前添加它的子路由
// 以便深度优先遍历路由树
// 子路由在扁平化后的数组中应出现在其父路由之前。
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;
}
// computeScore,计算每个分支的优先级
branches.push({ path, score: computeScore(path, route.index), routesMeta });
});
return branches;
}
computeScore
根据route的path和index来计算优先级,计算出的优先级为分支的优先级。
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;
// 有*时优先级-2
if (segments.some(isSplat)) {
initialScore += splatPenalty;
}
// 有index时说明为索引路由,优先级+2
if (index) {
initialScore += indexRouteValue;
}
// 遍历片段
// 1. 是:动态路由时+3
// 2. 是空时+1
// 3. 不是:也不是空时则是静态片段,+10
return segments
.filter((s) => !isSplat(s))
.reduce(
(score, segment) =>
score +
(paramRe.test(segment)
? dynamicSegmentValue
: segment === ""
? emptySegmentValue
: staticSegmentValue),
initialScore
);
}
rankRouteBranches
先根据score排序,高优先级在前面
当score相等时
- 如果两条路由是兄弟,我们应该首先尝试匹配较早的兄弟。这允许人们通过简单地将具有相同路径的路由按照他们希望尝试的顺序放置来对匹配行为进行细粒度控制。
- 否则,按索引对非兄弟姐妹进行排名实际上没有意义。因此它们排序相同。
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)
)
);
}
matchRouteBranch
遍历分支中的routesMeta,拿到分支的匹配规则和计算出当前的pathname,传给matchPath进行匹配。这里有一个不匹配时该分支就不被命中。
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;
// 剩余路径名,当前route路径名减去父route的路径
let remainingPathname =
matchedPathname === "/"
? pathname
: pathname.slice(matchedPathname.length) || "/";
// matchPath第一参数为route匹配规则,第二参数为当前实际路径名
let match = matchPath(
{ path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
remainingPathname
);
// 一个不匹配该分支就不被命中
if (!match) return null;
// 当前route的params要带有父route的params
Object.assign(matchedParams, match.params);
let route = meta.route;
// 计算出标准的的pathname和pathnameBase
// 就是拼接上面减掉的部分,再利用正则去掉多余的/等
matches.push({
params: matchedParams,
pathname: joinPaths([matchedPathname, match.pathname]),
pathnameBase: normalizePathname(
joinPaths([matchedPathname, match.pathnameBase])
),
route,
});
if (match.pathnameBase !== "/") {
matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
}
}
return matches;
}
matchPath
根据传入的规则去创建正则表达式,然后去匹配pathname,若没命中返回null,若命中命中后的路由信息,重点是pathname和params
/**
* 对URL路径名执行模式匹配并返回有关匹配的信息。
*
* @see https://reactrouter.com/docs/en/v6/api#matchpath
*/
export function matchPath<
ParamKey extends ParamParseKey<Path>,
Path extends string
>(
pattern: PathPattern<Path> | Path,
pathname: string
): PathMatch<ParamKey> | null {
// 从第一个参数名pattern也可以看出这就是路由的匹配规则
if (typeof pattern === "string") {
pattern = { path: pattern, caseSensitive: false, end: true };
}
// 将匹配规则转为匹配的正则表达式,paramNames为动态路由的params
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) => {
// 我们需要在此处使用原始splat值而不是稍后使用params["*"]来计算pathnameBase,因为它将被解码
if (paramName === "*") {
let splatValue = captureGroups[index] || "";
pathnameBase = matchedPathname
.slice(0, matchedPathname.length - splatValue.length)
.replace(/(.)\/+$/, "$1");
}
// safelyDecodeURIComponent解码
memo[paramName] = safelyDecodeURIComponent(
captureGroups[index] || "",
paramName
);
return memo;
},
{}
);
return {
params,
pathname: matchedPathname,
pathnameBase,
pattern,
};
}
compilePath
做两件事:
- 根据传入的规则生成正则表达式
- 在生成过程中碰到*或:的动态路由时记录params
function compilePath(
path: string,
caseSensitive = false,
end = true
): [RegExp, string[]] {
warning(
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
.replace(/\/*\*?$/, "") // 忽略尾随的 / 和 /*,将在下面处理它
.replace(/^\/*/, "/") // // 确保它有一个前导 /
.replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // 转义特殊的正则表达式字符
.replace(/:(\w+)/g, (_: string, paramName: string) => {
// 匹配到:动态路径时记录一下
paramNames.push(paramName);
return "([^\\/]+)";
});
if (path.endsWith("*")) {
// 匹配到*动态路径时记录一下
paramNames.push("*");
regexpSource +=
path === "*" || path === "/*"
? "(.*)$" // 已经匹配了最初的 /,只匹配其余的
: "(?:\\/(.+)|\\/*)$"; // 不要在 params["*"] 中包含 /
} else {
regexpSource += end
? "\\/*$" // 匹配到末尾时,忽略尾部斜杠
: // 否则,匹配单词边界或进行中的 /。
// 单词边界限制父路由只匹配他们自己的单词,仅此而已
// 例如 父路由“/home”不应匹配“/home2”。
// 此外,允许以 `.`、`-`、`~` 和 url 编码实体开头的路径
// 但不要使用匹配路径中的字符,以便它们可以匹配嵌套路径。
"(?:(?=[.~-]|%[0-9A-F]{2})|\\b|\\/|$)";
}
// 根据上面的规则形成正则表达式
let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
return [matcher, paramNames];
}
Outlet
用起来很像props.children
,逻辑还是调用useOutlet
/**
* 渲染子路由的元素,如果有的话。
*
* @see https://reactrouter.com/docs/en/v6/api#outlet
*/
export function Outlet(props: OutletProps): React.ReactElement | null {
return useOutlet(props.context);
}
useOutlet
上面所说,渲染时已将命中的子路由组件放在了context的outlet里,直接拿到渲染即可。
/**
* 返回此路由级别的子路由的元素
* <Outlet> 在内部使用以呈现子路由。
*
* @see https://reactrouter.com/docs/en/v6/api#useoutlet
*/
export function useOutlet(context?: unknown): React.ReactElement | null {
let outlet = React.useContext(RouteContext).outlet;
if (outlet) {
return (
<OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
);
}
return outlet;
}