TanStack Start 中文文档
渲染

CDN 资源 URL(CDN Asset URLs)

实验性

transformAssets 是实验性功能,将来可能发生变化。

当你需要 TanStack Start 在运行时重写 manifest 管理的资源 URL 时,使用本指南。最常见的场景是:从一个只有在服务器启动时才知道来源(或者每个请求来源不同)的 CDN 提供 JavaScript 和 CSS。

本指南讲的是资源 URL 重写。关于选择 CSS 导入模式和配置 CSS 内联,请看CSS 样式指南。

transformAssets 重写什么

createStartHandler 上的 transformAssets 选项会重写 Start 在其 SSR manifest 中管理的 URL:

  • JavaScript 预加载链接(module 输出用 <link rel="modulepreload">,IIFE 输出用 <link rel="preload" as="script">
  • manifest 管理 CSS 的 <link rel="stylesheet"> 标签
  • 客户端入口脚本的 URL
  • 启用 CSS URL 模板时,内联 CSS 内部的 url(...)@import URL

它不会重写你应用中的每一个 URL。特别是,它不会重写路由 head().links 中的任意条目,包括用 ?url 导入并从路由 head() 函数返回的 CSS。主要的排除项见不会重写什么

使用静态 CDN 前缀

当每个 Start 管理的资源都应该收到相同的 URL 前缀时,传一个字符串。

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

const handler = createStartHandler({
  handler: defaultStreamHandler,
  transformAssets: process.env.CDN_ORIGIN || '',
})

export default createServerEntry({ fetch: handler })

如果 CDN_ORIGINhttps://cdn.example.com,而某个资源 URL 是 /assets/index-abc123.js,Start 会渲染成 https://cdn.example.com/assets/index-abc123.js

当字符串为空或未设置时,URL 保持不变。

添加跨域属性

当你还需要在 manifest 管理的 <link> 标签上设置 crossOrigin 时,使用对象简写形式。

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

const handler = createStartHandler({
  handler: defaultStreamHandler,
  transformAssets: {
    prefix: process.env.CDN_ORIGIN || '',
    crossOrigin: 'anonymous',
  },
})

export default createServerEntry({ fetch: handler })

crossOrigin 可以是一个值应用于所有受支持的 link 类型,也可以是与 HeadContent assetCrossOrigin 形状匹配的按类型记录。

transformAssets: {
  prefix: 'https://cdn.example.com',
  crossOrigin: {
    script: 'anonymous',
    stylesheet: 'use-credentials',
  },
}

按类型记录中未列出的类型不会收到 crossOrigin 属性。字符串简写和对象简写默认都会被缓存。

你也可以从应用外壳用 HeadContent 设置跨域行为:

<HeadContent assetCrossOrigin="anonymous" />

或者:

<HeadContent
  assetCrossOrigin={{
    script: 'anonymous',
    stylesheet: 'use-credentials',
  }}
/>

如果 transformAssetsassetCrossOrigin 都设置了跨域值,assetCrossOrigin 会覆盖 transformAssets 的值。assetCrossOrigin 只作用于 manifest 管理的 script 和 stylesheet 链接,不作用于路由 head() 函数返回的任意链接。

用回调做按资源逻辑

当输出取决于资源类型或 URL 时,传一个回调。回调返回一个字符串、{ href, crossOrigin? } 或其中之一的 Promise

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

const handler = createStartHandler({
  handler: defaultStreamHandler,
  transformAssets: (asset) => {
    const href = `https://cdn.example.com${asset.url}`

    if (asset.kind === 'script') {
      return {
        href,
        crossOrigin: 'anonymous',
      }
    }

    return { href }
  },
})

export default createServerEntry({ fetch: handler })

kind 字段告诉你正在被转换的资源 URL 类型。

kind说明
'script'JavaScript 预加载或客户端入口脚本 URL
'stylesheet'manifest 管理的 CSS 样式表 URL
'css-url'内联 CSS 内部的 url(...)@import URL

kind === 'css-url' 时,上下文还包含 stylesheetHref,它是 CSS 内容正被内联的 manifest 样式表 href。

crossOrigin 作用于 manifest 管理的 script 和 stylesheet 标签。对于 CSS 内部的 URL,返回 { href } 等价于返回字符串。

默认情况下,回调结果在生产环境中第一次请求后会被缓存。只有当转换依赖按请求的数据时,才使用带 cache: false 的对象形式。

处理按请求的 CDN 选择

当 CDN 来源取决于当前请求(比如请求头、租户或区域)时,使用带 cache: false 的对象形式。

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

const handler = createStartHandler({
  handler: defaultStreamHandler,
  transformAssets: {
    transform: ({ kind, url }) => {
      const region = getRequest().headers.get('x-region') || 'us'
      const cdnBase =
        region === 'eu'
          ? 'https://cdn-eu.example.com'
          : 'https://cdn-us.example.com'

      if (kind === 'script') {
        return {
          href: `${cdnBase}${url}`,
          crossOrigin: 'anonymous',
        }
      }

      return { href: `${cdnBase}${url}` }
    },
    cache: false,
  },
})

export default createServerEntry({ fetch: handler })

对象形式接受这些属性:

属性类型说明
transformstring | (asset) => string | { href, crossOrigin? } | Promise<...>字符串前缀或回调,与上面的简写形式相同。
createTransform(ctx: { warmup: true } | { warmup: false; request: Request }) => (asset) => string | { href, crossOrigin? } | Promise<...>异步工厂,每次 manifest 计算运行一次,返回一个按资源的 transform。与 transform 互斥。
cacheboolean是否缓存转换后的 manifest。默认 true
warmupbooleantrue 时,在生产环境的服务器启动时预热缓存的 manifest。默认 false

当你需要为每次 manifest 计算做一次异步工作,然后用结果转换许多 URL 时,使用 createTransform

transformAssets: {
  cache: false,
  async createTransform(ctx) {
    if (ctx.warmup) {
      return ({ url }) => ({ href: url })
    }

    const region = ctx.request.headers.get('x-region') || 'us'
    const cdnBase = await fetchCdnBaseForRegion(region)

    return (asset) => {
      if (asset.kind === 'script') {
        return {
          href: `${cdnBase}${asset.url}`,
          crossOrigin: 'anonymous',
        }
      }

      return { href: `${cdnBase}${asset.url}` }
    }
  },
}

对于静态 CDN 前缀,优先用字符串或对象简写。它们更简单,并且使用默认的缓存 manifest。

转换内联 CSS 内部的 URL

当启用 Start 的 CSS 内联时,Start 也可以对内联 CSS 内容内部的 URL 运行 transformAssets。这覆盖相对和根相对的 url(...)@import 值,比如字体和背景图片。

因为 Start 不会在运行时解析 CSS,这需要选择启用构建时的 CSS URL 模板:

tanstackStart({
  server: {
    build: {
      inlineCss: {
        enabled: true,
        transformAssets: true,
      },
    },
  },
})

inlineCss: true 仍然会内联路由 CSS,但它不会发出运行时 CSS URL 转换所需的模板元数据。

相对的 CSS URL 会在你的转换运行之前,相对于已发出的样式表 href 解析。

/* emitted stylesheet href: /assets/dashboard.css */
.card {
  background-image: url('./dot.svg');
}

你的回调会收到 /assets/dot.svgkind: 'css-url'。例如,你可以从一个 CDN 来源提供 JavaScript 和 CSS 文件,从另一个来源提供内联 CSS 中引用的字体或图片 URL。

const handler = createStartHandler({
  handler: defaultStreamHandler,
  transformAssets: (asset) => {
    if (asset.kind === 'css-url') {
      return `https://static-assets.example.com${asset.url}`
    }

    return `https://cdn.example.com${asset.url}`
  },
})

asset.kind === 'css-url' 时,URL 来自内联 CSS 文件内部,比如 url(...)@import 引用。回调上下文还包含 stylesheetHref,它标识包含那个 URL 的已生成样式表。当转换需要根据来源样式表变化时使用它。

transformAssets: (asset) => {
  if (asset.kind === 'css-url') {
    const cdnBase = asset.stylesheetHref.includes('/admin-')
      ? 'https://admin-cdn.example.com'
      : 'https://cdn.example.com'

    return `${cdnBase}${asset.url}`
  }

  return `https://cdn.example.com${asset.url}`
}

CSS 内部的绝对 URL、协议相对 URL、data URL 和 hash 引用会保持不变,不会被传给 transformAssets。如果构建时没有启用 CSS URL 模板,内联 CSS 内部的 URL 在运行时保持不变。

选择何时缓存 URL 重写

在大多数应用中,每个请求的 CDN URL 都是相同的。在这种情况下保持默认的缓存行为。Start 在生产环境中计算一次转换后的 manifest,然后在后续请求中复用它。

只有当结果可能按请求变化时才关闭缓存,比如按区域、租户、请求头或 cookie 选择 CDN。

形式默认缓存行为
字符串前缀true计算一次,在生产环境中缓存。
对象简写true计算一次,在生产环境中缓存。
回调true第一次请求时运行一次,在生产环境中缓存。
cache: true 或省略的对象true同上。
cache: false 的对象false深拷贝基础 manifest,每个请求都转换。

只有当转换依赖按请求的数据时才用 cache: false。对于静态 CDN 前缀,默认的 cache: true 更快更简单。

如果你想避免在第一个用户请求期间做第一次缓存的转换,设置 warmup: true。Start 会在服务器启动时在后台计算转换后的 manifest。

transformAssets: {
  transform: process.env.CDN_ORIGIN || '',
  cache: true,
  warmup: true,
}

预热在开发模式或 cache: false 时没有效果。

注意: 在开发模式(TSS_DEV_SERVER)下,无论 cache 设置如何,缓存总是被跳过,所以你总是能拿到最新的 manifest。

让客户端导航的 chunk 留在 CDN 上

transformAssets 会重写 SSR HTML 中的 URL:script 预加载提示、stylesheet 链接和客户端入口脚本。这意味着浏览器初次页面加载可以从 CDN 获取这些资源。

当用户进行客户端导航时,TanStack Router 使用带打包器内置路径的 import() 调用懒加载路由 chunk。配置你的打包器,让这些异步 chunk URL 相对于 transformAssets 重写到 CDN 的客户端入口脚本解析。

vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  base: '',
  // ... plugins, etc.
})

