TanStack Start 中文文档
样式与元数据

CSS 样式

TanStack Start 支持你的打包器支持的所有 CSS 模式,并在其之上增加了感知 SSR 的路由资源发现能力。

使用本指南来选择如何在 React Start 应用中导入 CSS,以及如何配置生产环境的 CSS 行为,比如 SSR 样式表链接、早期提示(Early Hints)和 CSS 内联。

选择 CSS 模式

Start 根据你导入 CSS 的方式,对 CSS 的处理方式不同。

模式什么时候用SSR 行为生产特性
import css from './app.css?url'你想把样式表 URL 放进路由 head()head().links 渲染动态早期提示
import './global.css'你想让全局 CSS 挂在某个路由 chunk 上从匹配路由的 Start manifest 中发现静态早期提示、transformAssets、CSS 内联
import styles from './card.module.css'你想要作用域类名挂在某个路由 chunk 上从匹配路由的 Start manifest 中发现静态早期提示、transformAssets、CSS 内联

当样式表是你路由 head 输出的一部分时,用 ?url。当你希望 Start 把生成的样式表当作路由资源时,用副作用 CSS 导入或 CSS Modules。

?url 做显式样式表链接

当你希望打包器返回已发出的样式表 URL,并希望自己渲染 <link rel="stylesheet"> 时,用 ?url 导入 CSS。

// src/routes/__root.tsx
/// <reference types="vite/client" />
import { createRootRoute } from '@tanstack/react-router'
import appCss from '../styles/app.css?url'

export const Route = createRootRoute({
  head: () => ({
    links: [{ rel: 'stylesheet', href: appCss }],
  }),
})

这种模式对显式的全局样式表很有用,尤其是当你本来就想让样式表成为路由 head() 输出的一部分时。CSS 文件由打包器发出,HeadContent 把样式表链接放到最终文档中。

?url 样式表链接是路由 head 的输出,不是 Start manifest 管理的样式表资源。因此:

  • 它们会在路由 head() 运行时被发现。
  • 它们可以作为路由 head().links 条目出现在 dynamic 早期提示阶段。
  • 它们不会被 Start 的运行时 transformAssets 选项重写。
  • 它们不会被 Start 的 CSS 内联处理。

当对路由 head 的显式控制比 Start 的 manifest 管理 CSS 特性更重要时,使用这种模式。

用副作用导入做全局路由 CSS

当你希望全局选择器、全局类名或 CSS 自定义属性与某个路由或组件模块一起打包时,用不带赋值的 CSS 导入。

// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import '../styles/global.css'

export const Route = createFileRoute('/')({
  component: Home,
})

function Home() {
  return <div className="global-container">Global CSS</div>
}

类名保持全局。Start 从客户端构建中发现生成的 CSS 资源,并把它挂到匹配的路由 manifest 条目上。SSR 期间,HeadContent 为匹配的路由树渲染样式表链接,所以页面在水合之前就有样式。

你导入 CSS 的位置决定它何时被加载:

  • 在根路由或应用外壳导入,让它应用于每个页面。
  • 在布局路由导入,让它应用于该布局及其子路由。
  • 在叶子路由导入,让它只在匹配该路由时加载。
  • 只有当初次路由渲染不需要该 CSS 时,才从用 import()React.lazy 加载的组件导入。

从异步组件导入的 CSS 会随那个异步 chunk 一起加载。它不是 Start 初次路由匹配的静态路由 manifest 的一部分,所以 HeadContent 不会把它渲染为初始 SSR 样式表链接,它也不能用于静态早期提示或 CSS 内联。

副作用 CSS 导入非常适合全站 CSS 重置、设计令牌、全局工具类以及路由级全局样式。

用 CSS Modules 做作用域路由 CSS

从 Start 的路由资源发现角度看,CSS Modules 与副作用 CSS 导入的工作方式相同,但类名由打包器做作用域处理。

// src/routes/modules.tsx
import { createFileRoute } from '@tanstack/react-router'
import styles from '../styles/card.module.css'

export const Route = createFileRoute('/modules')({
  component: Modules,
})

function Modules() {
  return <div className={styles.card}>Scoped CSS module</div>
}

生成的样式表从路由 chunk 图中被发现,SSR 时为匹配路由链接,客户端导航时随路由 chunk 一起加载。

当你想要作用域类名和 Start 管理的样式表资源时,用 CSS Modules 做路由级或组件级样式。

了解 CSS 何时被发现

你选择的导入模式控制 Start 何时能看到样式表。

副作用导入和 CSS Modules 的 CSS 在构建时被发现。Start 检查客户端构建输出,记录为路由 chunk 发出的 CSS,并在 SSR 时使用这份 manifest。因为 Start 在路由加载器运行之前就已经知道这些样式表,它们可以在 static 早期提示阶段作为 rel=preload; as=style 链接发送。

?url 导入的 CSS 则不同。Start 只在路由 head() 返回链接时才看到该样式表,那发生在 router.load() 之后。这些链接仍然可以作为早期提示发送,但只能在 dynamic 阶段,与路由 head().links 中其他受支持的条目一起。

