前言
在真正进入 React Router 源码之前我们已经做了两期的准备了,即通过文章 什么,React Router 已经到 V6 了 ?? 介绍了 v6 各种 api 的用法,又通过 React Router 源码解析之 history 详细解析了 history 每个 api 的作用,如果还没看的话,强烈建议先看完前两期,再看本篇文章。本篇文章所有示例代码都来自react-router-source-analysis,如果哪里不太清楚可以点击查看。那接下来,系好安全带,让我们开始 React Router 源码之旅~
BrowserRouter
我们选择 BrowserRouter 来进行讲解,BrowserRouter 一般是作为 App 的 container
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
我们从BrowserRouter 入口开始,看看其做了哪些初始化工作:
export function BrowserRouter({
basename,
children,
window
}: BrowserRouterProps) {
const historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
// 如果为空,则创建
historyRef.current = createBrowserHistory({ window });
}
const history = historyRef.current;
const [state, setState] = React.useState({
action: history.action,
location: history.location
});
React.useLayoutEffect(() => {
/**
* popstate、push、replace时如果没有blokcers的话,会调用applyTx(nextAction)触发这里的setState
* function applyTx(nextAction: Action) {
* action = nextAction;
* // 获取当前index和location
* [index, location] = getIndexAndLocation();
* listeners.call({ action, location });
* }
*/
history.listen(setState)
}, [history]);
// 一般变化的就是action和location
return (
<Router
basename={basename}
children={children}
action={state.action}
location={state.location}
navigator={history}
/>
);
}
BrowserRouter 初始化会生成 history
实例,并把 setState<{action; location}>
放入对应的 listeners
,那么路由切换就会 setState
了,这个我们在上面文章 React Router 源码解析之 history 的末尾有提过。
Router
BrowserRouter 最后返回了 Router ,其接收的 prop 有变化的一般就是 action
和 location
,其他的一般都在初始化的时候就不变了。当然,我们一般也不直接渲染<Router>
,而是具体环境的 Router,如浏览器环境的 <BrowserRouter>
或 <HashRouter>
,也就是说这个一般是内部使用的
export function Router({
action = Action.Pop,
basename: basenameProp = "/",
children = null,
location: locationProp,
/** 实质上就是history */
navigator,
static: staticProp = false
}: RouterProps): React.ReactElement | null {
const basename = normalizePathname(basenameProp);
const navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
if (typeof locationProp === "string") {
locationProp = parsePath(locationProp);
}
const {
pathname = "/",
search = "",
hash = "",
state = null,
key = "default"
} = locationProp;
// 替换传入location的pathname
const location = React.useMemo(() => {
// 获取pathname中basename后的字符串
const trailingPathname = stripBasename(pathname, basename);
if (trailingPathname == null) {
// 1.pathname不是以basename开头的
// 2.pathname以basename开头的,但不是以`${basename}/`开头
return null;
}
// 到了这里则:
// 1.basename === "/"
// 2.pathname以`${basename}/`开头
return {
pathname: trailingPathname,
search,
hash,
state,
key
};
}, [basename, pathname, search, hash, state, key]);
if (location == null) {
return null;
}
return (
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ action, location }}
/>
</NavigationContext.Provider>
);
}
Router 最后返回了两个Context.Provider
,那么其子组件就可以:
通过
const { basename, navigator } = React.useContext(NavigationContext);
拿到NavigationContext
的 basename 和 navigator(一般是 history 实例)
通过
const { location } = React.useContext(LocationContext)
拿到 location 信息。
上面到了两个 Context 后就完了,那我们看下其 children,如<App/>
示例
我们以下面的 代码实例,看看http://localhost:3000/about/child
(后面简写为/about/child
)是如何匹配到<Route path="about/*" element={<About />}/>
中的 element,从而进一步匹配到<Route path='child' element={<AboutChild/> }/>
中的 element 的。
function App() {
return (
<Routes>
<Route element={<BasicLayout />}>
<Route index element={<Home />} />
{* 注意这里的尾缀必须写成 '/*' *}
<Route path="about/*" element={<About />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
}
function BasicLayout() {
return (
<div>
<h1>Welcome to the app!</h1>
<li>
<Link to=".">Home</Link>
</li>
<li>
<Link to="about">About</Link>
</li>
...
<hr />
<Outlet />
</div>
);
}
function Home() {
return (
<h2>Home</h2>
);
}
function About() {
return (
<div>
<h2>About</h2>
<Link to='child'>about-child</Link>
<Routes>
<Route path='child' element={<AboutChild/> }/>
</Routes>
</div>
);
}
function AboutChild() {
return (
<h2>AboutChild</h2>
);
}
...
Routes 和 Route
每一个 Route 必须放在 Routes 这个 container 中,我们看下 Routes 的源码
/**
* @description <Route> elements 的容器
*/
export function Routes({
children,
location
}: RoutesProps): React.ReactElement | null {
return useRoutes(createRoutesFromChildren(children), location);
}
发现其接收的 children,即下面
// 这里加下<>,不然代码不高亮
<>
<Route index element={<Home />} />
<Route path="about" element={<About />}/>
<Route path="dashboard" element={<Dashboard />} />
<Route path="*" element={<NoMatch />} />
</>
而当我们看 Route 的函数,发现其根本就不渲染
/**
* @description 实质上不渲染,只是用于收集Route的props
*/
export function Route(
_props: PathRouteProps | LayoutRouteProps | IndexRouteProps
): React.ReactElement | null {
// Route实际上没有render,只是作为Routes的child
// Route必须放Routes里面,不然一进来就会报错
// 以下是正确的使用方式
// <Route element={<Layout />}>
// <Route index element={<Home />} />
// <Route path="about" element={<About />} />
// </Route>
// </Routes>
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>.`
);
}
可能绝大部分人都以为使用了组件那么肯定会进入 render,但其实不是,Route 的目的只是提供 props 供createRoutesFromChildren
使用罢了。这也给我们提供了一种全新的思路,打破了以往的认知。
createRoutesFromChildren
createRoutesFromChildren
函数的作用是为了递归收集 Route 上的 props,最终返回一个嵌套数组(如果 Route 有多层的话)。
/**
* @description 创建一个route配置:
* @example
* RouteObject {
* caseSensitive?: boolean;
* children?: RouteObject[];
* element?: React.ReactNode;
* index?: boolean;
* path?: string;
* }[]
*/
export function createRoutesFromChildren(
children: React.ReactNode
): RouteObject[] {
const routes: RouteObject[] = [];
React.Children.forEach(children, element => {
if (!React.isValidElement(element)) {
// 忽悠掉 non-elements
// Ignore non-elements. This allows people to more easily inline
// conditionals in their route config.
return;
}
if (element.type === React.Fragment) {
// element为<></>
// Transparently support React.Fragment and its children.
routes.push.apply(
routes,
createRoutesFromChildren(element.props.children)
);
return;
}
const route: RouteObject = {
caseSensitive: element.props.caseSensitive,
element: element.props.element,
index: element.props.index,
path: element.props.path
};
if (element.props.children) {
/**
* 如果有children
* @example
* <Route path="/" element={<Layout />}>
* <Route path='user/*'/>
* <Route path='dashboard/*'/>
* </Route>
*/
route.children = createRoutesFromChildren(element.props.children);
}
routes.push(route);
});
return routes;
}
我们 log 下 routes,这样看起来比较清晰
也就是说,我们实际上不一定需要通过 Routes 的形式,也可以直接使用 useRoutes,传入嵌套路由数组,同样能生成相同的 routes element,例子如下
import { useRoutes } from 'react-router-dom'
function App() {
const routelements = useRoutes([
{
element: <BasicLayout />,
children: [
{
index: true,
element: <Home />
},
{
path: 'about/*',
element: <Home />
},
{
path: 'dashboard',
element: <Dashboard />,
},
{
path: '*',
element: <NoMatch />
}
]
}
])
return (
routelements
);
}
useRoutes
上面已经处理好了routes: RouteObject[]
,那么接下来 useRoutes 会通过 routes 匹配到对应的 route element。
函数可以分为三段:
- 获取 parentMatches 最后一项
routeMatch
- 通过 matchRoutes 匹配到对应的
matches
- 通过
_renderMatches
渲染上面得到的matches
export function useRoutes(
routes: RouteObject[],
// Routes没传入locationArg,这个我们忽略掉
locationArg?: Partial<Location> | string
): React.ReactElement | null {
// 第一段:获取parentMatches最后一项routeMatch
const { matches: parentMatches } = React.useContext(RouteContext);
const routeMatch = parentMatches[parentMatches.length - 1];
const parentParams = routeMatch ? routeMatch.params : {};
const parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
// 从 LocationContext 获取location
const locationFromContext = useLocation();
let location;
if (locationArg) {
const parsedLocationArg =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
location = parsedLocationArg;
} else {
location = locationFromContext;
}
// 第二段:通过remainingPathname和routes匹配到对应的`matches: RouteMatch<string>[]`
// 一般来说,对于http://localhost:3000/about/child,location.pathname为/auth/child
const pathname = location.pathname || "/";
// parentPathnameBase不为 '/'那么就从pathname中的parentPathnameBase后截取。
// 因为useRoutes是在<Routes></Routes>中调用的,remainingPathname代表当前Routes的相对路径
// eg: pathname = `${parentPathnameBase}xxx`,remainingPathname = 'xxx'
// eg: pathname = `/about/child`,parentPathnameBase = '/about', remainingPathname = '/child'
const remainingPathname =
parentPathnameBase === "/"
? pathname
: pathname.slice(parentPathnameBase.length) || "/";
const matches = matchRoutes(routes, { pathname: remainingPathname });
// 第三段:通过`_renderMatches`渲染上面得到的matches
return _renderMatches(
matches &&
matches.map(match =>
Object.assign({}, match, {
params: Object.assign({}, parentParams, match.params),
pathname: joinPaths([parentPathnameBase, match.pathname]),
pathnameBase: joinPaths([parentPathnameBase, match.pathnameBase])
})
),
parentMatches
);
}
那么我们分别来分析这三段。
获取 parentMatches 最后一项 routeMatch
我们看到一开始就 use 了一个 Context:RouteContext
,从其上面获取 matches,类型看下面代码,从下面的注释我们也得知了这个 context 实际上是在 useRoutes 最后的_renderMatches 函数中使用的,这个我们待会会讲到,这里先说明一下,不然可能看起来可能有点懵逼。
typeof RouteContext = {
outlet: React.ReactElement | null;
matches: RouteMatch[];
}
/** 在`_renderMatches`中会用到,`react-router-dom`中的`useOutlet`可得到最近Context的`outlet` */
const RouteContext = React.createContext<RouteContextObject>({
outlet: null,
matches: []
});
const { matches: parentMatches } = React.useContext(RouteContext);
const routeMatch = parentMatches[parentMatches.length - 1];
// 如果match了,获取params、pathname、pathnameBase
const parentParams = routeMatch ? routeMatch.params : {};
const parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
第一次进入useRoutes
的时候处理的是第一个 Routes 下的 Route
function App() {
return (
<Routes>
<Route element={<BasicLayout />}>
<Route index element={<Home />} />
{* 注意这里的尾缀必须写成 '/*' *}
<Route path="about/*" element={<About />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
}
这个时候的 parentMatches 为空数组,所以拿到的相应参数都是后面的默认值
const parentParams = {};
const parentPathnameBase = "/";
第二次进入useRoutes
的时候处理的是<About/>
中 Routes 下的 Route:
function About() {
return (
<div>
<h2>About</h2>
<Link to='child'>about-child</Link>
<Routes>
<Route path='child' element={<AboutChild/> }/>
</Routes>
</div>
);
}
这个时候的 parentMatches 为数组的 length 为 2,所以拿到的相应参数为
const parentParams = {
params: {*: 'child'},
pathname: "/about/child",
pathnameBase: "/about",
route: {caseSensitive: undefined, element: {…}, index: undefined, path: 'about/*'}
};
const parentPathnameBase = "/about";
如下图,是两次的 parentMatches,第一次为空数组,第二次图中标注了:
即上一次 Routes 中 useRoutes 后得到的 matches 会作为下一层的 parentMatches(这个我们再后面将 _renderMatches
的时候回详细讲到)
通过 matchRoutes 匹配到对应的 matches
获取 matches 的函数是 matchRoutes
,其根据当前 location
对应的 routes
获取相应的 matches:RouteMatch<string>[]
,类型如下
export interface RouteMatch<ParamKey extends string = string> {
// url中动态参数的key和value
params: Params<ParamKey>;
// route的pathname
pathname: string;
// 在子路由之前匹配的部分 URL pathname
pathnameBase: string;
route: RouteObject;
}
整个 matchRoutes 函数如下,下面会再分段详细解析:
/**
* @description 根据`location`对应的`routes`,获取对应的`RouteMatch<string>[]`
*
* Matches the given routes to a location and returns the match data.
*/
export function matchRoutes(
routes: RouteObject[],
locationArg: Partial<Location> | string,
basename = "/"
): RouteMatch[] | null {
/** 得到pathname、hash、search */
const location =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
// pathname是一个取出来basename的相对路径
const pathname = stripBasename(location.pathname || "/", basename);
if (pathname == null) {
return null;
}
// routes有可能是多层设置,那么flatten下
const branches = flattenRoutes(routes);
/**
* 通过score或childrenIndex[]排序branch
* @example
* // 排序前
* [
* {
* path: '/', score: 4, routesMeta: [
* {relativePath: "",caseSensitive: false,childrenIndex: 0},
* {relativePath: "",caseSensitive: false,childrenIndex: 0}
* ]
* },
* {
* path: '/login', score: 13, routesMeta: [
* {relativePath: "",caseSensitive: false,childrenIndex: 0},
* {relativePath: "login",caseSensitive: false,childrenIndex: 1}
* ]
* },
* {
* path: '/protected', score: 13, routesMeta: [
* {relativePath: "",caseSensitive: false,childrenIndex: 0},
* {relativePath: "protected",caseSensitive: true,childrenIndex: 2}
* ]
* }
* ]
* // 排序后
* [
* { path: '/login', score: 13, ...},
* { path: '/protected', score: 13, ...}
* { path: '/', score: 4, ... },
* ]
*/
rankRouteBranches(branches);
let matches = null;
// 直到`matches`有值(意味着匹配到,那么自然不用再找了)或遍历完`branches`才跳出循环
for (let i = 0; matches == null && i < branches.length; ++i) {
matches = matchRouteBranch(branches[i], pathname);
}
return matches;
}
flattenRoutes 收集 routeMeta 后通过 rankRouteBranches 排序
因为 routes 可能是多维数组,那么首先会将传入的 routes flatten 为一维数组,在 flatten 的过程中会收集每个 route 的 props 作为 routeMeta,收集过程是一个深度优先遍历:
/**
* @description 深度优先遍历,如果route有`children`,会先把children处理完push `branches`,然后再push该`route`,
* 要特别注意三点:
* - `LayoutRoute`即只有`element`和`children`,不会push进`branches`
* - `route`不写`path`的话,会给`meta.relativepath`赋值空字符串'' => `relativePath: route.path || ""`
* - 当前`route`处于第几层,那么得到的`branch.routesMeta.length` 就为多少
*
*
* @example
* <Route path='/' element={<Layout />}>
* <Route path='auth/*' element={<Auth />} />
* <Route path='basic/*' element={<Basic />} />
* </Route>
* 那么得到的branches为 [{ path: '/auth/*', ...},{ path: '/basic/*', ...}, { path: '/', ...}]
*
* // `LayoutRoute`只有`element`和`children`,不会push进`branches`
* <Route element={<Layout />}>
* <Route path='auth/*' element={<Auth />} />
* <Route path='basic/*' element={<Basic />} />
* </Route>
* 那么得到的branches为 [{ path: '/auth/*', ...},{ path: '/basic/*', ...}]
*/
function flattenRoutes(
routes: RouteObject[],
branches: RouteBranch[] = [],
parentsMeta: RouteMeta[] = [],
parentPath = ""
): RouteBranch[] {
routes.forEach((route, index) => {
const meta: RouteMeta = {
// 如果path为'',或者不写(LayoutRoute),那么relativePath统一为''
relativePath: route.path || "",
caseSensitive: route.caseSensitive === true,
childrenIndex: index,
route,
};
if (meta.relativePath.startsWith("/")) {
/**
* 如果相对路径以"/"开头,说明是绝对路径,那么必须要以parentPath开头,否则这里会报错。
* 因为这里是嵌套在parentPath下的路由
* eg from src/examples/basic/index.tsx:
* <Route path="about" element={<About />}>
* // parentPath为'/about', meta.relativePath = '/child',不是以parentPath开头的,会报错
* <Route path='/child' element={<AboutChild />} />
* // parentPath为'/about', meta.relativePath = '/about/child',是以parentPath开头的,那么不会报错
* <Route path='/about/child' element={<AboutChild />} />
* </Route>
*/
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.`
);
// 到这里就说明是以parentPath开头了,那么相对路径不需要parentPath,取后面的
// 如上面eg,path="about" 会被下面的joinPath变成'/about',
// meta.relativePath = '/about/child'.slice('/about'.length // 6) = '/child'
meta.relativePath = meta.relativePath.slice(parentPath.length);
}
// 将parentPath, meta.relativePath用 / 连起来成为绝对路径
// eg: parentPath = '/', meta.relativePath = 'auhth/*', path = '/auth/*'
// eg: <Route path="" element={<PublicPage />} /> joinPaths(['', '']) => '/'
const path = joinPaths([parentPath, meta.relativePath]);
// 这里用concat就不会影响到parentsMeta
// 而从这里我们也知道了,如果routesMeta.length > 1, 那么除最后一个meta,前面的肯定是本route的parentsMeta
const 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) {
// 如果route有children,那么不能为index route,即其prop的index不能为true
invariant(
route.index !== true,
`Index routes must not have child routes. Please remove ` +
`all child routes from route path "${path}".`
);
// 有children的先处理children,处理完的children branch放进branches
flattenRoutes(route.children, branches, routesMeta, path);
}
// Routes without a path shouldn't ever match by themselves unless they are
// index routes, so don't add them to the list of possible branches.
if (route.path == null && !route.index) {
// 如果route没有path,且不是index route,那么不放入branches,这里的route一般是LayoutRoute,
// 看下面example的<Route element={<Layout />}>
/**
* @example
* 对于 examples/auth/index.tsx, http://localhost:3000/auth
* // 最外层的Route(LayoutRoute)就没有path和index,那么就return
*
* <Route element={<Layout />}>
* <Route path="" element={<PublicPage />} />
* <Route path="login" element={<LoginPage />} />
* <Route
* path="protected"
* caseSensitive
* element={
* <RequireAuth>
* <ProtectedPage />
* </RequireAuth>
* }
* />
* </Route>
*
* 那么最终最下面收集到的branches为:
* [
* {
* path: '/', score: 4, routesMeta: [
* {relativePath: "",caseSensitive: false,childrenIndex: 0},
* {relativePath: "",caseSensitive: false,childrenIndex: 0}
* ]
* },
* {
* path: '/login', score: 13, routesMeta: [
* {relativePath: "",caseSensitive: false,childrenIndex: 0},
* {relativePath: "login",caseSensitive: false,childrenIndex: 1}
* ]
* },
* {
* path: '/protected', score: 13, routesMeta: [
* {relativePath: "",caseSensitive: false,childrenIndex: 0},
* {relativePath: "protected",caseSensitive: true,childrenIndex: 2}
* ]
* }
* ]
*/
return;
}
// 到了这里满足上面的所有条件了,那么放入branches
//那么routesMeta.length = route自身所处层数
branches.push({ path, score: computeScore(path, route.index), routesMeta });
});
return branches;
}
通过 flattenRoutes
获取到 branches
后,会根据 score 或 childIndex 排序每个 branch,如果 score 相等才去比较 routesMeta 的每个 childIndex:
/** 通过`score`或`childrenIndex[]`排序`branch` */
function rankRouteBranches(branches: RouteBranch[]): void {
branches.sort((a, b) =>
a.score !== b.score
// 不等的话,高的排前面
? b.score - a.score // Higher score first
// score相等的话,那么判断是否是siblings,是的话比较selfIndex(小的排前面),否则相等
: compareIndexes(
a.routesMeta.map(meta => meta.childrenIndex),
b.routesMeta.map(meta => meta.childrenIndex)
)
);
}
第一次进入 flattenRoutes
和通过 rankRouteBranches
排序后得到的 branches
如下
其代码为:
function App() {
return (
<Routes>
<Route element={<BasicLayout />}>
<Route index element={<Home />} />
{* 注意这里的尾缀必须写成 '/*' *}
<Route path="about/*" element={<About />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
}
第二次得到的 branches
如下
其代码为:
function About() {
return (
<div>
...
<Routes>
<Route path='child' element={<AboutChild/> }/>
</Routes>
</div>
);
}
最后根据 pathname 和每项 branches 看看是否能找到对应的匹配项,找到的话就跳出循环
let matches = null;
// 直到`matches`有值(意味着匹配到,那么自然不用再找了)或遍历完`branches`才跳出循环
for (let i = 0; matches == null && i < branches.length; ++i) {
matches = matchRouteBranch(branches[i], pathname);
}
return matches;
matchRouteBranch
matchRouteBranch 会通过每个 branch 的 routesMeta 来看看是否能匹配到相应的 pathname,只要有一个不匹配,就返回 null,而 routesMeta 最后一项是该 route 自己的路由信息,前面项都是 parentMetas,所以只有从头到尾都匹配到,才表示匹配到完整的路由信息。
function matchRouteBranch<ParamKey extends string = string>(
branch: RouteBranch,
pathname: string
): RouteMatch<ParamKey>[] | null {
const { routesMeta } = branch;
/** 已匹配到的动态参数 */
const matchedParams = {};
/** 表示已经匹配到的路径名 */
let matchedPathname = "/";
const matches: RouteMatch[] = [];
for (let i = 0; i < routesMeta.length; ++i) {
const meta = routesMeta[i];
// 是否到了最后一个routesMeta,最后一个就是当前branch自己的routeMeta
const end = i === routesMeta.length - 1;
// remainingPathname表示剩下还没匹配到的路径,因为下面是用meta.relativePath去正则匹配,所以这里
// 每遍历一次要去将传入pathname.slice(matchedPathname.length)
// matchedPathname不为 '/'那么就从pathname中的matchedPathname后截取
// eg: pathname = `${matchedPathname}xxx`,remainingPathname = 'xxx'
// eg: matchedPathname = '/', pathname = `/`,remainingPathname = '/'
// eg: matchedPathname = '/', pathname = `/auth`,remainingPathname = '/auth'
const remainingPathname =
matchedPathname === "/"
? pathname
: pathname.slice(matchedPathname.length) || "/";
/**
* 会返回{ params, pathname, pathnameBase, pattern } or null
*/
const match = matchPath(
{ path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
remainingPathname
);
// 只要有一个没match到,就return掉,意味着即使前面的都match了,但如果最后一个没match到,最终matchRouteBranch还是null
if (!match) return null;
Object.assign(matchedParams, match.params);
// const route = routes[meta.childrenIndex];
const route = meta.route;
matches.push({
params: matchedParams,
pathname: joinPaths([matchedPathname, match.pathname]),
pathnameBase: joinPaths([matchedPathname, match.pathnameBase]),
route
});
if (match.pathnameBase !== "/") {
matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
}
}
// matches.length肯定等于routesMeta.length
return matches;
}
matchPath
而每一项 routeMeta 都会通过 matchPath
函数看看是否匹配到,其会根据 routeMeta 的 relativePath
(即我们在 Route 中写的 path,如 path = 'about/*',path='child';),caseSensitive
(即根据 relativePath 生成的正则是否忽略大小写)以及 end
(是否是最后一项 routeMeta,最后一项表示是该 route 自己的路由信息,同时也意味着匹配到最后了)生成对应的正则匹配。
我们看下matchPath
:
/**
* @description 对pathname执行对应的正则匹配,看是否能返回match的信息
*/
export function matchPath<ParamKey extends string = string>(
pattern: PathPattern | string,
pathname: string
): PathMatch<ParamKey> | null {
if (typeof pattern === "string") {
pattern = { path: pattern, caseSensitive: false, end: true };
}
// 根据pattern.path生成正则以及获取path中的动态参数
// compilePath下面有讲,看到这里可先看下面再回来
const [matcher, paramNames] = compilePath(
pattern.path,
pattern.caseSensitive,
pattern.end
);
// pattern.path生成正则是否match传入的pathname
const match = pathname.match(matcher);
if (!match) return null;
const matchedPathname = match[0];
// eg: 'about/'.replace(/(.)\/+$/, "$1") => 'about' // 即(.),$1表示第一个匹配到的小括号中的值;
// eg: 'about/*'.replace(/(.)\/+$/, "$1") => 'about/*'; // 不匹配,返回原字符串
let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
// eg: pattern = {path: 'about/*', caseSensitive: false, end: true}, pathname = '/about/child';
// matcher = /^\/about(?:\/(.+)|\/*)$/i, paramNames = ['*'];
// match = ['/about/child', 'child', index: 0, input: '/about/child', groups: undefined]
// 那么 matchedPathname = '/about/child', captureGroups = ['child'], params = { '*': 'child' }, pathnamebase = '/about'
// 从第二项就是()中匹配的,所以叫slice从1开始
const captureGroups = match.slice(1);
const params: Params = paramNames.reduce<Mutable<Params>>(
(memo, paramName, index) => {
// We need to compute the pathnameBase here using the raw splat value
// instead of using params["*"] later because it will be decoded then
if (paramName === "*") {
const splatValue = captureGroups[index] || "";
// eg:
// pattern.path = 'about/*', matchedPathname = '/about/child', captureGroups =['child']
// matchedPathname.slice(0, matchedPathname.length - splatValue.length) => '/basic/'
// '/about/'.replace(/(.)\/+$/, "$1") = '/about'
// 即pathnameBase = '/about'
pathnameBase = matchedPathname
.slice(0, matchedPathname.length - splatValue.length)
.replace(/(.)\/+$/, "$1");
}
memo[paramName] = safelyDecodeURIComponent(
captureGroups[index] || "",
paramName
);
return memo;
},
{}
);
return {
params,
pathname: matchedPathname,
pathnameBase,
pattern
};
}
compilePath
matchPath 中的 compilePath
才是真正用到了 relativePath
、caseSensitive
和 end
,即根据这几个参数编译出对应的正则,同时编译过程同发现 path 有动态参数的话就收集到一个数组了,如 ['*', 'id', 'name']
:
/**
* @description: 根据path生成正则以及获取path中的动态参数
* @param {string} path path不能是:xxx*,如果尾部是*,那么需要以"/*"结尾,正常的"/", "/auth"没问题
* @param {boolean} caseSensitive 默认false,根据path生成的正则是否忽略大小写
* @param {boolean} end 默认true,是否到了最后一个routesMeta
* @return {[RegExp, string[]]} 正则以及获取path中的动态参数
*
* @example
*
* compilePath('/') => matcher = /^\/\/*$/i
* compilePath('/', true, false) => matcher = /^\/(?:\b|$)/i
* compilePath('/about') => matcher = /^\/about\/*$/i
* compilePath('/about/child', true) => matcher = /^\/about\/child\/*$/
* compilePath('about/*', true) => matcher = /^\/about(?:\/(.+)|\/*)$/
*/
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(/\*$/, "/*")}".`
);
// 动态参数名数组
// eg: '/auth/:id/www/:name/ee' => paramNames = ['id', 'name']
const paramNames: string[] = [];
let regexpSource =
"^" +
path
.replace(/\/*\*?$/, "") // 去掉尾部的'/'、'//' ..., 或'/*'、'//*', '///*' ..., '*'
.replace(/^\/*/, "/") // 开头没'/'那么加上;开头有多个'/',那么保留一个;eg: (//about | about) => /about
.replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // 对\.*+^$?{}或()[]都给加上\,eg: `()[]` => '\(\)\[\]';`.*+^$?{}` => '\.\*\+\^\$\?\{\}'
.replace(/:(\w+)/g, (_: string, paramName: string) => { // \w === [A-Za-z0-9_], /:(\w+)/g表示处理动态参数
paramNames.push(paramName);
/** [^\\/]+ 表示不能是出现/
* @example
* '/auth/:id/www/:name/ee' => '/auth/([^\/]+)/www/([^\/]+)/ee'
* const reg = new RegExp('/auth/([^\/]+)/www/([^\/]+)/ee', 'i')
* reg.test('/auth/33/www/a1_A/ee') // params = ['33', 'a1_A'], true
* reg.test('/auth/33/www/a1_A//ee')) // params = ['33', 'a1_A/'], false
*/
return "([^\\/]+)";
});
if (path.endsWith("*")) {
// 如果path以"*"结尾,那么paramNames也push
paramNames.push("*");
regexpSource +=
// 如果path等于*或/*, 那么regexpSource最终为regexpSource = '^/(.*)$',(.*)$ 表示match剩下的
path === "*" || path === "/*"
? "(.*)$" // Already matched the initial /, just match the rest
/**
* (?:x),匹配 'x' 但是不记住匹配项。这种括号叫作非捕获括号,使得你能够定义与正则表达式运算符一起使用的子表达式。
* @example
* eg1:
* /(?:foo){1,2}/。如果表达式是 /foo{1,2}/,{1,2} 将只应用于 'foo' 的最后一个字符 'o'。
* 如果使用非捕获括号,则 {1,2} 会应用于整个 'foo' 单词
*
* eg2: 对比下两种exec的结果
* const reg = new RegExp('w(?:\\d+)e')
* reg.exec('w12345e')
* ['w12345e', index: 0, input: 'w12345e', groups: undefined] // 不记住匹配项
*
* 而
* const reg = new RegExp('w(\\d+)e')
* reg.exec('w12345e')
* ['w12345e', '12345', index: 0, input: 'w12345e', groups: undefined] // 记住匹配项
*
* 本处eg:
* path = 'xxx/*'
* const reg = new RegExp("xxx(?:\\/(.+)|\\/*)$", 'i')
* 下面的abc是(.+)中的
* reg.exec('xxx/abc') // ['xxx/abc', 'abc', index: 0, input: 'xxx/abc', groups: undefined]
* 下面两处满足 `|` 后面的\\/*: '/' 出现出现零次或者多次
* reg.exec('xxx') // ['xxx', undefined, index: 0, input: 'xxx', groups: undefined]
* reg.exec('xxx/') // ['xxx/', undefined, index: 0, input: 'xxx/', groups: undefined]
* 当>= 2个'/',就又变成满足\\/(.+)了,所以个人感觉这里的\\/*是不是应该改为\\/{0,1} ????
* reg.exec('xxx//') // ['xxx//','/', index: 0, input: 'xxx//', groups: undefined]
*/
: "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
} else {
// path不以"*"结尾
regexpSource += end
? "\\/*$" // When matching to the end, ignore trailing slashes 如果是end的话,忽略斜杠"/"
: // 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".
/**
* 否则,至少匹配到一个单词边界,这限制了parent routes只能匹配自己的单词。比如/home不允许匹配为/home2。
*
* \b: 匹配这样的位置:它的前一个字符和后一个字符不全是(一个是,一个不是或不存在) \w (\w === [A-Za-z0-9_])
* 通俗的理解,\b 就是“隐式位置”
* "It"中 'I' 和 't' 就是显示位置,中间是“隐式位置”。 更多可见:https://www.cnblogs.com/litmmp/p/4925374.html
* 使用"moon"举例:
* /\bm/匹配“moon”中的‘m’;
* /oo\b/并不匹配"moon"中的'oo',因为'oo'被一个“字”字符'n'紧跟着
* /oon\b/匹配"moon"中的'oon',因为'oon'是这个字符串的结束部分。这样他没有被一个“字”字符紧跟着
*
* 本例:
* compilePath('/', true, false) => matcher = /^\/(?:\b|$)/i
* '/auth'.match(/^\/(?:\b|$)/i) // ['/', index: 0, input: '/auth', groups: undefined]
* 'auth'.match(/^\/(?:\b|$)/i) // null
* reg.exec('/xxx2') or reg.exec('/xxxx') // null
* */
"(?:\\b|$)";
}
const matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
return [matcher, paramNames];
}
matchRoutes
中调用了很多函数,这里我们小结一下:
- 先是通过
flattenRoutes
收集routesMeta
到每一个branches
中,之后通过rankRouteBranches
排序 - 然后通过
matchRouteBranch
根据每个 branch 和 pathname 找到对应的匹配项数组matches
,只要找到就跳出循环 matchRouteBranch
会遍历每个 branch 的routesMeta
,通过matchPath
里调用compilePath
生成正则以及获取 path 中的动态参数paramNames
,只有所有routesMeta
都匹配了才算该 branch 真正匹配
ps: 这段代码会比较复杂,可能看的过程中有些地方比较懵逼,特别是涉及到正则匹配的地方,想要真正搞懂还是需要通过 debugger 来验证,但相信我只要多看几遍,就能很好地理解
通过_renderMatches 渲染上面得到的 matches
ok,我们终于艰难地度过了第二步了,那么到了第三步就是快见到希望的曙光了~~
我们上面历尽千辛万苦终于拿到匹配项 matches
了,那么就要根据匹配项来渲染:
return _renderMatches(
matches &&
matches.map(match =>
Object.assign({}, match, {
params: Object.assign({}, parentParams, match.params),
pathname: joinPaths([parentPathnameBase, match.pathname]),
pathnameBase: joinPaths([parentPathnameBase, match.pathnameBase])
})
),
parentMatches
);
_renderMatches
会根据匹配项和父级匹配项 parentMatches
,从右到左,即从 child --> parent 渲染 RouteContext.Provider
。
/** 根据matches渲染出嵌套的 `<RouteContext.Provider></RouteContext.Provider>`*/
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 />实际上就是该RouteContext的outlet,就是下面value的outlet
return (
<RouteContext.Provider
children={match.route.element || <Outlet />}
value={{
outlet,
matches: parentMatches.concat(matches.slice(0, index + 1))
}}
/>
);
}, null as React.ReactElement | null);
}
其生成结构类似:
// 这里生成的结构如下,即对于matches `index + 1` 生成的Provider作为 `index` Provider value的outlet(出口) :
// matches.length = 2
return (
<RouteContext.Provider
value={{
matches: parentMatches.concat(matches.slice(0, 1)),
outlet: (
<RouteContext.Provider
value={{
matches: parentMatches.concat(matches.slice(0, 2)),
outlet: null // 第一次outlet为null,
}}
>
{<Layout2 /> || <Outlet />}
</RouteContext.Provider>
),
}}
>
{<Layout1 /> || <Outlet />}
</RouteContext.Provider>
)
上面刚好用到了<Outlet />
,我们顺便看下 Outlet
源码:
export function Outlet(_props: OutletProps): React.ReactElement | null {
return useOutlet();
}
export function useOutlet(): React.ReactElement | null {
return React.useContext(RouteContext).outlet;
}
<Outlet />
即返回最近一层 RouteContext 的 outlet
。
_renderMatches
生成的子 RouteContext.Provider
会作为前一父级的 outlet
,而又因为当前 RouteContext.Provider
的 children 就是其 match 的 element 或<Outlet />
,那么只要有 <Outlet />
就能拿到最近一层 RouteContext 的 outlet
了,所以我们常常在嵌套路由的 parent route 的 element 写上一个<Outlet />
,相当于插槽的作用,如:
function BasicLayout() {
return (
<div>
<h1>Welcome to the app!</h1>
<li>
<Link to=".">Home</Link>
</li>
<li>
<Link to="about">About</Link>
</li>
...
<hr />
<Outlet />
</div>
);
}
终于讲完了 Route 的匹配过程了,下面会讲一些上面没讲到,但比较常用的 hook 和组件。
useNavigate
v6
用 useNavigate 替代了 useHistory,其返回了一个 navigate (点击查看用法) 的方法,实现比较简单:
- 从
NavigationContext
拿到navigator
,也就是 history 实例。 - 然后根据
to
、matches
的每项pathnameBase
以及当前 URL pathname 生成最终的路径 path({pathname, search, hash}
) - 根据是否指定
replace
来判断是调用 replace 还是 push 方法
export function useNavigate(): NavigateFunction {
const { basename, navigator } = React.useContext(NavigationContext);
const { matches } = React.useContext(RouteContext);
const { pathname: locationPathname } = useLocation();
// stringgify是为了下面的memo??
const routePathnamesJson = JSON.stringify(
matches.map(match => match.pathnameBase)
);
/** 是否已挂载 */
const activeRef = React.useRef(false);
React.useEffect(() => {
activeRef.current = true;
});
const navigate: NavigateFunction = React.useCallback(
(to: To | number, options: { replace?: boolean; state?: State } = {}) => {
warning(
activeRef.current,
`You should call navigate() in a React.useEffect(), not when ` +
`your component is first rendered.`
);
if (!activeRef.current) return;
if (typeof to === "number") {
navigator.go(to);
return;
}
const path = resolveTo(
to,
JSON.parse(routePathnamesJson),
locationPathname
);
if (basename !== "/") {
path.pathname = joinPaths([basename, path.pathname]);
}
// replace为true才调用replace方法,否则都是push
(!!options.replace ? navigator.replace : navigator.push)(
path,
options.state
);
},
[basename, navigator, routePathnamesJson, locationPathname]
);
return navigate;
}
useLocation
useLocation
即从 LocationContext
获取 location。LocationContext
在 Router 中调用,没注意到的可以翻到上面看看
export function useLocation(): Location {
return React.useContext(LocationContext).location;
}
useResolvedPath
useResolvedPath
根据当前location
以及所处RouteContext
的matches
解析给定to
的pathname
export function useResolvedPath(to: To): Path {
const { matches } = React.useContext(RouteContext);
const { pathname: locationPathname } = useLocation();
// 转为字符串是为了避免memo依赖加上对象导致缓存失效?
const routePathnamesJson = JSON.stringify(
matches.map(match => match.pathnameBase)
);
return React.useMemo(
() => resolveTo(to, JSON.parse(routePathnamesJson), locationPathname),
[to, routePathnamesJson, locationPathname]
);
}
useParams
顾名思义,用于获取 params
export function useParams<Key extends string = string>(): Readonly<
Params<Key>
> {
const { matches } = React.useContext(RouteContext);
const routeMatch = matches[matches.length - 1];
return routeMatch ? (routeMatch.params as any) : {};
}
useLinkClickHandler
useLinkClickHandler
用于处理路由<Link>
组件的点击行为, 比较适用于我们要自定义<Link>
组件,因为其返回的方法和 <Link>
有相同的点击行为
export function useLinkClickHandler<
E extends Element = HTMLAnchorElement,
S extends State = State
>(
to: To,
{
target,
replace: replaceProp,
state
}: {
target?: React.HTMLAttributeAnchorTarget;
replace?: boolean;
state?: S;
} = {}
): (event: React.MouseEvent<E, MouseEvent>) => void {
const navigate = useNavigate();
const location = useLocation();
const path = useResolvedPath(to);
return React.useCallback(
(event: React.MouseEvent<E, MouseEvent>) => {
if (
event.button === 0 && // Ignore everything but left clicks 忽略除了左键外的所有内容
(!target || target === "_self") && // Let browser handle "target=_blank" etc. 让浏览器处理"target=_blank"等
!isModifiedEvent(event) // Ignore clicks with modifier keys 忽略meta、alt、ctrl、shift等修饰符
) {
event.preventDefault();
// If the URL hasn't changed, a regular <a> will do a replace instead of
// a push, so do the same here.
// 如果有传replace为true或当前location和传入path的`pathname + search + hash`相等,那么replace为true,
// 即URL没有改变的话,<a>会使用replace而不是push
// 比如当前路径为/basic, 点后点击<Link to='.'>,那上面的useResolvedPath(to)的path还是为{pathname: '/basic', search: '', hash: ''}
// 那么这里的replace就满足createPath(location) === createPath(path),即为true了,那就是replace,如果不是跳本路由,那么就为false,那就是push
const replace =
!!replaceProp || createPath(location) === createPath(path);
navigate(to, { replace, state });
}
},
[location, navigate, path, replaceProp, state, target, to]
);
}
讲完一下常用 hook,下面讲些常用的组件。
<Link />
Link (点击查看用法) 实质上只是包装了<a>
,对 onClick 事件做了处理,如果自定义了 onClick,那么用该 onClick,否则会用内部的函数internalOnClick
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
function LinkWithRef(
{ onClick, replace = false, state, target, to, ...rest },
ref
) {
const href = useHref(to);
const internalOnClick = useLinkClickHandler(to, { replace, state, target });
function handleClick(
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) {
// 如果有传onClick,那么调用该onClick
if (onClick) onClick(event);
// 否则如果事件的默认行为没有被阻止的话,那么调用internalOnClick,
// 因为internalOnClick里面会调用event.preventDefault(),使event.defaultPrevented = true
if (!event.defaultPrevented) {
internalOnClick(event);
}
}
return (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
{...rest}
href={href}
onClick={handleClick}
ref={ref}
target={target}
/>
);
}
);
<Navigate />
用于改变当前 location, 比较常用于 class 组件中,会在 useEffect 后 navigate 到对应的 to。函数组件建议用useNavigate
export function Navigate({ to, replace, state }: NavigateProps): null {
const navigate = useNavigate();
React.useEffect(() => {
navigate(to, { replace, state });
});
return null;
}
结语
我们以BrowserRouter
为例解析了 react router 的匹配过程,然后又解析了几个常用的 hook 如useNavigate
、useLocation
等和组件<Link/>
、<Navigate/>
。如果再看的过程中有什么不明白的,可以通过 clone 该分支feature/examples-source-analysis 跟着一起 debugger,多看几遍,相信可以看懂的。
最后
这是我们react router 源码分析的最后一篇文章了,感谢大家看到这里,可能过程中有些讲的不好,还请见谅。
感谢留下足迹,如果您觉得文章不错😄😄,还请动动手指😋😋,点赞+收藏+转发🌹🌹