Server Functions
本教程共 38 篇 · 第 23 篇 · 更新于 2026-07-27 · 约 16 分钟阅读
23. Server Functions
本节目标:搞懂 TanStack Start 的 Server Functions。学会用
createServerFn定义服务端函数、用validator校验输入、在客户端类型安全调用、流式传输数据、理解四种代码执行模式。学完你能在客户端像调普通函数一样调服务端代码,类型安全、输入有校验、不泄露服务端代码。
23.1 什么是 Server Functions
前端需要调后端接口时,传统做法是写 API 端点 + fetch 请求。手动拼 URL、序列化参数、处理响应、维护类型。又繁琐又容易出错。
Server Functions 换了个思路:在服务端定义函数,客户端直接调用。Start 自动处理网络请求、序列化、类型推导。
import { createServerFn } from '@tanstack/react-start'
// 服务端定义
export const getServerTime = createServerFn().handler(async () => {
// 这段代码只在服务端执行
return new Date().toISOString()
})
// 客户端调用(像普通函数一样)
const time = await getServerTime()
客户端调 getServerTime(),实际发了一个 HTTP 请求到服务端,服务端执行函数返回结果。但写代码的体验跟调普通函数一样,类型也完全对上。
NoteServer Functions 是给 Start 应用内部用的(同源请求)。如果需要给外部调用的公开 API,用 Server Routes。
23.2 创建 Server Function
基本用法
用 createServerFn() 创建,可以指定 HTTP 方法:
import { createServerFn } from '@tanstack/react-start'
// GET 请求(默认)
export const getData = createServerFn().handler(async () => {
return { message: 'Hello from server!' }
})
// POST 请求
export const saveData = createServerFn({ method: 'POST' }).handler(async () => {
return { success: true }
})
- GET:适合读取数据,可被浏览器缓存
- POST:适合写入/变更数据,默认不缓存
链式 API
createServerFn 用链式调用来配置:
export const createUser = createServerFn({ method: 'POST' })
.validator(UserSchema) // 1. 输入校验
.middleware([authMiddleware]) // 2. 中间件(下一章详讲)
.handler(async ({ data, context }) => {
// 3. 处理逻辑
// data:校验后的输入,类型安全
// context:中间件注入的上下文
return db.users.create(data)
})
顺序是:校验 -> 中间件 -> 处理。每一步的类型都自动推导到下一步。
23.3 输入校验:validator
Server Functions 跨网络边界,客户端传来的数据不可信。validator 负责校验和转换输入。
基本参数
export const greetUser = createServerFn({ method: 'GET' })
.validator((data: { name: string }) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
// 客户端调用
await greetUser({ data: { name: '张三' } })
客户端调用时传 { data: { name: '张三' } },data 是固定的参数名。
用 Zod 校验
手动校验太原始,用 Zod 更强大:
import { z } from 'zod'
const UserSchema = z.object({
name: z.string().min(1, '名字不能为空'),
age: z.number().min(0, '年龄不能为负'),
email: z.string().email('邮箱格式不对'),
})
export const createUser = createServerFn({ method: 'POST' })
.validator(UserSchema)
.handler(async ({ data }) => {
// data 类型自动推导,完全匹配 Zod schema
// name: string, age: number, email: string
return db.users.create({
name: data.name,
age: data.age,
email: data.email,
})
})
// 客户端调用
await createUser({
data: { name: '张三', age: 25, email: 'zhangsan@example.com' },
})
Zod 校验失败的错误会自动序列化传给客户端,try/catch 能接住。
处理表单数据
HTML 表单提交的是 FormData,Server Functions 也支持:
export const submitForm = createServerFn({ method: 'POST' })
.validator((data) => {
if (!(data instanceof FormData)) {
throw new Error('期望 FormData')
}
return {
name: data.get('name')?.toString() || '',
email: data.get('email')?.toString() || '',
}
})
.handler(async ({ data }) => {
// data 已经是 { name, email } 对象
return { success: true }
})
Tip
FormData只能在POST方法的 Server Function 里用。GET请求不支持。
序列化类型检查
Server Functions 的输入和输出要跨网络传输,TypeScript 会检查它们是否可序列化:
// ❌ 返回类型不可序列化(包含函数)
export const badFn = createServerFn().handler(async () => {
return { fn: () => 42 } // 报错:函数不能序列化
})
// ✅ 返回可序列化的数据
export const goodFn = createServerFn().handler(async () => {
return { value: 42 } // OK
})
Note默认是
strict模式,检查输入和输出的可序列化性。如果需要关闭,传strict: false。但除非你知道为什么需要,别关。
23.4 在哪里调用 Server Functions
Server Functions 可以从这些地方调用:
在 loader 里调用
最适合数据加载的场景:
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
})
loader 在服务端(SSR)和客户端(导航时)都会执行。getPosts 在服务端执行,客户端调用时自动走网络请求。
在组件里调用
配合 TanStack Query 使用:
function PostList() {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: () => getPosts(),
})
return (
<ul>
{data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
在事件处理器里调用
按钮点击、表单提交:
function DeleteButton({ postId }: { postId: string }) {
const handleDelete = async () => {
await deletePost({ data: { id: postId } })
// 刷新数据
router.invalidate()
}
return <button onClick={handleDelete}>删除</button>
}
在其他 Server Function 里调用
Server Functions 可以互相组合:
const getUser = createServerFn().handler(async () => {
return db.users.findFirst()
})
const getUserPosts = createServerFn().handler(async () => {
const user = await getUser() // 调用另一个 Server Function
return db.posts.findMany({ where: { userId: user.id } })
})
Tip在服务端环境里,Server Function 之间的调用不走网络请求,直接执行函数。性能很好。
23.5 文件组织
项目大了之后,Server Functions 需要好好组织。推荐这种结构:
src/utils/
├── users.functions.ts # Server Function 包装(createServerFn)
├── users.server.ts # 服务端专用代码(数据库查询等)
└── schemas.ts # 共享的校验 schema(客户端安全)
.functions.ts:导出createServerFn包装,任何地方都能 import.server.ts:服务端专用代码,只在 Server Function handler 里 import.ts(无后缀):客户端安全的代码(类型、schema、常量)
具体示例
// src/utils/schemas.ts - 共享 schema
import { z } from 'zod'
export const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
export type User = z.infer<typeof UserSchema>
// src/utils/users.server.ts - 服务端专用
import { db } from '../db'
// 这个文件只在服务端执行
export async function findUserById(id: string) {
return db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, id),
})
}
// src/utils/users.functions.ts - Server Function
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { findUserById } from './users.server'
export const getUser = createServerFn({ method: 'GET' })
.validator(z.object({ id: z.string() }))
.handler(async ({ data }) => {
return findUserById(data.id)
})
静态导入是安全的
Server Functions 可以在任何文件里静态导入,包括客户端组件:
// ✅ 安全:构建时会把服务端代码替换成 RPC 桩
import { getUser } from '~/utils/users.functions'
function UserProfile({ id }: { id: string }) {
const { data } = useQuery({
queryKey: ['user', id],
queryFn: () => getUser({ data: { id } }),
})
// ...
}
构建过程会把 Server Function 的实现替换成 RPC 调用桩。实际的服务端代码不会进入客户端 bundle。
Warning不要用动态导入调 Server Function,会导致打包问题:
// ❌ 别这么写 const { getUser } = await import('~/utils/users.functions')
23.6 错误处理与重定向
抛出错误
Server Function 里抛的错误会自动序列化传给客户端:
export const riskyFunction = createServerFn().handler(async () => {
if (Math.random() > 0.5) {
throw new Error('出错了!')
}
return { success: true }
})
// 客户端
try {
await riskyFunction()
} catch (error) {
console.log(error.message) // "出错了!"
}
重定向
用 redirect() 在 Server Function 里跳转页面,常用于认证:
import { redirect } from '@tanstack/react-router'
export const requireAuth = createServerFn().handler(async () => {
const user = await getCurrentUser()
if (!user) {
// 没登录,重定向到登录页
throw redirect({ to: '/login' })
}
return user
})
客户端调用时,redirect() 会被拦截,自动执行路由跳转。
404 Not Found
资源不存在时抛 notFound():
import { notFound } from '@tanstack/react-router'
export const getPost = createServerFn()
.validator(z.object({ id: z.string() }))
.handler(async ({ data }) => {
const post = await db.findPost(data.id)
if (!post) {
throw notFound()
}
return post
})
23.7 流式传输数据
AI 应用流行后,流式传输数据变得很常见。Server Functions 支持两种方式流式传输,而且数据是类型安全的。
ReadableStream
用标准的 ReadableStream 流式传输数据:
type Message = {
content: string
}
const streamingResponseFn = createServerFn().handler(async () => {
const messages: Message[] = [
{ content: '你好,' },
{ content: '我是' },
{ content: 'AI 助手' },
]
const stream = new ReadableStream<Message>({
async start(controller) {
for (const message of messages) {
// 每条消息之间的延迟
await new Promise((resolve) => setTimeout(resolve, 500))
controller.enqueue(message)
}
controller.close()
},
})
return stream
})
客户端消费:
async function handleStream() {
const stream = await streamingResponseFn()
if (!stream) return
const reader = stream.getReader()
let done = false
while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
if (value) {
// value 类型是 Message,类型安全
console.log(value.content)
}
}
}
Async Generator(推荐)
更简洁的方式是用 async generator:
const streamingWithGenerator = createServerFn().handler(
async function* () {
const messages: Message[] = [
{ content: '你好,' },
{ content: '我是' },
{ content: 'AI 助手' },
]
for (const msg of messages) {
await new Promise((resolve) => setTimeout(resolve, 500))
yield msg // 类型安全,yield 出来的是 Message
}
},
)
客户端消费更简洁:
async function handleStream() {
for await (const msg of await streamingWithGenerator()) {
// msg 类型是 Message
console.log(msg.content)
}
}
TipAsync Generator 比 ReadableStream 代码更干净,推荐用这种方式。两种方法传输的数据都是类型安全的。
23.8 代码执行模式
Start 有四种控制代码执行位置的 API。理解它们对写对代码很重要。
核心原则:默认是同构的
Start 里的代码默认是同构的(isomorphic),在服务端和客户端都执行。
// 这个函数在服务端和客户端都执行
function formatPrice(price: number) {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
}).format(price)
}
WarningRouter 的 loader 也是同构的! 我踩过这个坑,以为 loader 只在服务端执行,在里面用了
process.env.SECRET,结果密钥泄漏到了客户端。
四种执行控制 API
| API | 用途 | 客户端行为 |
|---|---|---|
createServerFn() | RPC 调用 | 发网络请求到服务端 |
createServerOnlyFn(fn) | 服务端工具函数 | 抛错 |
createClientOnlyFn(fn) | 客户端工具函数 | 正常执行 |
createIsomorphicFn() | 不同环境不同实现 | 用客户端版本 |
这四个函数都从 @tanstack/react-start 导入。
createServerFn:RPC
服务端执行,客户端可调用(通过网络请求):
const updateUser = createServerFn({ method: 'POST' })
.validator(z.object({ id: z.string(), name: z.string() }))
.handler(async ({ data }) => {
return db.users.update(data)
})
// 客户端调用:发网络请求
await updateUser({ data: { id: '1', name: '张三' } })
createServerOnlyFn:服务端专用
只在服务端执行,客户端调了就崩。适合放敏感操作:
const getSecret = createServerOnlyFn(() => process.env.API_SECRET)
// 服务端调用:正常返回
// 客户端调用:抛错
createClientOnlyFn:客户端专用
只在客户端执行,服务端调了就崩。适合用浏览器 API:
const saveToStorage = createClientOnlyFn(
(key: string, value: unknown) => {
localStorage.setItem(key, JSON.stringify(value))
},
)
createIsomorphicFn:不同环境不同实现
同一个函数,服务端和客户端有不同的实现:
const logger = createIsomorphicFn()
.server((msg: string) => console.log(`[SERVER]: ${msg}`))
.client((msg: string) => console.log(`[CLIENT]: ${msg}`))
// 服务端调用打印 [SERVER],客户端调用打印 [CLIENT]
logger('hello')
实际用途—环境感知存储:
const storage = createIsomorphicFn()
.server((key: string) => {
const fs = require('node:fs')
const cache = JSON.parse(fs.readFileSync('.cache', 'utf-8'))
return cache[key]
})
.client((key: string) => {
return JSON.parse(localStorage.getItem(key) || 'null')
})
useHydrated Hook
有时你需要在组件里知道当前是否已 hydration 完成:
import { useHydrated } from '@tanstack/react-router'
function TimeZoneDisplay() {
const hydrated = useHydrated()
const timeZone = hydrated
? Intl.DateTimeFormat().resolvedOptions().timeZone
: 'UTC'
return <div>你的时区:{timeZone}</div>
}
- SSR 时:返回
false,显示 ‘UTC’ - hydration 完成后:返回
true,显示真实时区
怎么选
- 访问敏感数据(环境变量、密钥):
createServerOnlyFn或createServerFn - 用浏览器 API(localStorage、DOM):
createClientOnlyFn - 不同环境不同逻辑:
createIsomorphicFn - 需要客户端调用服务端:
createServerFn
Note模块级别读
process.env是错的,两个原因:一是值可能被打包进客户端 bundle(安全风险),二是 Cloudflare Workers 等运行时是按请求注入 env 的,模块加载时 env 还不存在(运行时错误)。用createServerOnlyFn包一层,每次调用时读取。
23.9 访问请求信息
Server Functions 里可以访问 HTTP 请求的信息:
import {
createServerFn,
} from '@tanstack/react-start'
import {
getRequest,
getRequestHeader,
setResponseHeaders,
setResponseStatus,
} from '@tanstack/react-start/server'
export const getPublicData = createServerFn({ method: 'GET' }).handler(
async () => {
// 读取请求头
const userAgent = getRequestHeader('user-agent')
// 设置响应头(CDN 缓存 5 分钟)
setResponseHeaders(
new Headers({
'Cache-Control': 'public, max-age=300',
}),
)
setResponseStatus(200)
return fetchPublicData()
},
)
可用工具:
getRequest()- 获取完整 Request 对象getRequestHeader(name)- 读取某个请求头setResponseHeader(name, value)- 设置单个响应头setResponseHeaders(headers)- 设置多个响应头setResponseStatus(code)- 设置 HTTP 状态码
Warning如果响应内容跟用户身份有关(读了 session/cookie),缓存头必须用
private,不能用public。否则会把一个用户的数据缓存了给另一个用户看,造成数据泄漏。
// 认证数据:必须用 private
export const getMyOrders = createServerFn({ method: 'GET' }).handler(
async () => {
const session = await requireSession()
setResponseHeaders(
new Headers({
'Cache-Control': 'private, max-age=60',
Vary: 'Cookie, Authorization',
}),
)
return db.orders.findMany({ where: { userId: session.userId } })
},
)
23.10 小结
这一章覆盖了 Server Functions 的核心内容:
- createServerFn:定义服务端函数,链式配置 validator -> middleware -> handler
- validator:用 Zod 校验输入,类型安全到 handler 里
- 调用位置:loader、组件、事件处理器、其他 Server Function 都行
- 文件组织:
.functions.ts包装函数,.server.ts服务端专用代码 - 静态导入安全:构建时自动把实现替换成 RPC 桩,服务端代码不进客户端
- 错误处理:抛错自动序列化,
redirect()跳转,notFound()404 - 流式传输:ReadableStream 或 async generator,数据类型安全
- 四种执行模式:serverFn(RPC)、serverOnlyFn(崩客户端)、clientOnlyFn(崩服务端)、isomorphicFn(各环境不同实现)
- 请求信息:可读写 HTTP 头和状态码,注意认证数据用
private缓存
下一章讲 SSR 和渲染模式,搞懂 Start 的各种渲染策略。