createBrowserRouter
本页内容

createBrowserRouter

摘要

参考文档 ↗

创建一个新的数据路由器,通过 history.pushStatehistory.replaceState 管理应用程序路径。

签名

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

参数

routes

应用程序路由

opts.basename

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

opts.dataStrategy

覆盖默认的并行运行加载器的数据策略。请参阅 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 设置为“拥有加载器”,然后在 route.handle 上存储 GQL 片段来实现这一点。

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.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.patchRoutesOnNavigation

在导航时延迟定义路由树的部分内容。请参阅 PatchRoutesOnNavigationFunction

默认情况下,React Router 希望您通过 createBrowserRouter(routes) 提前提供一个完整的路由树。这使得 React Router 能够执行同步路由匹配,执行加载器,然后以最乐观的方式渲染路由组件,而不会引入瀑布流。代价是您的初始 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 段组成。因此,我们不需要费心去寻找任何其他可能得分更高的路由。

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

// 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 实例。

返回

一个初始化的数据路由器,用于传递给<RouterProvider>

文档和示例 CC 4.0
编辑