走一圈 React Router

react-router

React Router 版本为 v6.

从简单的 demo, 逐渐看看 react router 是如何运作的, Go.

BrowserRouter

<BrowserRouter>
  <App />
</BrowserRouter>

BrowserRouter 将创建 history 并监听(pushState/popstate event), history 传入到 navigator 以供后续 useNavigate 使用, 然后直接渲染 App

BrowserRouter

history 是 Remix 团队另外创建的库, 用于管理会话历史

useRoutes

创建完 Router, 就可以使用 useRoutes 配置路由. 当然, 你也可以使用 Routes+Route 组件设定.

useRoutes

拍平路由并计算优先级

先将所有设定路由拍平为一维数组, 如上图 [/, /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 组件, 它将根据路由匹配的结果, 渲染相应的组件.

Outlet

获取 RouteContext 对应的 outlet 直接渲染就是了.

其它

其它例如 useNavigate, useParams, useLocation 基本都是简单几行代码, 就不再赘述了.

-- Fin --