当人们第一次开始使用 TanStack Router 时,他们经常会有很多围绕以下主题的问题
为什么我必须这样做?
为什么这样做?而不是那样做?
我习惯了这样做,为什么我要改变?
这些都是有效的问题。在大多数情况下,人们习惯于使用非常相似的路由库。它们都有类似的 API、类似的概念和类似的做法。
但是 TanStack Router 是不同的。它不是普通的路由库。它不是普通的状态管理库。它不是任何普通的库。
重要的是要记住,TanStack Router 的起源于 Nozzle.io 对客户端路由解决方案的需求,该解决方案提供一流的 URL 查询参数体验,同时又不影响驱动其复杂仪表板所需的类型安全。
因此,从 TanStack Router 的最初构想开始,其设计的每个方面都经过精心考虑,以确保其类型安全和开发者体验都是一流的。
TypeScript! TypeScript! TypeScript!
TanStack Router 的每个方面都旨在尽可能地类型安全,这是通过最大限度地利用 TypeScript 的类型系统来实现的。这涉及到使用一些非常高级和复杂的类型、类型推断和其他功能,以确保开发者体验尽可能顺畅。
但是为了实现这一点,我们不得不做出一些偏离路由领域规范的决定。
总结;在 TanStack Router 的开发者体验中所做的所有设计决策都是为了让您拥有最佳的类型安全体验,而不会影响路由配置的控制、灵活性和可维护性。
当您想要最大限度地利用 TypeScript 的推断功能时,您会很快意识到泛型是您最好的朋友。因此,TanStack Router 在各处都使用泛型,以确保尽可能多地推断路由的类型。
这意味着您必须以允许 TypeScript 尽可能多地推断路由类型的方式定义路由。
我可以使用 JSX 来定义我的路由吗?
使用 JSX 定义路由是不可行的,因为 TypeScript 将无法推断路由器的路由配置类型。
// ⛔️ This is not possible
function App() {
return (
<Router>
<Route path="/posts" component={PostsPage} />
<Route path="/posts/$postId" component={PostIdPage} />
{/* ... */}
</Router>
// ^? TypeScript cannot infer the routes in this configuration
)
}
// ⛔️ This is not possible
function App() {
return (
<Router>
<Route path="/posts" component={PostsPage} />
<Route path="/posts/$postId" component={PostIdPage} />
{/* ... */}
</Router>
// ^? TypeScript cannot infer the routes in this configuration
)
}
而且由于这意味着您必须手动键入 to 属性的 <Link> 组件,并且在运行时之前不会捕获任何错误,因此这不是一个可行的选择。
也许我可以将我的路由定义为嵌套对象树?
// ⛔️ This file will just keep growing and growing...
const router = createRouter({
routes: {
posts: {
component: PostsPage, // /posts
children: {
$postId: {
component: PostIdPage, // /posts/$postId
},
},
},
// ...
},
})
// ⛔️ This file will just keep growing and growing...
const router = createRouter({
routes: {
posts: {
component: PostsPage, // /posts
children: {
$postId: {
component: PostIdPage, // /posts/$postId
},
},
},
// ...
},
})
乍一看,这似乎是个好主意。很容易一次性可视化整个路由层次结构。但是这种方法有一些很大的缺点,使其不适合大型应用程序
当您开始使用路由器的更多功能(例如嵌套上下文、加载器、查询参数验证等)时,情况只会变得更糟。
那么,定义路由的最佳方式是什么?
我们发现定义路由的最佳方式是将路由配置的定义抽象到路由树之外。然后将您的路由配置缝合到一个有凝聚力的路由树中,然后将其传递到 createRouter 函数中。
您可以阅读更多关于基于代码的路由的内容,以了解如何以这种方式定义路由。
提示
觉得基于代码的路由有点麻烦?了解为什么基于文件的路由是定义路由的首选方式。
为什么我必须声明 Router?
这种声明的东西对我来说太复杂了...
一旦您将路由构建成树并将其传递到 Router 实例中(使用 createRouter),并且所有泛型都正常工作,那么您就需要以某种方式将此信息传递到应用程序的其余部分。
我们为此考虑了两种方法
import { router } from '@/src/app'
export const PostsIdLink = () => {
return (
<Link<typeof router> to="/posts/$postId" params={{ postId: '123' }}>
Go to post 123
</Link>
)
}
import { router } from '@/src/app'
export const PostsIdLink = () => {
return (
<Link<typeof router> to="/posts/$postId" params={{ postId: '123' }}>
Go to post 123
</Link>
)
}
这种方法的缺点是您必须将整个 Router 实例导入到要使用它的每个文件中。这可能会导致捆绑包大小增加,并且管理起来可能很麻烦,并且随着应用程序的增长以及您使用路由器的更多功能,情况只会变得更糟。
您将在应用程序中执行此操作一次。
// src/app.tsx
declare module '@tanstack/solid-router' {
interface Register {
router: typeof router
}
}
// src/app.tsx
declare module '@tanstack/solid-router' {
interface Register {
router: typeof router
}
}
然后您可以在应用程序中的任何位置受益于其自动完成功能,而无需导入它。
export const PostsIdLink = () => {
return (
<Link
to="/posts/$postId"
// ^? TypeScript will auto-complete this for you
params={{ postId: '123' }} // and this too!
>
Go to post 123
</Link>
)
}
export const PostsIdLink = () => {
return (
<Link
to="/posts/$postId"
// ^? TypeScript will auto-complete this for you
params={{ postId: '123' }} // and this too!
>
Go to post 123
</Link>
)
}
我们选择了模块声明,因为我们发现它是最可扩展和可维护的方法,并且开销和样板代码最少。
为什么文档都在推崇基于文件的路由?
我习惯在一个文件中定义我的路由,为什么我要改变?
您很快就会在 TanStack Router 文档中注意到,我们推崇基于文件的路由作为定义路由的首选方法。这是因为我们发现基于文件的路由是定义路由的最可扩展和可维护的方式。
正如开头提到的,TanStack Router 是为需要高度类型安全性和可维护性的复杂应用程序设计的。为了实现这一点,路由器的配置以精确的方式完成,以便 TypeScript 尽可能多地推断路由的类型。
使用 TanStack Router 设置基本应用程序的一个关键区别是,您的路由配置需要为 getParentRoute 提供一个函数,该函数返回当前路由的父路由。
import { createRoute } from '@tanstack/solid-router'
import { postsRoute } from './postsRoute'
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
})
import { createRoute } from '@tanstack/solid-router'
import { postsRoute } from './postsRoute'
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
})
在此阶段,这样做是为了让 postsIndexRoute 的定义能够意识到其在路由树中的位置,以便它可以正确推断父路由返回的 context、路径参数、查询参数的类型。不正确地定义 getParentRoute 函数意味着子路由将无法正确推断父路由的属性。
因此,这是路由配置的关键部分,如果操作不当,则可能会导致失败。
但这只是设置基本应用程序的一部分。TanStack Router 要求将所有路由(包括根路由)缝合到一个路由树中,以便在模块上声明 Router 实例以进行类型推断之前,可以将其传递到 createRouter 函数中。这是路由配置的另一个关键部分,如果操作不当,则可能会导致失败。
🤯 如果这个路由树在一个单独的文件中,对于一个有大约 40-50 个路由的应用程序,它很容易增长到 700 多行。
const routeTree = rootRoute.addChildren([
postsRoute.addChildren([postsIndexRoute, postsIdRoute]),
])
const routeTree = rootRoute.addChildren([
postsRoute.addChildren([postsIndexRoute, postsIdRoute]),
])
当您开始使用路由器的更多功能(例如嵌套上下文、加载器、查询参数验证等)时,这种复杂性只会增加。因此,在一个文件中定义路由不再可行。因此,用户最终构建了自己的半一致的方式来跨多个文件定义路由。这可能会导致路由配置中的不一致和错误。
最后,是代码分割的问题。随着应用程序的增长,您将需要代码分割您的组件,以减小应用程序的初始捆绑包大小。当您在一个文件甚至跨多个文件定义路由时,这可能会有点头疼。
import { createRoute, lazyRouteComponent } from '@tanstack/solid-router'
import { postsRoute } from './postsRoute'
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
component: lazyRouteComponent(() => import('../page-components/posts/index')),
})
import { createRoute, lazyRouteComponent } from '@tanstack/solid-router'
import { postsRoute } from './postsRoute'
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
component: lazyRouteComponent(() => import('../page-components/posts/index')),
})
所有这些样板代码,无论对于提供一流的类型推断体验多么重要,都可能有点让人不知所措,并可能导致路由配置中的不一致和错误。
... 而这个示例配置仅用于渲染单个代码分割的路由。想象一下为 40-50 个路由执行此操作。现在请记住,您仍然没有接触到 context、loaders、查询参数验证和路由器的其他功能 🤕。
那么,为什么基于文件的路由是首选方式呢?
TanStack Router 的基于文件的路由旨在解决所有这些问题。它允许您以可预测的方式定义路由,这种方式易于管理和维护,并且随着应用程序的增长而可扩展。
基于文件的路由方法由 TanStack Router Bundler 插件驱动。它执行 3 个基本任务,这些任务解决了在使用基于代码的路由时路由配置中的痛点
让我们看一下前面示例的路由配置在使用基于文件的路由时的样子。
// src/routes/posts/index.ts
import { createFileRoute } from '@tanstack/solid-router'
export const Route = createFileRoute('/posts/')({
component: () => 'Posts index component goes here!!!',
})
// src/routes/posts/index.ts
import { createFileRoute } from '@tanstack/solid-router'
export const Route = createFileRoute('/posts/')({
component: () => 'Posts index component goes here!!!',
})
就是这样!无需担心定义 getParentRoute 函数、缝合路由树或代码分割组件。TanStack Router Bundler 插件为您处理所有这些。
TanStack Router Bundler 插件在任何时候都不会剥夺您对路由配置的控制权。它的设计尽可能灵活,允许您以适合您应用程序的方式定义路由,同时减少路由配置的样板代码和复杂性。
您的每周 JavaScript 新闻。每周一免费发送给超过 10 万名开发者。