使用 Vite 默认的 base: '/' 时,懒路由 chunk 路径是绝对的,比如 /assets/about-abc123.js,会相对于应用服务器来源解析,而不是 CDN。

对于 Vite 构建,设置 base: '' 让 Vite 为客户端 chunk 生成相对导入路径。

使用 base: '' 时,客户端入口脚本可以被 transformAssets 从 CDN 加载,相对的 import() 调用会相对于同一个 CDN 来源解析。这让懒加载的路由 chunk 在客户端导航期间保持在 CDN 上。

使用空字符串而不是 './' 很重要。两者都会生成相对的客户端导入,但 base: '' 会在 SSR manifest 中保留根相对路径,这样 transformAssets 才能正确地在前面加上 CDN 来源。

base 设置首屏加载的 SSR 资源客户端导航 chunk
'/'(默认)通过 transformAssets 走 CDN应用服务器
''通过 transformAssets 走 CDNCDN,相对于入口模块

当你对 Vite 使用 transformAssets,并且希望首屏加载资源和客户端导航 chunk 从同一个 CDN 提供时,使用 base: ''

rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'

export default defineConfig({
  output: {
    assetPrefix: 'auto',
  },
  // ... plugins, etc.
})

对于 Rsbuild 构建,设置 output.assetPrefix'auto',让 Rspack 从加载的客户端入口脚本派生异步 chunk URL。

