Start 全栈框架概览
本教程共 38 篇 · 第 22 篇 · 更新于 2026-07-27 · 约 14 分钟阅读
22. Start 全栈框架概览
本节目标:搞懂 TanStack Start 是什么、为什么需要它、跟 Next.js 有什么区别。学会用 CLI 或手动方式创建一个 Start 项目,理解项目目录结构。学完你能判断 Start 是否适合你的项目,并跑起第一个应用。
22.1 TanStack Start 是什么
前面学了 TanStack Router 的路由、loader、搜索参数、类型安全导航。Router 是一个纯客户端路由库,能做 SPA(单页应用),但做不了:
- 服务端渲染(SSR)
- 服务端 API 端点
- 客户端调用服务端函数(RPC)
- 全栈构建和部署
TanStack Start 就是补上这些能力的全栈框架。它基于 TanStack Router 构建,在 Router 的类型安全路由之上,加了 SSR、Server Functions、全栈构建等功能。
一句话定义:Start = Router + Vite + SSR + Server Functions。
两大依赖
Start 建立在两个核心技术上:
- TanStack Router:类型安全路由,Start 100% 依赖它做路由系统
- Vite 或 Rsbuild:现代构建工具,提供快速开发和优化构建
NoteStart 当前处于 v1 Release Candidate(候选发布)阶段,API 已稳定但仍在向 1.0 稳定版收尾。版本线与 Router 同步,本教程基于 v1 RC 编写。注意 Start 迭代很快,旧方案(
app.config.ts、vinxi)已过时,现以 Vite 插件方案为准。
什么时候用 Start,什么时候只用 Router
Router 已经能做很强大的 SPA。如果你确定不需要以下功能,只用 Router 就够了:
- 服务端渲染(SEO 需求)
- 服务端 API 端点
- 客户端到服务端的类型安全调用
- 全栈构建和部署
一旦需要其中任何一个,就该上 Start 了。
22.2 Start vs Next.js:两种思路
很多人会拿 Start 跟 Next.js 比。两者都是全栈 React 框架,但设计理念不同。
Next.js:平台优先
Next.js 优化的是平台集成:服务端优先渲染、与 Vercel 平台紧耦合、框架帮你做决策。
默认组件是 Server Component,不能用状态和事件处理器。要交互就加 "use client"。整个心智模型围绕服务端组件展开。
Start:开发者优先
Start 优化的是开发者控制力:类型安全无处不在、显式优于隐式、组合式原语、部署自由。
默认组件是交互式组件(传统 React),开箱就能用状态和事件处理器。Server Components 是可选的,哪里需要哪里开。
核心差异对比
| 方面 | TanStack Start | Next.js |
|---|---|---|
| 组件默认 | 交互式(传统 React) | Server Component |
| 类型安全 | 端到端,编译时检查 | 有支持,但客户端/服务端边界有断层 |
| 构建工具 | Vite 或 Rsbuild | Turbopack/Webpack |
| 缓存 | 显式 SWR 模式(Router/Query) | 多层隐式缓存 |
| 部署 | 各平台平等支持 | 针对 Vercel 优化 |
| 路由 | TanStack Router(最强类型安全) | 文件式路由,基础类型 |
| Server 函数 | 类型安全 + 输入校验 + 中间件 | Server Actions,边界无类型 |
Tip简单记:Next.js 像”全自动挡”,框架帮你做很多决策;Start 像”手动挡”,给你更多控制权。哪个好取决于你的项目和个人偏好。
缓存的区别
Next.js 的缓存是多层的:请求记忆、数据缓存、路由缓存、Router 缓存。每层有自己的失效规则,历史上改过好几次,社区反馈”难以预测”。
Start 用的是你已经熟悉的模式:
- Router 内置 SWR 缓存:
staleTime和gcTime控制 - TanStack Query:完整的数据状态管理
- CDN 缓存:标准 HTTP 缓存头
- Redis/数据库:想缓存就缓存,用你已有的基础设施
没有新的心智模型,用你已知的方式缓存数据。
22.3 创建 Start 项目
方式一:CLI 脚手架(推荐)
npx @tanstack/cli@latest create
按提示选择包管理器和可选插件(Tailwind CSS、ESLint 等),自动生成项目。
方式二:从示例克隆
# 克隆基础示例
npx gitpick TanStack/router/tree/main/examples/react/start-basic start-basic
cd start-basic
npm install
npm run dev
官方提供了不少示例:
start-basic- 基础示例start-basic-auth- 带认证start-basic-react-query- 集成 React Querystart-counter- 计数器(演示 Server Functions)start-supabase-basic- 集成 Supabasestart-clerk-basic- 集成 Clerk 认证
方式三:从零搭建
想理解每个文件的作用,可以手动搭建。
22.4 从零搭建一个 Start 项目
初始化项目
mkdir my-start-app
cd my-start-app
npm init -y
安装依赖
# Start 和 Router
npm i @tanstack/react-start @tanstack/react-router
# React
npm i react react-dom
# 构建工具(Vite)
npm i -D vite @vitejs/plugin-react
# TypeScript
npm i -D typescript @types/react @types/react-dom @types/node
配置 package.json
{
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build"
}
}
配置 Vite
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
server: {
port: 3000,
},
plugins: [
tanstackStart(),
// React 插件必须在 Start 插件之后
viteReact(),
],
})
Warning
viteReact()必须放在tanstackStart()之后。顺序反了会出问题,我踩过这个坑。
配置 TypeScript
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"module": "ESNext",
"target": "ES2022",
"skipLibCheck": true,
"strictNullChecks": true
}
}
Note不要开
verbatimModuleSyntax,否则服务端代码可能泄漏到客户端 bundle。
创建路由配置
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function getRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true,
})
return router
}
Tip必须导出
getRouter函数(不是直接导出 router 实例)。因为 SSR 环境下每个请求需要独立的 router 实例,函数确保每次调用都创建新的。
创建根路由
// src/routes/__root.tsx
import type { ReactNode } from 'react'
import {
Outlet,
createRootRoute,
HeadContent,
Scripts,
} from '@tanstack/react-router'
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: '我的 Start 应用' },
],
}),
component: RootComponent,
})
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
)
}
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}
根路由跟纯 Router 的根路由不一样,它要渲染完整的 HTML 文档(<html>、<head>、<body>),因为 Start 做 SSR,服务端要输出完整页面。
三个关键组件:
<HeadContent />:渲染<head>里的 meta、title、link 标签<Outlet />:渲染匹配的子路由<Scripts />:加载客户端 JavaScript(hydration 必须)
创建首页路由
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: HomePage,
})
function HomePage() {
return (
<div>
<h1>Hello, TanStack Start!</h1>
<p>你的第一个 Start 应用跑起来了。</p>
</div>
)
}
启动开发服务器
npm run dev
打开 http://localhost:3000,你会看到页面。第一次启动会自动生成 routeTree.gen.ts 文件。
22.5 项目目录结构
一个典型的 Start 项目长这样:
my-start-app/
├── src/
│ ├── routes/ # 路由文件(文件式路由)
│ │ ├── __root.tsx # 根路由(HTML 文档外壳)
│ │ ├── index.tsx # 首页 /
│ │ ├── about.tsx # 关于页 /about
│ │ ├── posts.tsx # 文章布局 /posts
│ │ └── posts.$postId.tsx # 文章详情 /posts/$postId
│ ├── router.tsx # 路由器配置
│ └── routeTree.gen.ts # 自动生成的路由树(别手动改)
├── vite.config.ts # Vite 配置
├── package.json
└── tsconfig.json
跟纯 Router 项目的区别
如果你之前用过纯 Router 做 SPA,Start 项目多了这些东西:
vite.config.ts里的tanstackStart()插件:处理 SSR 构建、Server Functions 编译__root.tsx渲染完整 HTML 文档:纯 Router 只渲染<div>片段getRouter()函数:而不是直接导出 router 实例<Scripts />组件:SSR hydration 必须
路由文件命名
跟纯 Router 完全一样,文件式路由的命名规则:
| 路径 | 文件名 | 类型 |
|---|---|---|
/ | index.tsx | 首页 |
/about | about.tsx | 静态路由 |
/posts/ | posts/index.tsx | 文章首页 |
/posts/$postId | posts.$postId.tsx | 动态路由 |
/rest/* | rest/$.tsx | 通配路由 |
Note
createFileRoute的路径参数是自动生成和维护的。你创建、移动、重命名路由文件时,路径会自动更新,不用手动改。
22.6 Start 的核心能力一览
后面几章会逐一深入,这里先有个全貌:
SSR(服务端渲染)
服务端渲染完整 HTML,用户首屏不用等 JavaScript 加载完才能看到内容。对 SEO 和首屏性能都重要。
Server Functions(服务器函数)
在服务端定义函数,客户端直接调用,类型安全。相当于内置的 RPC。
import { createServerFn } from '@tanstack/react-start'
// 服务端函数
const getCount = createServerFn({ method: 'GET' }).handler(() => {
return 42
})
// 客户端直接调用
const count = await getCount()
Server Routes(API 路由)
在路由文件里定义后端 API 端点,客户端可以 fetch。
Middleware(中间件)
请求级别的中间件,处理认证、日志、CORS 等。能在 Server Functions 和 API 路由上用。
Streaming(流式传输)
服务端渲染时,慢数据可以先发 HTML 骨架,数据好了再流式传输补上。用户不用等所有数据加载完。
部署自由
Start 构建出标准产物,可以部署到任何平台:
- Vercel
- Netlify
- Cloudflare
- AWS
- Fly.io
- Railway
- 你自己的服务器
不锁定平台,同一份代码到处跑。
22.7 第一个 Server Function 体验
来个简单的计数器,感受一下 Server Functions:
// src/routes/index.tsx
import * as fs from 'node:fs'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
const filePath = 'count.txt'
// 读取计数(服务端执行)
async function readCount() {
return parseInt(
await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
)
}
// GET 服务器函数:读取当前计数
const getCount = createServerFn({
method: 'GET',
}).handler(() => {
return readCount()
})
// POST 服务器函数:更新计数
const updateCount = createServerFn({ method: 'POST' })
.validator((d: number) => d)
.handler(async ({ data }) => {
const count = await readCount()
await fs.promises.writeFile(filePath, `${count + data}`)
})
export const Route = createFileRoute('/')({
component: Home,
loader: async () => await getCount(),
})
function Home() {
const router = useRouter()
const count = Route.useLoaderData()
return (
<button
type="button"
onClick={() => {
updateCount({ data: 1 }).then(() => {
// 刷新 loader 数据
router.invalidate()
})
}}
>
Add 1 to {count}?
</button>
)
}
这段代码做了什么:
getCount是服务端函数,读取文件里的数字updateCount是服务端函数,把数字加 1 写回文件loader调用getCount加载初始数据- 点击按钮调用
updateCount,然后router.invalidate()刷新
客户端代码调用的 updateCount 和 getCount,实际在服务端执行。类型安全,输入有校验。
Tip这个例子用了 Node.js 的
fs模块读写文件。Server Functions 里可以用任何服务端 API(数据库、文件系统、进程等),这些代码不会打包到客户端。
22.8 小结
这一章认识了 TanStack Start:
- 定位:基于 Router + Vite 的全栈 React 框架
- 与 Router 的关系:Start 100% 依赖 Router 做路由,在此基础上加了 SSR、Server Functions 等
- 与 Next.js 的区别:Start 是开发者优先(显式控制、类型安全、部署自由),Next.js 是平台优先(Vercel 集成、隐式优化)
- 项目结构:
src/routes/放路由,src/router.tsx配路由器,vite.config.ts配构建 - 根路由:要渲染完整 HTML 文档,包含
<HeadContent />和<Scripts /> - Server Functions:服务端定义函数,客户端类型安全调用
下一章深入 Server Functions,搞懂 createServerFn 的完整用法。