首页 / TanStack 生态入门教程 / 部署与生产环境

TanStack 生态入门教程

部署与生产环境

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

TanStackTanStack 生态入门教程TanStack Start部署环境变量中间件SEOSentry

25. 部署与生产环境

本节目标:学会把 TanStack Start 应用部署到生产环境。掌握平台选择、环境变量配置、中间件使用、SEO 优化、错误边界、可观测性。学完你能把应用安全稳定地上线,并且出问题时能快速定位。

25.1 部署平台选择

TanStack Start 设计上支持任何托管平台。官方推荐三个合作伙伴:Cloudflare、Netlify、Railway。

Cloudflare Workers

边缘计算平台,全球节点,延迟低。需要额外配置:

  1. 安装依赖:
npm install -D @cloudflare/vite-plugin wrangler
  1. 配置 Vite:
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { cloudflare } from '@cloudflare/vite-plugin'
import viteReact from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    cloudflare({ viteEnvironment: { name: 'ssr' } }),
    tanstackStart(),
    viteReact(),
  ],
})
  1. 添加 wrangler.jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "tanstack-start-app",
  "compatibility_date": "2025-09-02",
  "compatibility_flags": ["nodejs_compat"],
  "main": "@tanstack/react-start/server-entry"
}
  1. 部署:
npx wrangler login    # 登录
npm run build          # 构建
npx wrangler deploy    # 部署
Warning

Cloudflare Workers 是边缘运行时,process.env 在模块加载时为空(按请求注入)。环境变量必须在 handler 内部读取,不能在模块级别读。

Netlify

全栈平台,内置 CDN 和 Server Functions:

  1. 安装插件:
npm install -D @netlify/vite-plugin-tanstack-start
  1. 配置 Vite:
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import netlify from '@netlify/vite-plugin-tanstack-start'
import viteReact from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    tanstackStart(),
    netlify(),
    viteReact(),
  ],
})
  1. 部署:
npx netlify deploy

或手动配置 netlify.toml

[build]
  command = "vite build"
  publish = "dist/client"
[dev]
  command = "vite dev"
  port = 3000

Node.js / Docker

最通用的部署方式,用 Nitro 作为服务器层:

  1. 安装 Nitro:
npm install nitro
  1. 配置 Vite:
// vite.config.ts
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { defineConfig } from 'vite'
import { nitro } from 'nitro/vite'
import viteReact from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [tanstackStart(), nitro(), viteReact()],
})
  1. 配置 scripts:
{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "start": "node .output/server/index.mjs"
  }
}
  1. 构建和启动:
npm run build
npm run start
Tip

Node.js 部署可以配合 Docker 打包成镜像,部署到任何容器平台(AWS ECS、Kubernetes、Fly.io 等)。

Vercel

使用 Nitro 部署,跟 Node.js 方式一样配 Nitro 插件,然后连接 Vercel 仓库自动部署。

Bun

Bun 运行时部署,性能更好(需要 React 19):

// vite.config.ts
export default defineConfig({
  plugins: [tanstackStart(), nitro({ preset: 'bun' }), viteReact()],
})
bun run build
bun run server.ts

25.2 环境变量

服务端 vs 客户端

环境变量分两种:

  • 服务端变量:无前缀,只能在 Server Functions 和服务端代码里用
  • 客户端变量:Vite 用 VITE_ 前缀,Rsbuild 用 PUBLIC_ 前缀,客户端能用
# .env
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb    # 服务端
JWT_SECRET=super-secret-key                                 # 服务端
VITE_APP_NAME=我的应用                                       # 客户端
VITE_API_URL=https://api.example.com                        # 客户端

服务端读取

import { createServerFn } from '@tanstack/react-start'

const getUser = createServerFn().handler(async () => {
  // 直接用 process.env,任何变量都行
  const db = await connect(process.env.DATABASE_URL)
  return db.user.findFirst()
})

客户端读取

function AppHeader() {
  // 只能读 VITE_ 前缀的
  return <h1>{import.meta.env.VITE_APP_NAME}</h1>
}
Warning

import.meta.env.DATABASE_URL 在客户端是 undefined。这是安全特性,防止密钥泄漏。不要给敏感变量加 VITE_ 前缀。

环境文件层级

Start 自动按顺序加载:

.env.local          # 本地覆盖(加入 .gitignore)
.env.production     # 生产环境
.env.development    # 开发环境
.env                # 默认值(提交到 git)

后面的文件覆盖前面的同名变量。

按请求读取(边缘运行时)

Note

