发布于: -/最后更新: -/7 分钟/

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

摘要

TanStack Start 是一个基于 TanStack Router 的全栈框架,提供了强大的类型安全路由系统和服务端生态。它支持文件即路由,路由文件结构灵活,支持扁平化和嵌套目录。TanStack Start 的类型安全是其最引以为傲的特点,包括路径参数和搜索参数的验证。它还提供了 API 路由和 Server Functions,允许在前端代码中调用后端逻辑。TanStack Start 默认开启服务端渲染(SSR),支持 Data-Only SSR 模式,解决了 Hydration Mismatch 问题。总的来说,TanStack Start 是一个功能完备的全栈框架,适合喜欢 TypeScript 和极致开发体验的开发者。

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,为你生成类型定义。不仅如此,它还能帮你自动补全样板代码,省去了手写的繁琐。

一个标准的页面文件结构如下:

TSX
import { createFileRoute } from '@tanstack/react-router'

// 1. 路由声明:这里定义路由的配置,如加载器、参数验证等
export const Route = createFileRoute('/')({
  component: Home,
})

// 2. 页面组件
function Home() {
  return (
    <div className="p-2">
      <h3>Hello, TanStack Start!</h3>
    </div>
  )
}

处理嵌套路由与布局

如果你需要像 /post/$id 及其子页面 /post/$id/edit 这样的结构,并且希望它们共享同一个父级布局(Layout),推荐使用目录结构:

Plain Text
routes/
└── post/
    ├── $id/
    │   ├── route.tsx   // 布局文件 (Layout),处理公共逻辑或 UI
    │   ├── index.tsx   // 详情页 (/post/123)
    │   └── edit.tsx    // 编辑页 (/post/123/edit)

类型安全的参数魔法

TanStack Start 最引以为傲的就是其极致的类型安全。

路径参数 (Path Params)

定义带参数的路由非常简单,直接在文件名中使用 $ 前缀即可:

TSX
// 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 <div>Post ID: {id}</div>
}

搜索参数 (Search Params)

对于 URL 中的查询参数(如 ?page=1&sort=desc),TanStack Start 结合 zod 提供了强大的验证机制:

TSX
// 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 <div>Current Page: {page}</div>
}

API 路由

除了页面路由,你也可以轻松定义纯服务端的 API 路由:

TSX
// 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

TSX
// 默认是 GET 请求,适合数据获取
import { createServerFn } from '@tanstack/start'

export const getGreeting = createServerFn({ method: 'GET' })
  .handler(async () => {
    return "Hello from server!"
  })

在加载器 (Loader) 中使用

Loader 是在路由加载之前执行的数据获取函数。配合 Server Function,体验非常丝滑:

TSX
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 <div>{data}</div>
}

处理变更 (Mutation)

对于修改数据的操作(POST),我们可以结合 input 验证:

TSX
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 }
  })

在组件中调用:

TSX
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 里预取,在组件里消费。

TSX
// 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 <article><h2>{post.title}</h2></article>;
}

这套模式的好处是:Loader 负责 SSR 预取,useSuspenseQuery 负责客户端导航时的加载,全程享受缓存加持。

渲染模式:SSR 与 CSR

TanStack Start 默认开启 SSR (服务端渲染)

  1. 首屏加载:服务端执行 Loader,渲染 HTML 返回给浏览器。

  2. 水合 (Hydration):浏览器加载 JS,"激活"页面,使其有了交互能力。

  3. 后续导航:变为 SPA (单页应用) 模式,Loader 在客户端执行(通过 API 调用 Server Function)。

特殊模式:Data-Only SSR

有时候,你可能希望利用服务端的网络优势来获取数据,但组件本身严重依赖浏览器环境(比如使用了 window 对象或大型 Canvas 库)。

这时,你可以使用 ssr: 'data-only' 模式:

TSX
export const Route = createFileRoute('/chart')({
  ssr: 'data-only', // 关键配置
  loader: () => fetchDataFromServer(), // Loader 依然在服务端执行
  component: ChartComponent, // 组件只在客户端渲染
})

在这个模式下,数据会在服务端预取并注入到 HTML 中,但组件的 HTML 结构不会在服务端生成,而是等到客户端 JS 执行时才渲染。

解决 Hydration Mismatch

SSR 开发中常见的一个问题是"水合不匹配",比如服务端和客户端时区不一致导致的时间显示差异。

TanStack Router 贴心地提供了一个 <ClientOnly> 组件来解决这个问题:

TSX
import { ClientOnly } from '@tanstack/react-router'

function TimeDisplay() {
  return (
    <ClientOnly fallback={<span>Loading time...</span>}>
      {() => <span>{new Date().toLocaleString()}</span>}
    </ClientOnly>
  )
}

这样,包裹的内容就只会在客户端渲染,彻底杜绝了 Hydration Error。

结语

TanStack Start 给我的感觉是既稳重又现代。它没有像某些框架那样通过黑魔法隐藏一切细节,而是通过类型安全和清晰的 API 让你掌控全局。如果你喜欢 TypeScript 和极致的开发体验,TanStack Start 绝对值得一试。

正文结束