Tip
对于大多数应用程序,不建议使用基于代码的路由。推荐使用基于文件的路由替代方案。
基于代码的路由与基于文件的路由在本质上并无差异,二者均采用相同的路由树概念来组织、匹配路由,并将匹配的路由组合成组件树。唯一区别在于:前者通过代码而非文件系统来管理路由结构。
以下是将路由树与嵌套指南中的示例转换为基于代码路由的对比:
基于文件版本的结构:
routes/
├── __root.tsx
├── index.tsx
├── about.tsx
├── posts/
│ ├── index.tsx
│ ├── $postId.tsx
├── posts.$postId.edit.tsx
├── settings/
│ ├── profile.tsx
│ ├── notifications.tsx
├── _pathlessLayout.tsx
├── _pathlessLayout/
│ ├── route-a.tsx
├── ├── route-b.tsx
├── files/
│ ├── $.tsx
routes/
├── __root.tsx
├── index.tsx
├── about.tsx
├── posts/
│ ├── index.tsx
│ ├── $postId.tsx
├── posts.$postId.edit.tsx
├── settings/
│ ├── profile.tsx
│ ├── notifications.tsx
├── _pathlessLayout.tsx
├── _pathlessLayout/
│ ├── route-a.tsx
├── ├── route-b.tsx
├── files/
│ ├── $.tsx
基于代码的简化版本:
import { createRootRoute, createRoute } from '@tanstack/solid-router'
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'about',
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
})
const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
})
const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
})
const postEditorRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts/$postId/edit',
})
const settingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'settings',
})
const profileRoute = createRoute({
getParentRoute: () => settingsRoute,
path: 'profile',
})
const notificationsRoute = createRoute({
getParentRoute: () => settingsRoute,
path: 'notifications',
})
const pathlessLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'pathlessLayout',
})
const pathlessLayoutARoute = createRoute({
getParentRoute: () => pathlessLayoutRoute,
path: 'route-a',
})
const pathlessLayoutBRoute = createRoute({
getParentRoute: () => pathlessLayoutRoute,
path: 'route-b',
})
const filesRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'files/$',
})
import { createRootRoute, createRoute } from '@tanstack/solid-router'
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'about',
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
})
const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
})
const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
})
const postEditorRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts/$postId/edit',
})
const settingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'settings',
})
const profileRoute = createRoute({
getParentRoute: () => settingsRoute,
path: 'profile',
})
const notificationsRoute = createRoute({
getParentRoute: () => settingsRoute,
path: 'notifications',
})
const pathlessLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'pathlessLayout',
})
const pathlessLayoutARoute = createRoute({
getParentRoute: () => pathlessLayoutRoute,
path: 'route-a',
})
const pathlessLayoutBRoute = createRoute({
getParentRoute: () => pathlessLayoutRoute,
path: 'route-b',
})
const filesRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'files/$',
})
除根路由外,所有其他路由都通过createRoute函数配置:
const route = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
component: PostsComponent,
})
const route = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
component: PostsComponent,
})
其中getParentRoute选项是一个返回父路由的函数。
❓❓❓ "需要为每个路由指定父路由?"
确实如此!这种设计完全是为了实现TanStack Router强大的类型安全特性。若缺少父路由信息,TypeScript将无法推断路由的类型参数!
Important
对于非根路由和无路径布局路由,必须配置path选项。该路径用于匹配URL路径以确定路由是否匹配。
路由路径配置时会自动规范化首尾斜杠(不包括索引路由路径/)。以下为路径规范化示例表:
原始路径 | 规范化路径 |
---|---|
/ | / |
/about | about |
about/ | about |
about | about |
$ | $ |
/$ | $ |
/$/ | $ |
在代码中构建路由树时,仅定义父路由是不够的。必须通过将每个路由添加到其父路由的children数组来显式构建完整路由树,这与基于文件路由的自动构建不同。
/* prettier-ignore */
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
postsRoute.addChildren([
postsIndexRoute,
postRoute,
]),
postEditorRoute,
settingsRoute.addChildren([
profileRoute,
notificationsRoute,
]),
pathlessLayoutRoute.addChildren([
pathlessLayoutARoute,
pathlessLayoutBRoute,
]),
filesRoute.addChildren([
fileRoute,
]),
])
/* prettier-ignore-end */
/* prettier-ignore */
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
postsRoute.addChildren([
postsIndexRoute,
postRoute,
]),
postEditorRoute,
settingsRoute.addChildren([
profileRoute,
notificationsRoute,
]),
pathlessLayoutRoute.addChildren([
pathlessLayoutARoute,
pathlessLayoutBRoute,
]),
filesRoute.addChildren([
fileRoute,
]),
])
/* prettier-ignore-end */
但在构建路由树前,需要理解基于代码路由的核心概念。
实际上,基于文件的路由是代码路由的超集,它通过文件系统和代码生成抽象自动创建上述路由结构。
假设您已阅读路由核心概念指南并熟悉以下概念:
接下来我们将展示如何在代码中创建这些路由类型。
创建根路由的方式与文件路由相同,调用createRootRoute()函数即可。不同之处在于不需要强制导出根路由(虽然可以在单个文件中构建完整路由树,但仅为演示目的)。
// 标准根路由
import { createRootRoute } from '@tanstack/solid-router'
const rootRoute = createRootRoute()
// 带上下文的根路由
import { createRootRouteWithContext } from '@tanstack/solid-router'
import type { QueryClient } from '@tanstack/react-query'
export interface MyRouterContext {
queryClient: QueryClient
}
const rootRoute = createRootRouteWithContext<MyRouterContext>()
// 标准根路由
import { createRootRoute } from '@tanstack/solid-router'
const rootRoute = createRootRoute()
// 带上下文的根路由
import { createRootRouteWithContext } from '@tanstack/solid-router'
import type { QueryClient } from '@tanstack/react-query'
export interface MyRouterContext {
queryClient: QueryClient
}
const rootRoute = createRootRouteWithContext<MyRouterContext>()
关于上下文更多信息,请参阅路由上下文指南。
创建基础路由只需向createRoute提供普通路径字符串:
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'about',
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'about',
})
该路由将匹配URL路径/about。
与文件路由使用index文件名不同,代码路由使用单斜杠/表示索引路由:
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
})
const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/', // 注意此处的单斜杠
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
})
const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/', // 注意此处的单斜杠
})
此路由将匹配/posts/或/posts。
动态段的使用方式与文件路由一致。在路径段前添加$前缀,该参数将被捕获到路由的loader或component的params对象中:
const postIdRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
loader: ({ params }) => fetchPost(params.postId),
component: PostComponent,
})
function PostComponent() {
const { postId } = postIdRoute.useParams()
return <div>文章ID: {postId}</div>
}
const postIdRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
loader: ({ params }) => fetchPost(params.postId),
component: PostComponent,
})
function PostComponent() {
const { postId } = postIdRoute.useParams()
return <div>文章ID: {postId}</div>
}
Tip
若组件采用代码分割,可使用getRouteApi函数来避免导入路由配置即可获取类型化的useParams()钩子。
通配路由同样通过$前缀实现,捕获的参数存储在_splat键下:
const filesRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'files',
})
const fileRoute = createRoute({
getParentRoute: () => filesRoute,
path: '$',
})
const filesRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'files',
})
const fileRoute = createRoute({
getParentRoute: () => filesRoute,
path: '$',
})
对于URL/documents/hello-world,参数对象为:
{ '_splat': 'documents/hello-world' }
{ '_splat': 'documents/hello-world' }
布局路由通过嵌套子路由实现:
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
component: PostsLayoutComponent, // 布局组件
})
function PostsLayoutComponent() {
return (
<div>
<h1>文章列表</h1>
<Outlet />
</div>
)
}
const routeTree = rootRoute.addChildren([
postsRoute.addChildren([postsIndexRoute, postsCreateRoute]),
])
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
component: PostsLayoutComponent, // 布局组件
})
function PostsLayoutComponent() {
return (
<div>
<h1>文章列表</h1>
<Outlet />
</div>
)
}
const routeTree = rootRoute.addChildren([
postsRoute.addChildren([postsIndexRoute, postsCreateRoute]),
])
子路由内容将渲染在PostsLayoutComponent布局中。
代码路由中通过id而非path选项创建无路径路由:
const pathlessLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'pathlessLayout',
component: PathlessLayoutComponent,
})
const routeTree = rootRoute.addChildren([
pathlessLayoutRoute.addChildren([pathlessLayoutARoute, pathlessLayoutBRoute]),
])
const pathlessLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'pathlessLayout',
component: PathlessLayoutComponent,
})
const routeTree = rootRoute.addChildren([
pathlessLayoutRoute.addChildren([pathlessLayoutARoute, pathlessLayoutBRoute]),
])
子路由内容将嵌套在无路径布局组件中渲染。
实现非嵌套路由需要从目标嵌套层级开始构建完整路径:
const postEditorRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts/$postId/edit', // 完整路径
})
const routeTree = rootRoute.addChildren([
postEditorRoute, // 直接挂在根路由下
postsRoute.addChildren([postRoute]),
])
const postEditorRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts/$postId/edit', // 完整路径
})
const routeTree = rootRoute.addChildren([
postEditorRoute, // 直接挂在根路由下
postsRoute.addChildren([postRoute]),
])
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.