走一圈 React Router

React Router 版本为 v6.
从简单的 demo, 逐渐看看 react router 是如何运作的, Go.
BrowserRouter
<BrowserRouter>
<App />
</BrowserRouter>
BrowserRouter
将创建 history
并监听(pushState/popstate event), history
传入到 navigator 以供后续 useNavigate
使用, 然后直接渲染 App

history 是 Remix 团队另外创建的库, 用于管理会话历史
useRoutes
创建完 Router
, 就可以使用 useRoutes
配置路由. 当然, 你也可以使用 Routes
+Route
组件设定.

拍平路由并计算优先级
先将所有设定路由拍平为一维数组, 如上图 [/
, /about
, /*
], 再按路径计算后的优先级排序以用于匹配
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
// 如含有 `*`, 降低优先级, 如 /a/*
if (segments.some(isSplat)) {
initialScore += splatPenalty
}
// 索引页
if (index) {
initialScore += indexRouteValue
}
return segments
.filter((s) => !isSplat(s))
.reduce(
(score, segment) =>
score +
(paramRe.test(segment)
? dynamicSegmentValue // 动态参数 如 :p
: segment === ''
? emptySegmentValue
: staticSegmentValue), // 静态路径 如 /a/b/c 优先级最高
initialScore
)
}
匹配后的路由会进入渲染 _renderMatches
function _renderMatches(
matches: RouteMatch[] | null,
parentMatches: RouteMatch[] = []
): React.ReactElement | null {
if (matches == null) return null
/**
* location: '/about'
* matches: [
* {path: '/', ...},
* {path: '/about', ...}
* ]
*/
return matches.reduceRight((outlet, match, index) => {
return (
/**
* 将匹配路由嵌套层级, 形成
* Provider<{
* path: '/',
* children: 设定的element,
* outlet: Provider<{ // 嵌套的路由
* path: '/about',
* children: 设定的element,
* outlet: null
* }>
* }>,
* 当渲染 `/` 时, Provider会将设定的value(outlet)设置到RouteContext中
* 所以当某层级含有 <Outlet /> 时, 渲染的即为当层嵌套路由 (见下)
*
*/
<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)
}
Provider 更新详见 ReactFiberBeginWork.new.js
Outlet
最后, 在需要的地方渲染 Outlet
组件, 它将根据路由匹配的结果, 渲染相应的组件.

获取 RouteContext
对应的 outlet 直接渲染就是了.
其它
其它例如 useNavigate
, useParams
, useLocation
基本都是简单几行代码, 就不再赘述了.
-- Fin --