Start 服务端与部署
这一篇是 TanStack Start 真正进入项目决策层的地方。一个框架能不能落地,最后拼的不是首页 demo,而是这几件事:
- 页面怎么渲
- 数据和服务端逻辑怎么放
- 运行时和部署是不是顺手
- 出了问题你能不能定位
1. 渲染主线
官方文档给 Start 的默认叙述是:它能做 full-document SSR 和 streaming。这意味着它不是只在客户端补水,而是真的能在服务端先把文档级 HTML 渲出来。
默认 SSR 是什么感觉
按官方 Selective SSR 文档的描述,路由命中首个请求时:
beforeLoad和loader会先在服务端执行- 路由组件随后在服务端渲染
- 产出的 HTML 发 给浏览器
- 浏览器再完成 hydrate
这个流程对 SEO、首屏速度和首个请求的数据直出都很重要。
一个最小 SSR 入口大致长这样
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
export default createServerEntry({
requestHandler: handler,
})
真实项目里你不一定天天改这里,但你最好知道:服务端入口不是黑盒。Start 允许你在这一层继续扩展,而不是把请求处理死锁在固定模板里。
Streaming 解决什么问题
SSR 本身不是万能药。只要某些数据慢,整个页面就可能被卡住。
官方把 streaming 当成重点能力,就是因为它允许你先把壳发出去,再把慢数据对应的部分一点点送到客户端。这个思路如果你熟悉 React Suspense,会比较容易代入。
Selective SSR 很值得注意
这是 Start 跟很多“默认整页 SSR 或整页 CSR”的方案相比,比较有意思的一点。
它允许你按路由控制:
- 哪些路由继续走服务端执行
- 哪些路由只保留数据层服务端处理
- 哪些路由干脆纯客户端
这对混合型应用很实用。比如后台图表页、依赖浏览器 API 的编辑器页,就没必要强行让它们和营销页用同一种 SSR 策略。
SPA Mode 不是“放弃服务端”
官方 SPA Mode 文档里有一句很实在:不用 SSR,不等于不能继续用服务端能力。
也就是说,在 Start 里你即便把页面主体验证成 SPA:
- 仍然可以保留
Server Functions - 仍然可以保留
Server Routes - 仍然可以做预渲染 shell
这很适合那些不太需要 SEO,但又想保留同仓服务端能力的后台系统。
一段最常见的 Start 插件配置
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
export default defineConfig({
plugins: [
tanstackStart({
prerender: {
routes: ['/', '/docs', '/blog/posts/*'],
crawlLinks: true,
},
}),
react(),
],
})
这段配置有两个很实用的点:
- 哪些路由要预渲染,可以显式写出来
crawlLinks: true可以沿着链接继续扩展预渲染范围
2. Server Functions
如果只记一句话:
Server Functions 是 Start 最有辨识度的一层能力之一。
官方文档对它的定义很直接:你可以把“只该在服务端执行的逻辑”定义出来,然后从组件、loader、hooks 或其他 server function 里调用它;调用端和服务端之间仍保持类型约束。
一个最小示例
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
const CreatePostInput = z.object({
title: z.string().min(1),
})
export const createPost = createServerFn({ method: 'POST' })
.inputValidator(CreatePostInput)
.handler(async ({ data }) => {
return {
id: crypto.randomUUID(),
title: data.title,
}
})
组件里调用时会很像在调本地函数:
import { useState } from 'react'
import { createPost } from '../server/create-post'
export function PostForm() {
const [title, setTitle] = useState('')
async function handleSubmit() {
const result = await createPost({
data: { title },
})
console.log('created', result.id)
}
return (
<div>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button onClick={handleSubmit}>创建</button>
</div>
)
}
这就是 Start 很讨人喜欢的一点:服务端逻辑离前端很近,但边界又没有糊掉。