react-router v8 源码深度解析
这是「三套路由器」系列的第三篇,深度拆解 react-router v8 的源码实现。
源码版本:react-router v8.x
核心文件:packages/react-router/lib/router/
一、History 层实现
1.1 统一的 URL 历史工厂
react-router 用一个工厂函数统一了 Browser 和 Hash 两种模式:
// packages/react-router/lib/router/history.ts
function getUrlBasedHistory(
getLocation: (window, globalHistory) => Location,
createHref: (window, to) => string,
validateLocation: ((location, to) => void) | null,
options: UrlHistoryOptions = {},
): UrlHistory {
let { window = document.defaultView!, v5Compat = false } = options
let globalHistory = window.history
let action = Action.Pop
let listener: Listener | null = null
let index = getIndex()!
// 初始化 index
if (index == null) {
index = 0
globalHistory.replaceState({ ...globalHistory.state, idx: index }, '')
}
function push(to, state) {
action = Action.Push
let location = isLocation(to) ? to : createLocation(history.location, to, state)
if (validateLocation) validateLocation(location, to)
index = getIndex() + 1
let historyState = getHistoryState(location, index)
let url = history.createHref(location.mask || location)
try {
globalHistory.pushState(historyState, '', url)
} catch (error) {
// iOS 限制:降级到 location.assign
if (error instanceof DOMException && error.name === 'DataCloneError') throw error
window.location.assign(url)
}
if (v5Compat && listener) listener({ action, location: history.location, delta: 1 })
}
function replace(to, state) {
action = Action.Replace
let location = isLocation(to) ? to : createLocation(history.location, to, state)
if (validateLocation) validateLocation(location, to)
let historyState = getHistoryState(location, index)
let url = history.createHref(location.mask || location)
globalHistory.replaceState(historyState, '', url)
if (v5Compat && listener) listener({ action, location: history.location, delta: 0 })
}
// ... 其他方法
return history
}
1.2 Browser 和 Hash 的差异
// Browser 模式
export function createBrowserHistory(options = {}) {
return getUrlBasedHistory(
(window, globalHistory) => {
let { pathname, search, hash } = window.location
return createLocation('', { pathname, search, hash } /* state */)
},
(window, to) => normalizeRelativeUrl(typeof to === 'string' ? to : createPath(to)),
null,
options,
)
}
// Hash 模式
export function createHashHistory(options = {}) {
return getUrlBasedHistory(
(window, globalHistory) => {
let { pathname = '/', search = '', hash = '' } = parsePath(window.location.hash.substring(1))
return createLocation('', { pathname, search, hash } /* state */)
},
(window, to) => {
let base = window.document.querySelector('base')
let href = ''
if (base && base.getAttribute('href')) {
let url = window.location.href
let hashIndex = url.indexOf('#')
href = hashIndex === -1 ? url : url.slice(0, hashIndex)
}
return href + '#' + (typeof to === 'string' ? to : createPath(to))
},
(location, to) => {
warning(location.pathname.charAt(0) === '/' /* ... */)
},
options,
)
}
1.3 State 结构
interface HistoryState {
usr: any // 用户自定义 state
key?: string // 唯一标识
idx: number // 历史栈索引
masked?: Path // 掩码路径(用于 UI 路由)
}
masked 的用途:支持 URL 掩码,即显示给用户的 URL 和实际路由不同。常用于登录后跳转等场景。
二、路由匹配
2.1 路由树扁平化
// packages/react-router/lib/router/utils.ts
function flattenRoutes<RouteObjectType extends RouteObject = RouteObject>(
routes: RouteObjectType[],
branches: RouteBranch<RouteObjectType>[] = [],
parentsMeta: RouteMeta<RouteObjectType>[] = [],
parentPath = '',
): RouteBranch<RouteObjectType>[] {
routes.forEach((route, index) => {
let meta: RouteMeta<RouteObjectType> = {
relativePath: route.path || '',
caseSensitive: route.caseSensitive === true,
childrenIndex: index,
route,
}
let path = joinPaths([parentPath, meta.relativePath])
let routesMeta = parentsMeta.concat(meta)
// 先递归处理子路由
if (route.children && route.children.length > 0) {
flattenRoutes(route.children, branches, routesMeta, path)
}
// 只有有 path 或 index 的路由才参与匹配
if (route.path == null && !route.index) return
branches.push({
path,
score: computeScore(path, route.index),
routesMeta: routesMeta.map((meta, i) => {
let [matcher, params] = compilePath(
meta.relativePath,
meta.caseSensitive,
i === routesMeta.length - 1,
)
return { ...meta, matcher, compiledParams: params }
}),
})
})
return branches
}
2.2 路径编译
function compilePath(path, caseSensitive, end) {
let keys = []
let pattern =
'^' +
path
.replace(/\/*\*?$/, '') // 移除尾部通配符
.replace(/^\/*/, '/') // 确保开头有 /
.replace(/[.+*?^${}()[\]|\\]/g, '\\$&') // 转义特殊字符
.replace(/\/:(\w+)/g, (_, key) => {
keys.push(key)
return '/([^\\/]+)'
})
if (path.endsWith('*')) {
keys.push('*')
pattern += '(.*)'
} else if (end) {
pattern += '\\/?$'
}
let flags = caseSensitive ? '' : 'i'
let matcher = new RegExp(pattern, flags)
return [matcher, keys]
}
2.3 评分系统
const staticSegmentValue = 10
const dynamicSegmentValue = 3
const indexRouteValue = 2
const emptySegmentValue = 1
const splatPenalty = -2
function computeScore(path, index) {
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 +
(paramRe.test(segment)
? dynamicSegmentValue
: segment === ''
? emptySegmentValue
: staticSegmentValue),
initialScore,
)
}
设计思路:静态路径权重最高(10),动态路径次之(3),通配符惩罚(-2)。这让路由优先级一目了然。
三、loader/action 数据模型
3.1 loader 函数签名
// packages/react-router/lib/router/utils.ts
interface LoaderFunctionArgs<Context = DefaultContext> {
request: Request // 标准 Request 对象
url: URL // 应用位置
pattern: string // 匹配的路由模式
params: Params // 路由参数
context: Context // 自定义上下文
}
export type LoaderFunction<Context = DefaultContext> = {
(args: LoaderFunctionArgs<Context>, handlerCtx?: unknown): DataFunctionReturnValue
hydrate?: boolean // 是否在 SSR 时执行
}
3.2 action 函数签名
interface ActionFunctionArgs<Context = DefaultContext> {
request: Request
url: URL
pattern: string
params: Params
context: Context
}
export interface ActionFunction<Context = DefaultContext> {
(args: ActionFunctionArgs<Context>, handlerCtx?: unknown): DataFunctionReturnValue
}
设计哲学:loader/action 都接收 Request 对象,返回 Response。这种设计让 SSR 变得简单——服务端和客户端用同一套 API。
3.3 数据结果类型
type ResultType = 'success' | 'error' | 'redirect'
interface SuccessResult {
type: ResultType.success
data: any
}
interface ErrorResult {
type: ResultType.error
error: any
}
interface RedirectResult {
type: ResultType.redirect
status: number
location: string
}
四、middleware 系统
4.1 middleware 函数签名
interface MiddlewareNextFunction<Result = unknown> {
(): Promise<Result>
}
type MiddlewareFunction<Result = unknown> = (
args: DataFunctionArgs<Readonly<RouterContextProvider>>,
next: MiddlewareNextFunction<Result>,
) => MaybePromise<Result | void>
4.2 执行顺序
// 1. 匹配路由
let matches = matchRoutes(routes, location)
// 2. 执行 middleware(从上到下)
for (const match of matches) {
if (match.route.middleware) {
for (const middleware of match.route.middleware) {
await middleware(args, async () => {
// 3. 执行下游
const result = await next()
// 4. 后置逻辑(从下到上)
return result
})
}
}
}
// 5. 执行 loader/action
const result = await match.route.loader(args)
设计思路:middleware 可以包裹 loader/action,实现认证、日志、错误处理等横切关注点。
五、并发控制
5.1 AbortController 应用
// packages/react-router/lib/router/router.ts
let pendingNavigationController: AbortController | null
async function startNavigation(historyAction, location, opts) {
// 中止上一次导航
pendingNavigationController && pendingNavigationController.abort()
pendingNavigationController = null
// 创建新的 controller
pendingNavigationController = new AbortController()
let request = createClientSideRequest(init.history, location, pendingNavigationController.signal)
// 执行 loader
let result = await callLoaderOrAction(match.route.loader, request, match.params, scopedContext)
}
5.2 loader 中使用 signal
export async function loader({ request }: LoaderFunctionArgs) {
// 可以检查 signal 来取消请求
const data = await fetch('/api/user', {
signal: request.signal,
})
// 也可以传递给其他异步操作
const db = await getDb()
const user = await db.query('SELECT * FROM users WHERE id = ?', {
signal: request.signal,
})
return json({ user })
}
设计思路:AbortController 不仅用于路由,还可以传递给 fetch、数据库查询等异步操作,形成完整的取消链路。
六、Fog of War
6.1 什么是 Fog of War?
react-router v8 引入「战争迷雾」概念,用于懒加载路由定义。在微前端或大型应用中,不需要一次性加载所有路由。
6.2 实现原理
// packages/react-router/lib/dom/ssr/fog-of-war.ts
const discoveredPathsMaxSize = 1000
const discoveredPaths = new Set<string>()
export function isFogOfWarEnabled(routeDiscovery, ssr) {
return routeDiscovery.mode === 'lazy' && ssr === true
}
export function getPartialManifest({ sri, ...manifest }, router) {
// 只返回当前匹配的路由
let routeIds = new Set(router.state.matches.map((m) => m.route.id))
// 包含父路由(用于无路径路由)
let segments = router.state.location.pathname.split('/').filter(Boolean)
segments.pop()
while (segments.length > 0) {
paths.push(`/${segments.join('/')}`)
segments.pop()
}
let initialRoutes = [...routeIds].reduce(
(acc, id) => Object.assign(acc, { [id]: manifest.routes[id] }),
{},
)
return { ...manifest, routes: initialRoutes }
}
6.3 按需发现路由
function checkFogOfWar(matches, routes, pathname) {
if (!isFogOfWarEnabled(routeDiscovery, ssr)) {
return { active: false }
}
// 检查是否已发现
if (discoveredPaths.has(pathname)) {
return { active: false }
}
return {
active: true,
matches: discoverRoutes(pathname),
}
}
async function discoverRoutes(pathname) {
// 发送请求到服务器获取路由信息
const response = await fetch(`/__manifest?path=${pathname}`)
const newRoutes = await response.json()
// 合并到现有路由表
patchRoutesOnNavigation(newRoutes)
discoveredPaths.add(pathname)
}
应用场景:
- 微前端:按需加载子应用路由
- 大型应用:避免一次性加载所有路由定义
- 动态路由:运行时生成的路由
七、shouldRevalidate
7.1 函数签名
interface ShouldRevalidateFunctionArgs {
currentUrl: URL
currentParams: DataRouteMatch['params']
nextUrl: URL
nextParams: DataRouteMatch['params']
formMethod?: Submission['formMethod']
formAction?: Submission['formAction']
formData?: FormData
actionResult?: any
}
type ShouldRevalidateFunction = (args: ShouldRevalidateFunctionArgs) => boolean
7.2 默认行为
// 默认的重新验证逻辑
function shouldRevalidate(match, matchRoutes, location, submission) {
// 1. URL 变化时重新验证
if (match.route.path !== currentPath) return true
// 2. 有 action 提交时重新验证
if (submission && isMutationMethod(submission.formMethod)) return true
// 3. 默认不重新验证
return false
}
设计思路:shouldRevalidate 让用户精确控制何时重新加载数据,避免不必要的请求。
八、View Transitions
8.1 Link 组件集成
// 通过 Link 组件
<Link to="/new-page" viewTransition>
Navigate
</Link>
8.2 实现原理
// 在 completeNavigation 中
if (pendingAction === NavigationType.Pop) {
// 检查之前是否启用了 transition
let priorPaths = appliedViewTransitions.get(state.location.pathname)
if (priorPaths && priorPaths.has(location.pathname)) {
viewTransitionOpts = {
currentLocation: state.location,
nextLocation: location,
}
}
} else if (pendingViewTransitionEnabled) {
// 存储 transition 配置
let toPaths = appliedViewTransitions.get(state.location.pathname)
if (toPaths) {
toPaths.add(location.pathname)
} else {
toPaths = new Set([location.pathname])
appliedViewTransitions.set(state.location.pathname, toPaths)
}
viewTransitionOpts = {
currentLocation: state.location,
nextLocation: location,
}
}
九、源码位置速查
| 功能 | 文件路径 |
|---|---|
| History 实现 | lib/router/history.ts |
| 路由匹配 | lib/router/utils.ts (matchRoutes, flattenRoutes) |
| 核心路由器 | lib/router/router.ts |
| 路由工具 | lib/router/utils.ts |
| URL 工具 | lib/router/url.ts |
| Fog of War | lib/dom/ssr/fog-of-war.ts |
| SSR 路由 | lib/dom/ssr/routes.tsx |
| 组件 | lib/components.tsx |
| Hooks | lib/hooks.tsx |
源码版本
react-router v8.x
源码路径:packages/react-router/lib/router/