创建一个新的 data router,它通过 URL 的 [hash
]https://mdn.org.cn/en-US/docs/Web/API/URL/hash 来管理应用程序的路径。
function createHashRouter(
routes: RouteObject[],
opts?: DOMRouterOpts,
): DataRouter
应用程序路由
应用程序的基准名称路径。
为路由器启用的未来标志。
一个函数,返回一个 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
},
},
}
);
覆盖并行运行 loader 的默认数据策略。参见 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
设置为“拥有 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;
},
});
在导航时懒加载路由树的部分。参见 PatchRoutesOnNavigationFunction
。
默认情况下,React Router 希望您通过 createBrowserRouter(routes)
预先提供一个完整的路由树。这使得 React Router 能够执行同步路由匹配,执行 loader,然后以最乐观的方式渲染路由组件,而不会引入瀑布流。其代价是您的初始 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 段组成。因此,我们不需要费心去寻找任何其他可能得分更高的路由。
然而,带有参数(动态或通配符)的路由不能做出这个假设,因为可能存在一个尚未发现的得分更高的路由。考虑一个完整的路由树,例如:
// 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
实例。
一个初始化的 data router,传递给 <RouterProvider>
。