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(...)和@importURL
它不会重写你应用中的每一个 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_ORIGIN 是 https://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',
}}
/>如果 transformAssets 和 assetCrossOrigin 都设置了跨域值,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 })对象形式接受这些属性:
| 属性 | 类型 | 说明 |
|---|---|---|
transform | string | (asset) => string | { href, crossOrigin? } | Promise<...> | 字符串前缀或回调,与上面的简写形式相同。 |
createTransform | (ctx: { warmup: true } | { warmup: false; request: Request }) => (asset) => string | { href, crossOrigin? } | Promise<...> | 异步工厂,每次 manifest 计算运行一次,返回一个按资源的 transform。与 transform 互斥。 |
cache | boolean | 是否缓存转换后的 manifest。默认 true。 |
warmup | boolean | 为 true 时,在生产环境的服务器启动时预热缓存的 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.svg,kind: '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 的客户端入口脚本解析。
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 走 CDN | CDN,相对于入口模块 |
当你对 Vite 使用 transformAssets,并且希望首屏加载资源和客户端导航 chunk 从同一个 CDN 提供时,使用 base: ''。
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 控制。
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。
import { defineConfig } from '@rsbuild/core'
export default defineConfig({
output: {
assetPrefix: 'https://cdn.example.com/',
},
})对于 CDN 来源在构建时已知的 Rsbuild 构建,使用 rsbuild.config.ts 中的 output.assetPrefix。