用一个经验法则:

  • 当你希望 Start 尽可能早地发现路由 CSS 时,用副作用导入或 CSS Modules。
  • 当你想要显式的路由 head() 控制、并且能接受更晚的发现时,用 ?url
  • 当你想要一条合并的、同时包含静态 manifest 资源和动态 head 链接的早期提示响应时,在 dynamic 阶段用 allLinks
  • 如果启用了 CSS 内联,被内联的 manifest 管理样式表资源会被静态早期提示跳过,因为它们已经嵌入在 HTML 中。

回调与响应头 API 见早期提示指南

在生产环境内联路由 CSS

实验性

CSS 内联是实验性功能,将来可能发生变化。

CSS 内联会把 Start manifest 管理的路由 CSS 直接嵌入到生产构建的 SSR HTML 响应中。这可以避免初始路由匹配所需 CSS 的阻塞式样式表请求,从而改善首次渲染。

在 Start 插件选项中用 server.build.inlineCss 启用它。传 true{ enabled: true, transformAssets: false } 的简写。

vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'

export default defineConfig({
  plugins: [
    tanstackStart({
      server: {
        build: {
          inlineCss: true,
        },
      },
    }),
  ],
})

这个选项只影响生产构建。开发模式继续使用 Start 正常的开发 CSS 处理。

CSS 内联适用于从副作用导入和 CSS Modules 发现的 CSS,因为这些样式表是 Start 的 manifest 资源。它不会内联用 ?url 导入并从 head().links 返回的 CSS,因为那些链接是动态的路由 head 输出,而不是 manifest 管理的样式表资源。

启用 CSS 内联时,Start 仍然会在客户端构建中发出 CSS 文件。它只改变 SSR 文档:由 Start manifest 管理的样式表链接会被替换成匹配路由的一个内联 <style> 标签。内联样式在水合期间会被保留,以避免重复的样式表链接和水合不一致。

在运行时控制内联

你可以在服务器入口点按请求控制内联。这只影响用 server.build.inlineCss 启用创建的构建。

// src/server.ts
import {
  createStartHandler,
  defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createServerEntry } from '@tanstack/react-start/server-entry'

const handler = createStartHandler({
  handler: defaultStreamHandler,
  inlineCss: ({ request }) => request.headers.get('x-inline-css') !== 'false',
})

export default createServerEntry({ fetch: handler })

对于自定义运行时包装器,handler(request, { inlineCss }) 会为那个请求覆盖 handler 级的 inlineCss 设置。

export default createServerEntry({
  fetch(request) {
    return handler(request, {
      inlineCss: request.headers.get('x-inline-css') !== 'false',
    })
  },
})

URL 重写(URL Rebasing)

如果被内联的样式表包含相对的 url(...)@import 引用,Start 会在嵌入 CSS 之前,相对于已发出的 CSS 资源 URL 重新定基。

例如,当生成的样式表从 /_build/assets/dashboard.css 提供时:

.card {
  background-image: url('./dot.svg');
}

Start 会把它嵌入为:

.card {
  background-image: url(/_build/assets/dot.svg);
}

根相对 URL 在构建时保持不变。绝对 URL、协议相对 URL、data URL 和 hash 引用也保持不变。

如果你需要在运行时重写 CSS 内部的 URL(比如给字体或背景图片加上 CDN 来源前缀),用 server.build.inlineCss: { enabled: true, transformAssets: true } 启用 CSS URL 模板。详细的 transformAssets 行为见转换内联 CSS 内部的 URL

权衡

当匹配路由的 CSS 足够小、把它放进 HTML 比额外一次样式表请求更划算时,CSS 内联很有用。当你的初始路由加载了一个本应作为独立文件缓存的巨大全局样式表时,它可能效果较差。

启用前考虑这些权衡:

  • HTML 响应会变得更大。
  • 首屏加载的 CSS 不能再独立于 HTML 响应被缓存。
  • 严格的内容安全策略(CSP)必须允许内联样式。在路由器上配置 ssr.nonce,让 HeadContent 能把 nonce 应用到渲染出的 <style> 标签,包括内联 CSS。
  • CSS 文件仍然会被发出,供初始响应后的客户端导航和浏览器缓存使用。

当减少请求开销的价值大于更大 HTML 响应的代价(结合你的部署和路由结构)时,使用 CSS 内联。

配置生产 CSS 行为

根据你在生产环境的需求,组合使用这些选项。

需求使用
路由 head 中的显式样式表链接head().links 返回的 ?url 导入
路由 CSS 的 SSR 链接副作用导入或 CSS Modules
最早的 CSS 早期提示带静态提示的副作用导入或 CSS Modules
重定向安全的样式表早期提示带动态提示的 ?url 导入
首屏更少的阻塞式 CSS 请求server.build.inlineCss
Start 资源的运行时 CDN 重写transformAssets

On this page