createHashRouter
本页内容

createHashRouter

摘要

参考文档 ↗

创建一个新的 data router,它通过 URL 的 [hash]https://mdn.org.cn/en-US/docs/Web/API/URL/hash 来管理应用程序的路径。

签名

function createHashRouter(
  routes: RouteObject[],
  opts?: DOMRouterOpts,
): DataRouter

参数

路由

应用程序路由

opts.basename

应用程序的基准名称路径。

opts.future

为路由器启用的未来标志。

opts.unstable_getContext

一个函数,返回一个 unstable_RouterContextProvider 实例,该实例作为 context 参数提供给客户端的 actionloader中间件。此函数在每次导航或 fetcher 调用时被调用以生成一个新的 context 实例。

import {
  unstable_createContext,
  unstable_RouterContextProvider,
} from "react-router";

const apiClientContext = unstable_createContext<APIClient>();

function createBrowserRouter(routes, {
  unstable_getContext() {
    let context = new unstable_RouterContextProvider();
    context.set(apiClientContext, getApiClient());
    return context;
  }
})

opts.hydrationData

当进行服务器端渲染并选择退出自动水合时,hydrationData 选项允许您从服务器端渲染中传入水合数据。这几乎总是 StaticHandlerquery 方法返回的 StaticHandlerContext 值的数据子集。

const router = createBrowserRouter(routes, {
  hydrationData: {
    loaderData: {
      // [routeId]: serverLoaderData
    },
    // may also include `errors` and/or `actionData`
  },
});

部分水合数据

您几乎总是会包含一套完整的 loaderData 来水合一个服务器端渲染的应用。但在高级用例中(例如框架模式的 clientLoader),您可能只想为某些在服务器上加载/渲染的路由包含 loaderData。这允许您水合一些路由(例如应用布局/外壳),同时显示一个 HydrateFallback 组件,并在水合期间为其他路由运行 loader

路由的 loader 将在两种情况下在水合期间运行:

  1. 未提供水合数据。在这些情况下,HydrateFallback 组件将在初始水合时渲染。
  2. loader.hydrate 属性设置为 true。这允许您运行 loader,即使您在初始水合时没有渲染回退组件(即,用初始水合数据来填充缓存)。
const router = createBrowserRouter(
  [
    {
      id: "root",
      loader: rootLoader,
      Component: Root,
      children: [
        {
          id: "index",
          loader: indexLoader,
          HydrateFallback: IndexSkeleton,
          Component: Index,
        },
      ],
    },
  ],
  {
    hydrationData: {
      loaderData: {
        root: "ROOT DATA",
        // No index data provided
      },
    },
  }
);

opts.dataStrategy

覆盖并行运行 loader 的默认数据策略。参见 DataStrategyFunction

这是一个用于高级用例的底层 API。它覆盖了 React Router 内部对 action/loader 执行的处理,如果操作不当将会破坏您的应用程序代码。请谨慎使用并进行适当的测试。

默认情况下,React Router 对于如何加载/提交数据有自己的主张——最值得注意的是,它会并行执行您所有的 loader 以实现最佳的数据获取。虽然我们认为这对于大多数用例来说是正确的行为,但我们也意识到,当涉及到满足广泛的应用程序需求时,并没有“一刀切”的解决方案。

dataStrategy 选项让您完全控制您的 actionloader 的执行方式,并为构建更高级的 API(如中间件、上下文和缓存层)奠定了基础。随着时间的推移,我们希望我们能内部利用这个 API 为 React Router 带来更多的一流 API,但在此之前(以及之后),这是您为应用程序的数据需求添加更高级功能的方式。

dataStrategy 函数应返回一个 routeId -> DataStrategyResult 的键/值对象,并应包含任何执行了处理程序的路由的条目。DataStrategyResult 根据 DataStrategyResult.type 字段指示处理程序是否成功。如果返回的 DataStrategyResult.result 是一个 Response,React Router 会为您解包(通过 res.jsonres.text)。如果您需要对 Response 进行自定义解码但又想保留状态码,您可以使用 data 工具返回您解码后的数据以及一个 ResponseInit

dataStrategy 使用案例示例

添加日志记录

在最简单的情况下,让我们看看如何利用这个 API 来为我们的路由 action/loader 执行时添加一些日志记录。

let router = createBrowserRouter(routes, {
  async dataStrategy({ matches, request }) {
    const matchesToLoad = matches.filter((m) => m.shouldLoad);
    const results: Record<string, DataStrategyResult> = {};
    await Promise.all(
      matchesToLoad.map(async (match) => {
        console.log(`Processing ${match.route.id}`);
        results[match.route.id] = await match.resolve();;
      })
    );
    return results;
  },
});

中间件

让我们通过 handle 在每个路由上定义一个中间件,首先按顺序调用中间件,然后并行调用所有 loader,并提供通过中间件可用的任何数据。

const routes = [
  {
    id: "parent",
    path: "/parent",
    loader({ request }, context) {
       // ...
    },
    handle: {
      async middleware({ request }, context) {
        context.parent = "PARENT MIDDLEWARE";
      },
    },
    children: [
      {
        id: "child",
        path: "child",
        loader({ request }, context) {
          // ...
        },
        handle: {
          async middleware({ request }, context) {
            context.child = "CHILD MIDDLEWARE";
          },
        },
      },
    ],
  },
];

