上手Tanstack Start框架，舒适的开发体验

TanStack Start 是一个基于 TanStack Router 构建的全栈框架。简单来说，TanStack Router 提供了强大的类型安全路由系统，而 TanStack Start 则是为其插上了翅膀，引入了 Server Functions 等服务端生态，使其摇身一变，成为一个功能完备的全栈框架。今天，我们就来一起领略一下它的魅力。路由系统：熟悉的配方，更强的味道文件即路由对于熟悉 Next.js 的朋友来说，TanStack Start 的上手门槛非常低。它同样采用了基于文件的路由定义，但结构上更加直观。我们可以做一个简单的类比： Next.js App Router TanStack Start 说明 app/page.tsx routes/index.tsx 首页 app/layout.tsx routes/__root.tsx 根布局 app/about/page.tsx routes/about.tsx 普通页面 TanStack Start 的路由文件结构非常灵活，它支持扁平化的文件命名（如 routes/about.tsx），也支持嵌套目录。创建你的第一个页面在开发模式下，当你创建一个文件时，Vite 插件会自动更新 routeTree.gen.ts，为你生成类型定义。不仅如此，它还能帮你自动补全样板代码，省去了手写的繁琐。一个标准的页面文件结构如下： import { createFileRoute } from '@tanstack/react-router' // 1. 路由声明：这里定义路由的配置，如加载器、参数验证等 export const Route = createFileRoute('/')({ component: Home, }) // 2. 页面组件 function Home() { return (

