创建一个新的数据路由器,通过 history.pushState
和 history.replaceState
管理应用程序路径。
function createBrowserRouter(
routes: RouteObject[],
opts?: DOMRouterOpts,
): DataRouter
应用程序路由
应用程序的基准名称路径。
覆盖默认的并行运行加载器的数据策略。请参阅 DataStrategyFunction
。
action
/loader
执行的处理,如果操作不当,将会破坏您的应用代码。请谨慎使用并进行适当的测试。
默认情况下,React Router 对如何加载/提交数据有自己的主张——最值得注意的是,它会并行执行所有loader
,以实现最佳数据获取。虽然我们认为这对于大多数用例来说是正确的行为,但我们也认识到,在数据获取方面,没有一个“一刀切”的解决方案能够满足广泛的应用程序需求。
dataStrategy
选项让您可以完全控制action
和loader
的执行方式,并为构建更高级的 API(如中间件、上下文和缓存层)奠定了基础。随着时间的推移,我们希望能在内部利用这个 API 为 React Router 带来更多一流的 API,但在此之前(以及之后),这是您为应用程序的数据需求添加更高级功能的方式。
dataStrategy
函数应返回一个 routeId
-> DataStrategyResult
的键/值对象,并应包含执行了处理程序的任何路由的条目。DataStrategyResult
根据 DataStrategyResult.type
字段指示处理程序是否成功。如果返回的 DataStrategyResult.result
是一个 Response
,React Router 将为您解包(通过 res.json
或 res.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;
},
});
为路由器启用的未来标志。
一个返回 unstable_RouterContextProvider
实例的函数,该实例作为 context
参数提供给客户端的 action
、loader
和 中间件。此函数在每次导航或 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;
}
})
当进行服务器端渲染并选择退出自动注水时,hydrationData
选项允许您从服务器渲染中传入注水数据。这几乎总是您从 StaticHandler
的 query
方法中获取的 StaticHandlerContext
值的子集。
const router = createBrowserRouter(routes, {
hydrationData: {
loaderData: {
// [routeId]: serverLoaderData
},
// may also include `errors` and/or `actionData`
},
});
部分注水数据
您几乎总是会包含一整套 loaderData
来注水一个服务器渲染的应用。但在高级用例中(例如框架模式的 clientLoader
),您可能只想为在服务器上加载/渲染的部分路由包含 loaderData
。这允许您注水一些路由(例如应用布局/外壳),同时显示 HydrateFallback
组件并在注水期间为其他路由运行 loader
。
路由的 loader
会在两种情况下在注水期间运行:
HydrateFallback
组件将在初始注水时渲染。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
},
},
}
);
在导航时延迟定义路由树的部分内容。请参阅 PatchRoutesOnNavigationFunction
。
默认情况下,React Router 希望您通过 createBrowserRouter(routes)
提前提供一个完整的路由树。这使得 React Router 能够执行同步路由匹配,执行加载器,然后以最乐观的方式渲染路由组件,而不会引入瀑布流。代价是您的初始 JS 包在定义上会更大——这可能会随着应用程序的增长而减慢应用程序的启动时间。
为了解决这个问题,我们在 v6.9.0 中引入了 route.lazy
,它允许您延迟加载路由的实现(loader
、Component
等),同时仍然提前提供路由的定义方面(path
、index
等)。这是一个很好的折衷方案。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
},
});
Window
对象覆盖。默认为全局 window
实例。
一个初始化的数据路由器,用于传递给<RouterProvider>
。