let router = createBrowserRouter(routes, {
  async dataStrategy({ matches, params, request }) {
    // Run middleware sequentially and let them add data to `context`
    let context = {};
    for (const match of matches) {
      if (match.route.handle?.middleware) {
        await match.route.handle.middleware(
          { request, params },
          context
        );
      }
    }

    // Run loaders in parallel with the `context` value
    let matchesToLoad = matches.filter((m) => m.shouldLoad);
    let results = await Promise.all(
      matchesToLoad.map((match, i) =>
        match.resolve((handler) => {
          // Whatever you pass to `handler` will be passed as the 2nd parameter
          // to your loader/action
          return handler(context);
        })
      )
    );
    return results.reduce(
      (acc, result, i) =>
        Object.assign(acc, {
          [matchesToLoad[i].route.id]: result,
        }),
      {}
    );
  },
});

自定义处理程序

也可能您甚至不想在路由级别定义一个 loader 实现。也许您只想确定路由并为您的所有数据发出一个单一的 GraphQL 请求?您可以通过将 route.loader=true 设置为“拥有 loader”,然后将 GQL 片段存储在 route.handle 上来做到这一点。

const routes = [
  {
    id: "parent",
    path: "/parent",
    loader: true,
    handle: {
      gql: gql`
        fragment Parent on Whatever {
          parentField
        }
      `,
    },
    children: [
      {
        id: "child",
        path: "child",
        loader: true,
        handle: {
          gql: gql`
            fragment Child on Whatever {
              childField
            }
          `,
        },
      },
    ],
  },
];

let router = createBrowserRouter(routes, {
  async dataStrategy({ matches, params, request }) {
    // Compose route fragments into a single GQL payload
    let gql = getFragmentsFromRouteHandles(matches);
    let data = await fetchGql(gql);
    // Parse results back out into individual route level `DataStrategyResult`'s
    // keyed by `routeId`
    let results = parseResultsFromGql(data);
    return results;
  },
});

opts.patchRoutesOnNavigation

在导航时懒加载路由树的部分。参见 PatchRoutesOnNavigationFunction

默认情况下,React Router 希望您通过 createBrowserRouter(routes) 预先提供一个完整的路由树。这使得 React Router 能够执行同步路由匹配,执行 loader,然后以最乐观的方式渲染路由组件,而不会引入瀑布流。其代价是您的初始 JS 包按定义会更大——随着应用程序的增长,这可能会减慢应用程序的启动时间。

为了解决这个问题,我们在 v6.9.0 中引入了 route.lazy,它允许您懒加载路由的*实现*(loaderComponent 等),同时仍然预先提供路由的*定义*方面(pathindex 等)。这是一个很好的折衷方案。React Router 仍然预先知道您的路由定义(轻量级部分)并可以执行同步路由匹配,但将路由实现方面的任何加载(重量级部分)延迟到实际导航到该路由时。

在某些情况下,即使这样做也还不够。对于大型应用程序,预先提供所有路由定义可能会非常昂贵。此外,在某些微前端或模块联邦架构中,甚至可能无法预先提供所有路由定义。

这就是 patchRoutesOnNavigation 发挥作用的地方(RFC)。这个 API 适用于您无法预先提供完整路由树并且需要一种在运行时懒加载“发现”路由树部分的高级用例。这个功能通常被称为“战争迷雾”,因为类似于视频游戏随着您的移动而扩展“世界”的方式——路由器会随着用户在应用程序中导航而扩展其路由树——但最终只会加载用户访问过的部分树。

patchRoutesOnNavigation 将在 React Router 无法匹配 path 的任何时候被调用。参数包括 path、任何部分 matches,以及一个您可以调用以将新路由修补到树中特定位置的 patch 函数。此方法在 GET 请求的 loading 阶段执行,在非 GET 请求的 submitting 阶段执行。

patchRoutesOnNavigation 使用案例示例

将子路由修补到现有路由中

const router = createBrowserRouter(
  [
    {
      id: "root",
      path: "/",
      Component: RootComponent,
    },
  ],
  {
    async patchRoutesOnNavigation({ patch, path }) {
      if (path === "/a") {
        // Load/patch the `a` route as a child of the route with id `root`
        let route = await getARoute();
        //  ^ { path: 'a', Component: A }
        patch("root", [route]);
      }
    },
  }
);

在上面的示例中,如果用户点击一个指向 /a 的链接,React Router 最初不会匹配任何路由,并将调用 patchRoutesOnNavigation,其中 path = "/a"matches 数组包含根路由匹配。通过调用 patch('root', [route]),新路由将作为 root 路由的子路由添加到路由树中,然后 React Router 将对更新后的路由进行匹配。这次它将成功匹配 /a 路径,导航将成功完成。

修补新的根级路由

如果您需要将新路由修补到树的顶部(即,它没有父路由),您可以将 null 作为 routeId 传递。

