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

// 通过 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 Warlib/dom/ssr/fog-of-war.ts
SSR 路由lib/dom/ssr/routes.tsx
组件lib/components.tsx
Hookslib/hooks.tsx

源码版本

react-router v8.x
源码路径:packages/react-router/lib/router/