Cloudflare Workers 等边缘运行时按请求注入环境变量。模块加载时 process.env 还是空的。必须在 handler 内部读:

// ❌ 模块级别读取(边缘运行时会 undefined)
const apiKey = process.env.API_SECRET

// ✅ 在 handler 内读取
const getData = createServerFn().handler(async () => {
  const apiKey = process.env.API_SECRET
  return fetchExternalData(apiKey)
})

类型安全

用 TypeScript 声明和 Zod 校验:

// src/env.d.ts
interface ImportMetaEnv {
  readonly VITE_APP_NAME: string
  readonly VITE_API_URL: string
  readonly VITE_SENTRY_DSN?: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      readonly DATABASE_URL: string
      readonly JWT_SECRET: string
      readonly STRIPE_SECRET_KEY: string
      readonly NODE_ENV: 'development' | 'production' | 'test'
    }
  }
}

25.3 中间件

中间件用来处理认证、日志、CORS 等,在 Server Functions 和 API 路由之前执行。

两种中间件

类型作用范围方法
请求中间件所有服务端请求.server()
Server Function 中间件仅 Server Functions.client() + .server()

创建请求中间件

import { createMiddleware } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware().server(
  async ({ next, request }) => {
    console.log(`请求: ${request.method} ${request.url}`)
    const startTime = Date.now()

    const result = await next()

    const duration = Date.now() - startTime
    console.log(`响应: ${duration}ms`)

    return result
  },
)

认证中间件

const authMiddleware = createMiddleware().server(
  async ({ next, context }) => {
    // 从请求中获取 token
    const token = getRequestHeader('authorization')?.replace('Bearer ', '')

    if (!token) {
      throw new Error('未认证')
    }

    const user = await verifyToken(token)

    // 注入到上下文,后续中间件和 handler 能用
    return next({ context: { user } })
  },
)

在 Server Function 上使用

export const getProfile = createServerFn({ method: 'GET' })
  .middleware([authMiddleware])
  .handler(async ({ context: { user } }) => {
    // user 从 authMiddleware 的上下文来,类型安全
    return fetchUserProfile(user.id)
  })

全局注册

src/start.ts 里注册全局请求中间件:

import { createStart } from '@tanstack/react-start'

export const startInstance = createStart(() => ({
  requestMiddleware: [loggingMiddleware, csrfMiddleware],
}))
Tip

中间件可以组合。一个中间件可以依赖另一个,形成链式执行。顺序很重要:认证 -> 日志 -> 实际处理。

25.4 SEO 优化

head 管理

路由的 head 属性控制页面 head 标签:

// src/routes/index.tsx
export const Route = createFileRoute('/')({
  head: () => ({
    meta: [
      { title: '我的应用 - 首页' },
      { name: 'description', content: '欢迎来到我的应用' },
    ],
  }),
  component: HomePage,
})

动态 meta 标签

用 loader 数据生成动态 SEO 信息:

// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    return { post }
  },
  head: ({ loaderData }) => ({
    meta: [
      { title: loaderData.post.title },
      { name: 'description', content: loaderData.post.excerpt },
    ],
  }),
  component: PostPage,
})

Open Graph 和社交分享

head: ({ loaderData }) => ({
  meta: [
    { title: loaderData.post.title },
    { name: 'description', content: loaderData.post.excerpt },
    // Open Graph
    { property: 'og:title', content: loaderData.post.title },
    { property: 'og:description', content: loaderData.post.excerpt },
    { property: 'og:image', content: loaderData.post.coverImage },
    { property: 'og:type', content: 'article' },
    // Twitter Card
    { name: 'twitter:card', content: 'summary_large_image' },
    { name: 'twitter:title', content: loaderData.post.title },
    { name: 'twitter:image', content: loaderData.post.coverImage },
  ],
}),
Note

Start 默认做 SSR,搜索引擎能拿到完整渲染的 HTML。配合静态预渲染,SEO 效果更好。

根路由 head

在根路由设置全局 head:

// src/routes/__root.tsx
export const Route = createRootRoute({
  head: () => ({
    meta: [
      { charSet: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { title: '我的应用' },
      { name: 'description', content: '应用描述' },
    ],
  }),
})

子路由的 head 会合并到根路由的 head 里。

25.5 错误边界

全局默认错误组件

在路由器上设置:

// src/router.tsx
import { createRouter, ErrorComponent } from '@tanstack/react-router'

export function getRouter() {
  const router = createRouter({
    routeTree,
    defaultErrorComponent: ({ error, reset }) => (
      <ErrorComponent error={error} />
    ),
  })
  return router
}

按路由自定义

// src/routes/posts.$postId.tsx
import { createFileRoute, ErrorComponent } from '@tanstack/react-router'
import type { ErrorComponentProps } from '@tanstack/react-router'

function PostError({ error, reset }: ErrorComponentProps) {
  return (
    <div>
      <p>加载文章失败:{error.message}</p>
      <button onClick={reset}>重试</button>
    </div>
  )
}

export const Route = createFileRoute('/posts/$postId')({
  component: PostComponent,
  errorComponent: PostError,
})

loader 错误处理

loader 抛的错误会被错误边界捕获:

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    if (!post) {
      throw new Error('文章不存在')
    }
    return { post }
  },
  errorComponent: PostError,
  component: PostPage,
})
Tip

reset() 会重置错误边界。如果是 loader 出错,用 router.invalidate() 更好,它会重新执行 loader 并重置错误边界。

25.6 可观测性

Sentry 集成

官方推荐 Sentry 做错误追踪和性能监控:

客户端

// src/app.tsx
import * as Sentry from '@sentry/react'

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: process.env.NODE_ENV,
})

服务端

import * as Sentry from '@sentry/node'

const getUser = createServerFn().handler(async () => {
  try {
    return await riskyOperation()
  } catch (error) {
    Sentry.captureException(error)
    throw error
  }
})

请求日志中间件

用中间件统一记录所有请求:

import { createMiddleware } from '@tanstack/react-start'

const requestLogger = createMiddleware().server(
  async ({ request, next }) => {
    const startTime = Date.now()
    const { method, url } = request

    try {
      const result = await next()
      const duration = Date.now() - startTime
      console.log(`[${method}] ${url} - ${duration}ms`)
      return result
    } catch (error) {
      const duration = Date.now() - startTime
      console.error(`[${method}] ${url} - ERROR after ${duration}ms`, error)
      throw error
    }
  },
)

Server Function 日志

const getUser = createServerFn({ method: 'GET' })
  .validator((id: string) => id)
  .handler(async ({ data: id }) => {
    const startTime = Date.now()
    console.log(`[SERVER] 获取用户 ${id}`)

    try {
      const user = await db.users.findUnique({ where: { id } })
      const duration = Date.now() - startTime
      console.log(`[SERVER] 用户 ${id} 获取完成,耗时 ${duration}ms`)
      return user
    } catch (error) {
      const duration = Date.now() - startTime
      console.error(`[SERVER] 用户 ${id} 获取失败,耗时 ${duration}ms`, error)
      throw error
    }
  })

25.7 生产环境检查清单

上线前对照检查:

环境变量

  • 所有敏感变量没有 VITE_/PUBLIC_ 前缀
  • .env.local 已加入 .gitignore
  • 生产环境变量已配置到托管平台
  • 必需变量在启动时校验
  • 源码里没有硬编码密钥

安全

  • Server Functions 有 CSRF 保护(createCsrfMiddleware
  • 认证数据缓存头用 private
  • Server Function 输入有校验(validator
  • 敏感操作的 Server Function 有认证中间件

性能

  • 静态页面做了预渲染
  • CDN 缓存头配置正确
  • 大页面用了 deferred-hydration
  • 数据库连接用了连接池

可观测性

  • 错误追踪已接入(Sentry 等)
  • 请求日志中间件已配置
  • 关键操作有日志记录
  • 生产环境 NODE_ENV=production

部署

  • npm run build 能成功
  • 构建产物检查过(没有服务端代码泄漏到客户端)
  • 部署后页面能正常访问
  • Server Functions 能正常调用

25.8 小结

这是 TanStack 生态教程 Router + Start 部分的最后一章:

  • 部署平台:Cloudflare Workers(边缘)、Netlify(全栈)、Node.js/Docker(通用)、Vercel、Bun
  • 环境变量:无前缀是服务端专用,VITE_ 前缀客户端可用,边缘运行时按请求读取
  • 中间件:请求中间件处理所有请求,Server Function 中间件处理 RPC,可组合可链式
  • SEOhead 属性管理 meta 标签,支持动态数据和 Open Graph
  • 错误边界:全局 defaultErrorComponent + 按路由 errorComponent
  • 可观测性:Sentry 集成 + 请求日志中间件 + Server Function 日志

整个 TanStack 生态教程到这就收尾了。从 Query 的数据获取、Router 的类型安全路由、Start 的全栈能力,一路走下来该讲的都讲了。剩下的就是动手练—这些库我用了好几年,最大的感受是「类型安全这东西,用过就回不去」。希望这些内容能帮到你。