const router = createBrowserRouter(
  [
    {
      id: "root",
      path: "/",
      Component: RootComponent,
    },
  ],
  {
    async patchRoutesOnNavigation({ patch, path }) {
      if (path === "/root-sibling") {
        // Load/patch the `/root-sibling` route as a sibling of the root route
        let route = await getRootSiblingRoute();
        //  ^ { path: '/root-sibling', Component: RootSibling }
        patch(null, [route]);
      }
    },
  }
);

异步修补子树

您还可以执行异步匹配,以懒加载应用程序的整个部分。

let router = createBrowserRouter(
  [
    {
      path: "/",
      Component: Home,
    },
  ],
  {
    async patchRoutesOnNavigation({ patch, path }) {
      if (path.startsWith("/dashboard")) {
        let children = await import("./dashboard");
        patch(null, children);
      }
      if (path.startsWith("/account")) {
        let children = await import("./account");
        patch(null, children);
      }
    },
  }
);

如果正在进行的 patchRoutesOnNavigation 执行被后续导航中断,那么被中断执行中任何剩余的 patch 调用都不会更新路由树,因为操作已被取消。

将路由发现与路由定义并置

如果您不希望执行自己的伪匹配,可以利用部分 matches 数组和路由上的 handle 字段来保持子路由定义的并置。

let router = createBrowserRouter(
  [
    {
      path: "/",
      Component: Home,
    },
    {
      path: "/dashboard",
      children: [
        {
          // If we want to include /dashboard in the critical routes, we need to
          // also include it's index route since patchRoutesOnNavigation will not be
          // called on a navigation to `/dashboard` because it will have successfully
          // matched the `/dashboard` parent route
          index: true,
          // ...
        },
      ],
      handle: {
        lazyChildren: () => import("./dashboard"),
      },
    },
    {
      path: "/account",
      children: [
        {
          index: true,
          // ...
        },
      ],
      handle: {
        lazyChildren: () => import("./account"),
      },
    },
  ],
  {
    async patchRoutesOnNavigation({ matches, patch }) {
      let leafRoute = matches[matches.length - 1]?.route;
      if (leafRoute?.handle?.lazyChildren) {
        let children =
          await leafRoute.handle.lazyChildren();
        patch(leafRoute.id, children);
      }
    },
  }
);

关于带参数的路由的说明

由于 React Router 使用排名路由来为给定路径找到最佳匹配,因此在任何给定时间点只知道部分路由树时会引入一种有趣的歧义。如果我们匹配一个完全静态的路由,如 path: "/about/contact-us",那么我们知道我们已经找到了正确的匹配,因为它完全由静态 URL 段组成。因此,我们不需要费心去寻找任何其他可能得分更高的路由。

然而,带有参数(动态或通配符)的路由不能做出这个假设,因为可能存在一个尚未发现的得分更高的路由。考虑一个完整的路由树,例如:

// Assume this is the full route tree for your app
const routes = [
  {
    path: "/",
    Component: Home,
  },
  {
    id: "blog",
    path: "/blog",
    Component: BlogLayout,
    children: [
      { path: "new", Component: NewPost },
      { path: ":slug", Component: BlogPost },
    ],
  },
];

然后假设我们想在用户导航时使用 patchRoutesOnNavigation 来填充它:

// Start with only the index route
const router = createBrowserRouter(
  [
    {
      path: "/",
      Component: Home,
    },
  ],
  {
    async patchRoutesOnNavigation({ patch, path }) {
      if (path === "/blog/new") {
        patch("blog", [
          {
            path: "new",
            Component: NewPost,
          },
        ]);
      } else if (path.startsWith("/blog")) {
        patch("blog", [
          {
            path: ":slug",
            Component: BlogPost,
          },
        ]);
      }
    },
  }
);

如果用户首先访问一篇博客文章(即 /blog/my-post),我们会修补 :slug 路由。然后,如果用户导航到 /blog/new 写一篇新文章,我们会匹配到 /blog/:slug,但这将不是*正确*的匹配!我们需要调用 patchRoutesOnNavigation,以防存在一个我们尚未发现的得分更高的路由,而在这种情况下确实存在。

所以,每当 React Router 匹配一个至少包含一个参数的路径时,它都会调用 patchRoutesOnNavigation 并再次匹配路由,以确认它找到了最佳匹配。

如果您的 patchRoutesOnNavigation 实现开销很大,或者正在对后端服务器进行有副作用的 fetch 调用,您可能需要考虑跟踪以前见过的路由,以避免在您知道已找到正确路由的情况下过度获取。这通常可以通过维护一个先前 path 值的小缓存来简单实现,您已经为这些值修补了正确的路由。

let discoveredRoutes = new Set();

const router = createBrowserRouter(routes, {
  async patchRoutesOnNavigation({ patch, path }) {
    if (discoveredRoutes.has(path)) {
      // We've seen this before so nothing to patch in and we can let the router
      // use the routes it already knows about
      return;
    }

    discoveredRoutes.add(path);

    // ... patch routes in accordingly
  },
});

opts.window

覆盖 Window 对象。默认为全局 window 实例。

返回

一个初始化的 data router,传递给 <RouterProvider>

文档和示例 CC 4.0
编辑