使用 assetPrefix: 'auto' 时,客户端入口脚本可以被 transformAssets 从 CDN 加载,异步路由 chunk 在客户端导航期间相对于该入口脚本解析。

不会重写什么

transformAssets 重写 Start manifest 管理的资源,以及在启用 CSS URL 模板时,Start 内联进 HTML 的 CSS 内部的 URL。

它不会重写从路由 head() 函数返回的任意链接:

import { createRootRoute } from '@tanstack/react-router'
import appCss from '../styles/app.css?url'

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

如果这个样式表必须使用 CDN URL,用打包器级的选项或构建时配置来处理那个 URL。如果你想让 Start 管理生成的样式表 URL,改用副作用导入或 CSS Module 导入 CSS。见选择 CSS 模式

transformAssets 也不会重写直接在组件中导入的资源 URL:

// This import resolves to a URL at build time by your bundler.
import logo from './logo.svg'

function Header() {
  return <img src={logo} /> // This URL is not affected by transformAssets.
}

对于这些资源导入,使用你的打包器提供的 URL 控制。

vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  experimental: {
    renderBuiltUrl(filename, { hostType }) {
      if (hostType === 'js') {
        return { relative: true }
      }

      return `https://cdn.example.com/${filename}`
    },
  },
})

对于 Vite 构建,使用 vite.config.ts 中的 Vite experimental.renderBuiltUrl

rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'

export default defineConfig({
  output: {
    assetPrefix: 'https://cdn.example.com/',
  },
})

对于 CDN 来源在构建时已知的 Rsbuild 构建,使用 rsbuild.config.ts 中的 output.assetPrefix

On this page