之前组内统一将请求的 Hook 统一替换成了TanStack Query,比起aHooks的useRequest来说却是要丝滑不少。打开官网的时候偶然发现了TanStack不只有Query这一个工具,还有其他的一些工具。
今天就来尝试一些TanStack Router这个工具。
总体看下来,TanStack Router比起其他路由工具,最主要的亮点是类型安全,不只是typescipt的类型约束,更主要的是path的路由安全,通过文件路径声明的路由配合自动生成的routeTree.gen.ts
文件来约束路由相关的路径,例如
user文件下声明的路由必须是’/user’
路由跳转时只能跳转已存在的路由
初始化
以 Vite + React 的环境为例,使用TanStack Router非常简单
$ pnpm install @tanstack/react-router
$ pnpm install --save-dev @tanstack/router-vite-plugin // vite 插件,用于自动生成路由文件
在vite.config.ts
中添加插件
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-vite-plugin';
export default defineConfig({
plugins: [react(), TanStackRouterVite()],
})
在src/routes
下添加文件(必须)创建index.tsx
和user.tsx
两个文件,这两个文件将会匹配/
和/user
这两个路由
此时启动devServer,将会看到src目录下生成了routeTree.gen.ts
文件,这个文件就是路由类型安全的基础。
在两个文件中添加内容
// index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: () => <div>Hello /!</div>
})
// user.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/user')({
component: () => <div>Hello /user!</div>,
});
然后创建一个__root.tsx
,这个是整个应用的根结点,所有的路由都会显示这个组件,使用createRootRoute
进行创建
import { Link, Outlet, createRootRoute } from '@tanstack/react-router';
import React from 'react';
export const Route = createRootRoute({
component: () => (
<>
<h1>TanStack Router</h1>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/user">User</Link>
</li>
</ul>
<Outlet />
</>
),
});
将App.tsx改写为以下内容,通过生成的路由文件来创建路由实例
import { RouterProvider, createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
import './App.css';
const router = createRouter({ routeTree });
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
function App() {
return <RouterProvider router={router} />;
}
export default App;
打开浏览器,可以看到这样的效果
基本的路由已经可用了,下面再来安转下开发者工具(TanStack真的很喜欢搞这些开发者工具,非常赞),安装依赖
$ pnpm install @tanstack/router-devtools
在刚才的__root.tsx
中添加开发者工具
import { Link, Outlet, createRootRoute } from '@tanstack/react-router';
import React, { Suspense } from 'react';
const TanStackRouterDevtools =
process.env.NODE_ENV === 'production'
? () => null
: React.lazy(() =>
import('@tanstack/router-devtools').then((res) => ({
default: res.TanStackRouterDevtools,
})),
)
export const Route = createRootRoute({
component: () => (
<>
<h1>TanStack Router</h1>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/user">User</Link>
</li>
</ul>
<Outlet />
<Suspense>
<TanStackRouterDevtools />
</Suspense>
</>
),
});
回到浏览器,可以看到页面上出现了一个悬浮图标,点击即可打开面板
懒加载是一种常用的减少页面白屏时间的优化手段,在首次加载页面时只加载必要的资源,后续访问到对应路由时再去请求对应的资源。
TanStack Router中开启懒加载非常简单,在文件名中添加lazy标识即可,例如
user.lazy.tsx
。
文件路由
通过上面的安装过程,我们已经了解了TanStack Router的基本使用方法,这一小节我们将继续研究它的更多路由类型。
静态路由
第一小节中我们看到的路由就是静态路由,是一种最常见的形式,会精确匹配路由路径。
动态路由
动态路由是常见的一种对某个页面传递参数的形式,例如/user/$id
的路由可以匹配/user/1
和/user/2
等路由,并且页面中可以拿到id = 1
的路由参数。
TanStack Router中使用动态路由的方式就是在文件名中添加$param
,例如文件名设置为user.$id.lazy.tsx
可以用来匹配/user/$id
路由
在组件中获取路由参数的方法如下
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/user/$id')({
component: UserComponent,
})
function UserComponent() {
const { id } = Route.useParams();
return <div>Hello user: {id}</div>
}
注意:路由跳转的时候不能直接使用/user/1
这种路径,而是应该使用模板加传参的形式,例如
<Link to="/user/$id" params={{ id: '1' }}>User</Link>
索引路由
索引路由用来绝对匹配一个路径,当有子路径时是不命中的,例如路由文件posts.index.tsx
, 可以命中/posts
,但是无法命中/posts/1
。
TanStack Router中通过在文件名中添加index标识来声明索引路由。
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/')({
component: PostsIndexComponent,
})
function PostsIndexComponent() {
return <div>Hello! posts</div>
}
通配路由
通配路由,只要与声明部分路由匹配,后面的部分不论内容是什么都可以命中,在TanStack Router 中,通配路由的表达方式是$
,例如catch.$.tsx
,可以匹配到/catch
开头的任何路径,并且后续内容会作为参数放到params中
// /catch/document/123
{
_split: 'document/123'
}
通过全局通配路由,可以实现404的兜底展示。
无路径路由
无路径路由主要处理不同路由之间存在相同layout的场景,例如
routes/
├── _pathless.tsx
├── _pathless.a.tsx
├── _pathless.b.tsx
_pathless.tsx
不参与路由匹配,实际匹配的只有_pathless.a.tsx
和_pathless.b.tsx
,分别对应/a
和/b
。
如果需要对某些文件进行文件夹归类管理或者某些自路由没有共同的layout文件时,可以使用小括号包裹
routes/
├── (pathless)
├── a.tsx
├── b.tsx
导航
TanStack Router 同市场上大部分路由工具一样提供了声明式和命令式两种路由能力(当然这也是必须的)。
Link 组件
<Link>
声明式导航,默认会渲染为一个a标签,用于声明跳转路由的页面元素。在第一小节中我们已经初步接触过了,Link 组件支持任何路由的跳转,只是不同形式的路由传参不同。
静态路由,静态路由直接写明要跳转的目标路由即可
<Link to="/about">About</Link>
动态路由,动态路由需要声明完整的路由模板,然后再加入路由参数
<Link
to="/user/$userId"
params={{
postId: '1',
}}
>
User Detail
</Link>
相对路由,Link 组件支持相对路由跳转,为了类型安全,需要手动声明当前页面的完整路径,以便计算要跳转的地址是否存在
const postIdRoute = createRoute({
path: '/blog/post/$postId',
})
const link = (
<Link from={postIdRoute.fullPath} to="../categories">
Categories
</Link>
)
除了动态路由的params参数,还支持search和hash等传参形式
const link = (
<Link
to="/search"
search={{
query: 'tanstack',
}}
hash="section-1"
>
Search
</Link>
)
Link组件支持自定义激活状态以及未激活状态,可以通过activeProps
和inactiveProps
来声明或者通过children函数的形式来自定义渲染
// 方法1
<Link
to="/use/$userId"
params={{
userId: '1',
}}
activeProps={{
style: {
fontWeight: 'bold',
},
}}
>
User Detail
</Link>
// 方法2
<Link
to="/use/$userId"
params={{
userId: '1',
}}
>
{({isActive}) => (
<span className={isActive ? 'active' : 'inactive}>User Detail</span>
)}
</Link>
是否处于active状态也可以由开发者来定义,通过activeOptions来进行控制,可以控制的内容包括:
- exact:控制是否绝对匹配(开启后存在子路由并且不完全匹配不会处于active状态)
- includeHash:进行匹配时是否包含hash部分
- includeSearch:进行匹配时是否包含search部分
<Link
to="/use/$userId"
params={{
userId: '1',
}}
activeOptions={{
exact: true
}}
>
User Detail
</Link>
预加载:TanStack Router 的 Link 组件提供了体验优化的能力,可以在鼠标 hover 时提前进行目标路由资源的加载,以减少跳转路由的白屏时间,只需要设置属性
preload='intent'
即可开启。另外,触发预加载的时间可以通过
preloadTimeout
参数控制,当鼠标悬浮指定时间后才视为有跳转意图,进行预加载。
navigate
navigate 对象是进行路由跳转的实际执行者,获得这个对象的方法有两种。
useNavigate,这个一个 Hook,功能和 React Router 类似,区别是加了TanStack Router 的特色(类型安全)。Route.navigate,使用 createFileRoute 等API创建的路由会继承一个navigate方法用于导航。
const Route = ceateFileRoute('/user/$userId/edit')({
component: Component,
})
function Component() {
const navigate = useNavigate({ from: '/user/$userId/edit' })
// const navigate = Route.navigate
const handleSubmit = async (e) => {
e.preventDefault()
const name = document.getElementsByName('name')[0]
const response = await fetch('/api/v1/user/edit', {
method: 'POST',
body: JSON.stringify({ name }),
})
const { userId } = await response.json()
if (response.ok) {
navigate({ to: '/use/$userId', params: { userId } })
}
}
}
高阶路由配置
在第一小节我们接触过了路由的几种配置方式,这一小节我们来看一下几个更高级的用法。
loader
loader 是路由配置中用于数据加载的钩子,本质上是一个函数,可以从参数中拿到路由相关的一些参数,可以进行请求,最终将结果返回供组件渲染使用。
大致如下
export const Route = createFileRoute('/user/$id')({
component: UserComponent,
loader: ({ params }) => {
console.log('[ params.id ] >', params.id)
return Promise.resolve({ username: 'feng', sex: 'man' })
}
})
function UserComponent() {
const data = Route.useLoaderData();
return <div>Hello user: {data.username}</div>
}
⚠️:loader 不可用于懒加载的路由
loader 可以获得的参数类型如下
- abortController:路由的 abortController,当路由被卸载或者 Route 不再相关且当前 loader 函数的调用变得过时时,其信号将被取消
- cause:当前路由匹配的原因,
enter
或者stay
。 - context:路由的上下文对象
- deps:Route.loaderDeps函数返回的对象值。如果Route.loaderDeps未定义,则将提供一个空对象,可以通过loaderDeps来处理search参数
- location:当前location
- params:动态路由参数
- parentMatchPromise:
Promise<void>
或者 undefined - preload:当路由正在预加载而不是加载时为true的布尔值
- route:路由本身
兜底组件
当某些状态下页面需要显示不同的兜底内容,比如加载中、页面报错、页面未找到时,可以通过以下配置项进行对应状态组件的控制
- pendingComponent:当路由处于挂起状态并已达到其挂起阈值时显示
- errorComponent:路由遇到错误内容时显示
- notFountComponent:未找到路由时显示