当人们第一次开始使用 TanStack Router 时,他们通常会围绕以下主题提出许多问题:
为什么我必须这样做?
为什么是这种方式,而不是那种方式?
我习惯了这种方式,为什么要改变?
这些都是有效的问题。大多数情况下,人们习惯使用彼此非常相似的路由库。它们都具有相似的 API、相似的概念和相似的做事方式。
但 TanStack Router 不同。它不是你普通的路由库。它不是你普通的状态管理库。它不是你普通的任何东西。
重要的是要记住,TanStack Router 的起源源于 Nozzle.io 需要一种客户端路由解决方案,该解决方案能提供一流的 *URL 搜索参数* 体验,同时不牺牲其复杂仪表板所需的 **_类型安全_**。
因此,从 TanStack Router 诞生之初,其设计的各个方面都经过精心考虑,以确保其类型安全和开发者体验首屈一指。
TypeScript!TypeScript!TypeScript!
TanStack Router 的每个方面都旨在尽可能实现类型安全,这是通过最大限度地利用 TypeScript 的类型系统来实现的。这包括使用一些非常高级和复杂的类型、类型推断和其他功能,以确保开发者体验尽可能流畅。
但为了实现这一点,我们不得不做出一些偏离路由领域规范的决定。
TLDR; 使用 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
)
}
由于这意味着您必须手动为 <Link> 组件的 to 属性进行类型标注,并且在运行时之前无法捕获任何错误,因此这不是一个可行的选择。
也许我可以将我的路由定义为嵌套对象的树形结构?
// ⛔️ 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/react-router' {
interface Register {
router: typeof router
}
}
// src/app.tsx
declare module '@tanstack/react-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/react-router'
import { postsRoute } from './postsRoute'
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
})
import { createRoute } from '@tanstack/react-router'
import { postsRoute } from './postsRoute'
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
})
在此阶段,这样做是为了让 postsIndexRoute 的定义能够知道其在路由树中的位置,并能够正确推断父路由返回的 context、path params、search params 的类型。错误定义 getParentRoute 函数意味着子路由将无法正确推断父路由的属性。
因此,这是路由配置的关键部分,如果操作不当,就会导致故障。
但这只是设置基本应用程序的一部分。TanStack Router 要求所有路由(包括根路由)都拼接成一个**_路由树_**,以便将其传递给 createRouter 函数,然后才能在模块上声明 Router 实例以进行类型推断。这是路由配置的另一个关键部分,如果操作不当,就会导致故障。
🤯 如果对于一个拥有约 40-50 个路由的应用程序,这个路由树放在它自己的文件中,它很容易增长到 700 多行。
const routeTree = rootRoute.addChildren([
postsRoute.addChildren([postsIndexRoute, postsIdRoute]),
])
const routeTree = rootRoute.addChildren([
postsRoute.addChildren([postsIndexRoute, postsIdRoute]),
])
随着您开始使用路由器更多功能,例如嵌套上下文、加载器、搜索参数验证等,这种复杂性只会增加。因此,在一个文件中定义路由不再可行。因此,用户最终会构建自己的 *半一致* 的方式来跨多个文件定义路由。这可能导致路由配置中的不一致和错误。
最后,是代码分割的问题。随着应用程序的增长,您会希望代码分割您的组件以减小应用程序的初始捆绑包大小。当您在一个文件甚至跨多个文件定义路由时,这可能是一个令人头疼的管理问题。
import { createRoute, lazyRouteComponent } from '@tanstack/react-router'
import { postsRoute } from './postsRoute'
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
component: lazyRouteComponent(() => import('../page-components/posts/index')),
})
import { createRoute, lazyRouteComponent } from '@tanstack/react-router'
import { postsRoute } from './postsRoute'
export const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
component: lazyRouteComponent(() => import('../page-components/posts/index')),
})
所有这些样板代码,无论对于提供一流的类型推断体验多么重要,都可能有点让人不知所措,并可能导致路由配置中的不一致和错误。
... 而这个示例配置仅仅是为了渲染一个单一的代码分割路由。想象一下,要为 40-50 个路由执行此操作。现在请记住,您还没有涉及 context、loaders、search param validation 和路由器的其他功能 🤕。
那么,为什么基于文件的路由是首选方式呢?
TanStack Router 的基于文件的路由旨在解决所有这些问题。它允许您以可预测的方式定义路由,易于管理和维护,并且随着应用程序的增长而具有可扩展性。
基于文件的路由方法由 TanStack Router Bundler 插件提供支持。它执行 3 个基本任务,解决了使用基于代码的路由时路由配置中的痛点:
让我们看看上一个示例的路由配置在使用基于文件的路由时会是什么样子。
// src/routes/posts/index.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/')({
component: () => 'Posts index component goes here!!!',
})
// src/routes/posts/index.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/')({
component: () => 'Posts index component goes here!!!',
})
就这样!无需担心定义 getParentRoute 函数、拼接路由树或代码分割您的组件。TanStack Router Bundler 插件会为您处理所有这些。
TanStack Router Bundler 插件在任何时候都不会剥夺您对路由配置的控制权。它旨在尽可能灵活,允许您以适合您应用程序的方式定义路由,同时减少路由配置的样板代码和复杂性。
您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。