react-routerbeta了两年半,出来了。 来吧,展示!!!
此次的变化,更新了一些内置组件,react组件和history包分开管理。交给了具体的平台去选择,只要实现具体的接口就好。比如单个使用Router
组件不行
一些变化:
- Route和lInk组件是在Routes相对的,由context实现。这使得代码更加精简。 路由的匹配是根据最佳匹配度来决定的,也就是
score
匹配度根据路由的嵌套层级,和index来计算 - history在reactRouter中移除,由navgigate代替,本质上还是history对象。
- route组件的三种渲染方式统一变为element。是ReactElement类型,直接传递props元信息更方便。
:id
动态参数路由*
通配符- 移除了正则匹配
- caseSensitive 区分大小写
- index 为主路由,如果为true,route组件不能存在children属性,index选项只是增加了匹配度(2)
- 支持嵌套路由, 增加
Outlet
组件,和同样效果的useOutlet
hook. outlet 也是基于context,会显示当前路由上下文下,与路由最匹配的组件 - 对于跳转路由相关,增加
useHref
命类似于cd
也是基于context, 一个.. 从当前路由向上取一位 - history由navigate替代,功能合并为一个函数
- 一个新的匹配规则
score
score的计算是根据当前路由的嵌套路由,和一些参数来决定的
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;
}
// index属性, 一个bool值, 会增加路由的匹配度
if (index) {
initialScore += indexRouteValue;
}
// 计算匹配度,根据我们的定义的路由(Route)组件计算所有的匹配度
return segments
.filter(s => !isSplat(s))
.reduce(
(score, segment) =>
score +
(paramRe.test(segment)
? dynamicSegmentValue
: segment === ""
? emptySegmentValue
: staticSegmentValue),
initialScore
);
}
score匹配度计算完毕以后,会把所有定义的路由信息数组拍平并排序
interface RouteMatch<ParamKey extends string = string> {
params: Params<ParamKey>;
pathname: string;
pathnameBase: string;
// route的porps
route: RouteObject;
}
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
};
// 连接父级路由, 收集路由下所有的routeMeta
let routesMeta = parentsMeta.concat(meta);
if (route.children && route.children.length > 0) {
// 递归展开所有子组件
flattenRoutes(route.children, branches, routesMeta, path);
}
if (route.path == null && !route.index) {
return;
}
branches.push({ path, score: computeScore(path, route.index), routesMeta });
});
return branches;
}
// 排序
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)
)
);
}
function matchRoutes(
routes: RouteObject[],
locationArg: Partial<Location> | string,
basename = "/"
): RouteMatch[] | null {
// ...
// 拍平所有定义的routes,也就是
/*
[
{
path:"home",
element:ReactElement,
children: [
// ... {}
]
},
{
path:"about",
element:ReactElement
}
]
*/
let branches = flattenRoutes(routes);
// 根据匹配度来排序
rankRouteBranches(branches);
let matches = null;
// 第一个branch匹配到以后停止循环
for (let i = 0; matches == null && i < branches.length; ++i) {
matches = matchRouteBranch(branches[i], routes, pathname);
}
return matches;
}
match
对定义的路由信息处理完成以后, 开始匹配路由,包括父级
// 这里通过没有给branch的routesMeta来决定是否能匹配到相应的pathname
// 如果路由信息存在一个没有匹配的就return -> null
// routesMeta最后一项是定义的route自己的路由信息,前面的都是父级路由信息
// 只有从头到尾的全部匹配,才能表示完整的路由信息
function matchRouteBranch<ParamKey extends string = string>(
branch: RouteBranch,
routesArg: RouteObject[],
pathname: string
): RouteMatch<ParamKey>[] | null {
let routes = routesArg;
// 取出定义的路由信息
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 =
matchedPathname === "/"
? pathname
: pathname.slice(matchedPathname.length) || "/";
// 这里根据页面的路由生成正则,并把url上的参数取处理
// 和匹配路由组件没关系
let match = matchPath(
{ path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
remainingPathname
);
// 存在不匹配项返回空
if (!match) return null;
Object.assign(matchedParams, match.params);
// 取出路由定义, childrenIndex 在拍平时会把路由的索引保存下来
let route = routes[meta.childrenIndex];
// 这里把所有的路由分别取出, 比如嵌套路由,会把本级路由下的路由全部取出
// /about
// /about/about2
// /about/about2/about3
// 但是每一个路由的参数会和页面的路由匹配,匹配参数
matches.push({
params: matchedParams,
pathname: joinPaths([matchedPathname, match.pathname]),
pathnameBase: joinPaths([matchedPathname, match.pathnameBase]),
route
});
if (match.pathnameBase !== "/") {
matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
}
routes = route.children!;
}
return matches;
}
function matchPath<ParamKey extends string = string>(
pattern: PathPattern | string,
pathname: string
): PathMatch<ParamKey> | null {
// init...
// 生成正则
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);
// 根据路由的定义,替换参数
// 比如 /about/:id -> about/123 & params = { id: 123 }
let params: Params = paramNames.reduce<Mutable<Params>>(
(memo, paramName, index) => {
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
};
}
// 这里会用到relativePath\caseSensitive\end.
// 根据这几个参数动态计算出相应的正则
// 同时会收集到路由上所有的参数名称
function compilePath(
path: string,
caseSensitive = false,
end = true
): [RegExp, string[]] {
let paramNames: string[] = [];
// 根据路由动态生成正则
let regexpSource =
"^" +
path
.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
.replace(/^\/*/, "/") // Make sure it has a leading /
.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
? "\\/*$" // When matching to the end, ignore trailing slashes
: // Otherwise, at least match a word boundary. This restricts parent
// routes to matching only their own words and nothing more, e.g. parent
// route "/home" should not match "/home2".
"(?:\\b|$)";
}
//生成正则对象,是否区分大小写
let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
return [matcher, paramNames];
}
完毕
- 第一步我们执行
flattenRoutes
收集routesMeta到一个数组中,这里会把一个路由下面所有的路由定义全部取出,也就是嵌套路由。 执行computeScore
计算匹配度 - 执行
rankRouteBranches
根据匹配度来排序,匹配度高的在前 - 循环,执行
matchRouteBranch
根据每一个branch和最新的pathname调用matchPath
, 函数根据每一个路由的path生成正则, 由pathname去匹配, 并取出参数。matchRouteBranch
函数找到第一个匹配项的数组,找到以后停止。 匹配时只有所有的routesMeta
全部匹配时才算匹配
render
第一步
function useRoutes(
routes: RouteObject[],
locationArg?: Partial<Location> | string
): React.ReactElement | null {
// 省略匹配的故事
// ...
// 对路由信息做最后的处理
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
);
}
第二步
这里反着来, branchs是匹配度高的在前,这里渲染时会渲染匹配低的也就是父级路由,然后才是子级路由
function _renderMatches(
matches: RouteMatch[] | null,
parentMatches: RouteMatch[] = []
): React.ReactElement | null {
if (matches == null) return null;
return matches.reduceRight((outlet, match, index) => {
// match.route.element为空就渲染当前层级的outlet,也就是value属性的outlet
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);
}
outlet 和useOutlet
export function Outlet(_props: OutletProps): React.ReactElement | null {
return useOutlet();
}
export function useOutlet(): React.ReactElement | null {
return React.useContext(RouteContext).outlet;
}
outlet拿到的就是最近的一个context的outleft,也就是我们当前路由匹配的branch
Router
router组件改了,之前默认存在路由为browser模式的路由器,现在没了由router-dom实现
interface RouterProps {
basename?: string;
children?: React.ReactNode;
location: Partial<Location> | string;
navigationType?: NavigationType;
// histoty
navigator: Omit<
History,
"action" | "location" | "back" | "forward" | "listen" | "block"
>;
static?: boolean;
}
interface BrowserRouterProps {
basename?: string;
children?: React.ReactNode;
window?: Window;
}
function BrowserRouter({
basename,
children,
window
}: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
// 和history包结合
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}
/>
);
}
// dom 包中提供的Link组件
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
function LinkWithRef(
{ onClick, reloadDocument, replace = false, state, target, to, ...rest },
ref
) {
let href = useHref(to);
let internalOnClick = useLinkClickHandler(to, { replace, state, target });
function handleClick(
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) {
if (onClick) onClick(event);
if (!event.defaultPrevented && !reloadDocument) {
internalOnClick(event);
}
}
return (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
{...rest}
href={href}
onClick={handleClick}
ref={ref}
target={target}
/>
);
}
);
browserRouter组件props中没有关于history的参数,以为着在router-dom中不能再干预router的工作,比如react中实现路由导航。 但是可以基于router组件来改造
link组件也是使用的useHref
hook来实现的navlink是基于link组件来改造的。那么React-router-dom这个包就没用了,比如自定义的路由导航可以自己实现,那么link组件同样也可以。