Hello, TanStack Start!

) } 处理嵌套路由与布局如果你需要像 /post/$id 及其子页面 /post/$id/edit 这样的结构，并且希望它们共享同一个父级布局（Layout），推荐使用目录结构： routes/ └── post/ ├── $id/ │ ├── route.tsx // 布局文件 (Layout)，处理公共逻辑或 UI │ ├── index.tsx // 详情页 (/post/123) │ └── edit.tsx // 编辑页 (/post/123/edit) 类型安全的参数魔法 TanStack Start 最引以为傲的就是其极致的类型安全。路径参数 (Path Params) 定义带参数的路由非常简单，直接在文件名中使用 $ 前缀即可： // routes/post/$id.tsx import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/post/$id')({ component: PostPage, }) function PostPage() { // 这里的 id 是完全类型安全的，IDE 会自动提示 const { id } = Route.useParams() return
Post ID: {id}
} 搜索参数 (Search Params) 对于 URL 中的查询参数（如 ?page=1&sort=desc），TanStack Start 结合 zod 提供了强大的验证机制： // routes/posts.tsx import { createFileRoute } from '@tanstack/react-router' import { z } from 'zod' export const Route = createFileRoute('/posts')({ // 定义并验证搜索参数 validateSearch: z.object({ page: z.number().optional().default(1), filter: z.string().optional(), }), component: PostsList, }) function PostsList() { // 获取到的 search 参数也是类型安全的 const { page, filter } = Route.useSearch() return
Current Page: {page}
} API 路由除了页面路由，你也可以轻松定义纯服务端的 API 路由： // routes/api/weather.ts import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/api/weather')({ server: { handlers: { GET: async ({ request }) => { const weather = await fetchWeatherFromSource() // 注意：使用 Response.json() 静态方法，返回json return Response.json({ weather }) }, }, }, }) Server Functions：打破前后端边界 Server Functions 是全栈框架的灵魂。它允许你在前端代码中像调用普通函数一样调用后端逻辑，框架会自动处理序列化、反序列化以及网络请求。相比 Next.js 的 Server Actions（主要针对 POST），TanStack Start 的 Server Functions 更加灵活，原生支持 GET 和 POST。定义 Server Function // 默认是 GET 请求，适合数据获取 import { createServerFn } from '@tanstack/start' export const getGreeting = createServerFn({ method: 'GET' }) .handler(async () => { return "Hello from server!" }) 在加载器 (Loader) 中使用 Loader 是在路由加载之前执行的数据获取函数。配合 Server Function，体验非常丝滑： import { createFileRoute } from '@tanstack/react-router' import { getGreeting } from './funcs' // 假设上面的函数定义在这里 export const Route = createFileRoute('/')({ // loader 在服务端(SSR)和客户端导航时都会执行 loader: async () => { const data = await getGreeting() return data }, component: Home, }) function Home() { // 获取 loader 返回的数据 const data = Route.useLoaderData() return
{data}
} 处理变更 (Mutation) 对于修改数据的操作（POST），我们可以结合 input 验证： import { createServerFn } from '@tanstack/start' import { z } from 'zod' export const createPostFn = createServerFn({ method: 'POST' }) .inputValidator(z.object({ title: z.string().min(1), content: z.string(), })) .handler(async ({ data }) => { // data 已经被 zod 验证过，类型安全 await db.post.create({ data }) return { success: true } }) 在组件中调用： function CreatePost() { const savePost = useServerFn(createPostFn) const handleSubmit = async () => { await savePost({ data: { title: 'New Post', content: '...' } }) } // ... } 进阶推荐：TanStack Query 虽然可以直接在 Loader 中调用 Server Function，但我强烈推荐集成 TanStack Query。既然用了 TanStack 的全家桶，就不要错过这个业界最强的状态管理库。它可以完美解决缓存、去重、后台更新等问题。TanStack Start 与 Query 的集成非常丝滑，你可以直接将 Server Function 作为 Query Fn 使用。核心思路很简单：用 queryOptions 把查询逻辑封装起来，然后在 Loader 里预取，在组件里消费。 // routes/post/$id.tsx import { createFileRoute, notFound } from "@tanstack/react-router"; import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; import { getPostByIdFn } from "./post.server"; // 1. 定义查询选项 const postQuery = (id: string) => queryOptions({ queryKey: ["post", id], queryFn: () => getPostByIdFn({ data: { id } }), }); export const Route = createFileRoute("/post/$id")({ // 2. 在 Loader 中预取数据（SSR 阶段执行） loader: async ({ context, params }) => { const post = await context.queryClient.ensureQueryData(postQuery(params.id)); if (!post) throw notFound(); }, component: PostPage, }); function PostPage() { const { id } = Route.useParams(); // 3. 在组件中消费（直接命中缓存，无需 loading 状态） const { data: post } = useSuspenseQuery(postQuery(id)); return

{post.title}

; } 这套模式的好处是：Loader 负责 SSR 预取，useSuspenseQuery 负责客户端导航时的加载，全程享受缓存加持。渲染模式：SSR 与 CSR TanStack Start 默认开启 SSR (服务端渲染)。首屏加载：服务端执行 Loader，渲染 HTML 返回给浏览器。水合 (Hydration)：浏览器加载 JS，"激活"页面，使其有了交互能力。后续导航：变为 SPA (单页应用) 模式，Loader 在客户端执行（通过 API 调用 Server Function）。特殊模式：Data-Only SSR 有时候，你可能希望利用服务端的网络优势来获取数据，但组件本身严重依赖浏览器环境（比如使用了 window 对象或大型 Canvas 库）。这时，你可以使用 ssr: 'data-only' 模式： export const Route = createFileRoute('/chart')({ ssr: 'data-only', // 关键配置 loader: () => fetchDataFromServer(), // Loader 依然在服务端执行 component: ChartComponent, // 组件只在客户端渲染 }) 在这个模式下，数据会在服务端预取并注入到 HTML 中，但组件的 HTML 结构不会在服务端生成，而是等到客户端 JS 执行时才渲染。解决 Hydration Mismatch SSR 开发中常见的一个问题是"水合不匹配"，比如服务端和客户端时区不一致导致的时间显示差异。 TanStack Router 贴心地提供了一个 组件来解决这个问题： import { ClientOnly } from '@tanstack/react-router' function TimeDisplay() { return ( Loading time...}> {() => {new Date().toLocaleString()}} ) } 这样，包裹的内容就只会在客户端渲染，彻底杜绝了 Hydration Error。结语 TanStack Start 给我的感觉是既稳重又现代。它没有像某些框架那样通过黑魔法隐藏一切细节，而是通过类型安全和清晰的 API 让你掌控全局。如果你喜欢 TypeScript 和极致的开发体验，TanStack Start 绝对值得一试。