ByteNoteByteNote

字节笔记本

2026年5月3日

TanStack Start - Execution Model 执行模型

API中转
¥120

理解代码在哪里运行是构建 TanStack Start 应用的基础。本文介绍同构执行模型、执行边界和控制 API。

核心原则:默认同构

TanStack Start 中的所有代码默认都是同构的 - 除非明确限制,否则代码在服务端和客户端包中都会运行和包含。

tsx
// ✅ 在服务端和客户端上都运行
function formatPrice(price: number) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(price)
}

// ✅ Route loaders 是同构的
export const Route = createFileRoute('/products')({
  loader: async () => {
    // SSR 期间在服务端运行,导航期间在客户端运行
    const response = await fetch('/api/products')
    return response.json()
  },
})

Route loader同构的 - 它们在服务端和客户端上都运行,不仅仅在服务端。

执行边界

TanStack Start 应用在两个环境中运行:

服务端环境

  • Node.js 运行时,可访问文件系统、数据库、环境变量
  • SSR 期间 - 初始页面在服务端渲染
  • API 请求 - 服务端函数在服务端执行
  • 构建时 - 静态生成和预渲染

客户端环境

  • 浏览器运行时,可访问 DOM、localStorage、用户交互
  • 水合后 - 初始服务端渲染后客户端接管
  • 导航 - 导航期间路由加载器在客户端运行
  • 用户交互 - 事件处理、表单提交等

执行控制 API

仅服务端执行

API用途客户端行为
createServerFn()RPC 调用、数据变更向服务端发网络请求
createServerOnlyFn(fn)工具函数抛出错误
tsx
import { createServerFn, createServerOnlyFn } from '@tanstack/react-start'

// RPC: 服务端执行,客户端可调用
const updateUser = createServerFn({ method: 'POST' })
  .inputValidator((data: UserData) => data)
  .handler(async ({ data }) => {
    return await db.users.update(data)
  })

// 工具: 仅服务端,客户端调用会崩溃
const getEnvVar = createServerOnlyFn(() => process.env.DATABASE_URL)

仅客户端执行

API用途服务端行为
createClientOnlyFn(fn)浏览器工具抛出错误
<ClientOnly>需要浏览器 API 的组件渲染 fallback
tsx
import { createClientOnlyFn } from '@tanstack/react-start'
import { ClientOnly } from '@tanstack/react-router'

// 工具: 仅客户端,服务端调用会崩溃
const saveToStorage = createClientOnlyFn((key: string, value: any) => {
  localStorage.setItem(key, JSON.stringify(value))
})

// 组件: 仅在水合后渲染子元素
function Analytics() {
  return (
    <ClientOnly fallback={null}>
      <GoogleAnalyticsScript />
    </ClientOnly>
  )
}

useHydrated Hook

tsx
import { useHydrated } from '@tanstack/react-router'

function TimeZoneDisplay() {
  const hydrated = useHydrated()
  const timeZone = hydrated
    ? Intl.DateTimeFormat().resolvedOptions().timeZone
    : 'UTC'

  return <div>Your timezone: {timeZone}</div>
}

行为:

  • SSR 期间: 始终返回 false
  • 首次客户端渲染: 返回 false
  • 水合后: 返回 true

环境特定实现

tsx
import { createIsomorphicFn } from '@tanstack/react-start'

const getDeviceInfo = createIsomorphicFn()
  .server(() => ({ type: 'server', platform: process.platform }))
  .client(() => ({ type: 'client', userAgent: navigator.userAgent }))

架构模式

渐进增强

tsx
function SearchForm() {
  const [query, setQuery] = useState('')

  return (
    <form action="/search" method="get">
      <input
        name="q"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <ClientOnly fallback={<button type="submit">Search</button>}>
        <SearchButton onSearch={() => search(query)} />
      </ClientOnly>
    </form>
  )
}

RPC vs 直接函数调用

tsx
// createServerFn: RPC 模式 - 服务端执行,客户端可调用
const fetchUser = createServerFn().handler(async () => await db.users.find())

// 从客户端组件使用:
const user = await fetchUser() // ✅ 网络请求

// createServerOnlyFn: 客户端调用会崩溃
const getSecret = createServerOnlyFn(() => process.env.SECRET)

// 从客户端使用:
const secret = getSecret() // ❌ 抛出错误

常见反模式

环境变量暴露

tsx
// ❌ 暴露到客户端 bundle
const apiKey = process.env.SECRET_KEY

// ✅ 仅服务端访问
const apiKey = createServerOnlyFn(() => process.env.SECRET_KEY)

错误的 Loader 假设

tsx
// ❌ 假设 loader 仅在服务端运行
export const Route = createFileRoute('/users')({
  loader: () => {
    const secret = process.env.SECRET // 暴露到客户端
    return fetch(`/api/users?key=${secret}`)
  },
})

// ✅ 对服务端操作使用服务端函数
const getUsersSecurely = createServerFn().handler(() => {
  const secret = process.env.SECRET // 仅服务端
  return fetch(`/api/users?key=${secret}`)
})

export const Route = createFileRoute('/users')({
  loader: () => getUsersSecurely(),
})

水合不匹配

tsx
// ❌ 服务端 vs 客户端内容不同
function CurrentTime() {
  return <div>{new Date().toLocaleString()}</div>
}

// ✅ 一致的渲染
function CurrentTime() {
  const [time, setTime] = useState<string>()

  useEffect(() => {
    setTime(new Date().toLocaleString())
  }, [])

  return <div>{time || 'Loading...'}</div>
}

安全考虑

环境变量策略

  • 客户端暴露: 使用 VITE_ 前缀表示客户端可访问变量
  • 仅服务端: 通过 createServerOnlyFn()createServerFn() 访问
  • 永不暴露: 数据库 URL、API 密钥、密钥

API 速查

API环境用途
createServerFn()ServerRPC 调用,客户端可调用
createServerOnlyFn()Server仅服务端工具函数
createClientOnlyFn()Client仅客户端工具函数
createIsomorphicFn()Both环境特定实现
<ClientOnly>Client条件渲染组件
useHydrated()Both水合状态检测
分享: