首页 / TanStack 生态入门教程 / Server Functions

TanStack 生态入门教程

Server Functions

本教程共 38 篇 · 第 23 篇 · 更新于 2026-07-27 · 约 16 分钟阅读

TanStackTanStack 生态入门教程TanStack StartServer FunctionscreateServerFnRPC流式传输代码执行模式

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 请求到服务端,服务端执行函数返回结果。但写代码的体验跟调普通函数一样,类型也完全对上。

Note

Server 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)
  }
}
Tip

Async Generator 比 ReadableStream 代码更干净,推荐用这种方式。两种方法传输的数据都是类型安全的。

23.8 代码执行模式

Start 有四种控制代码执行位置的 API。理解它们对写对代码很重要。

核心原则:默认是同构的

Start 里的代码默认是同构的(isomorphic),在服务端和客户端都执行。

// 这个函数在服务端和客户端都执行
function formatPrice(price: number) {
  return new Intl.NumberFormat('zh-CN', {
    style: 'currency',
    currency: 'CNY',
  }).format(price)
}
Warning

Router 的 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,显示真实时区

怎么选

  • 访问敏感数据(环境变量、密钥):createServerOnlyFncreateServerFn
  • 用浏览器 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 的各种渲染策略。