TanStack Start Static Server Functions:构建期预执行 + 静态缓存的全栈数据加速方案
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
Static Server Functions(静态服务端函数)是 TanStack Start 中一类特殊的服务端函数:它们在构建期预渲染阶段执行一次,将结果作为静态 JSON 资产缓存进构建产物,客户端挂载后所有后续调用直接改走 fetch 静态文件,从而把"服务器计算"彻底转变为"静态资源分发"。本文以官方指南 static-server-functions.md 为主体,结合仓库内 start-static-server-functions 包源码、e2e 测试 与 start-basic-static 示例 展开讲解,读完你将掌握该中间件的接入方式、运行机制、缓存键规则、与静态预渲染(SSG)的配合方式及其实验性限制。
背景:为什么需要把 Server Function 变成"静态"
TanStack Start 的 Server Functions 允许你定义只在服务端运行的逻辑,并从 loaders、组件、hooks 或其他服务端函数中类型安全地调用它。默认情况下,每次客户端调用都会通过 RPC 走到真实服务器执行 handler。
但在使用 静态预渲染(Static Prerendering) 部署纯静态站点时,服务器可能根本不存在——或者你希望那些内容固定、不含用户身份的数据完全绕开服务器,直接由 CDN/静态托管平台分发。Static Server Functions 正是为这种场景设计的:在构建期把 handler 执行掉,把结果以静态 JSON 形式缓存进产物,运行时零服务器计算。
适用前提非常明确:该 Server Function 必须是确定性、无副作用、可公开缓存的——例如读取一份公开的文章列表、拉取外部公开 API 的数据并固化到产物中。任何依赖会话、Cookie、请求头或用户身份的 handler,都不应使用本方案(public缓存导致的跨租户数据泄漏风险请参考 Server Functions 指南中的 Cache-Control 安全说明)。
快速接入:给 createServerFn 挂上 staticFunctionMiddleware
文档给出的最小用法如下:
import { createServerFn } from '@tanstack/react-start' import { staticFunctionMiddleware } from '@tanstack/start-static-server-functions' const myServerFn = createServerFn({ method: 'GET' }) .middleware([staticFunctionMiddleware]) .handler(async () => { return 'Hello, world!' })关键约束(来自原文档的警告):staticFunctionMiddleware必须是最后一个(final)middleware。原因是该中间件需要接管"执行结果并写入缓存"与"从缓存读取结果"这两端,若其后还有其它中间件,则缓存内容与真实最终响应之间会出现不一致。
从仓库源码看,该包导出即这一中间件本体(index.ts),其 peer 依赖同时声明了@tanstack/react-start与@tanstack/solid-start(均为可选),因此该模式在 React 与 Solid 的 Start 应用中都可使用;包依赖@tanstack/start-client-core(提供createMiddleware与默认 seroval 插件)和seroval(负责跨端序列化),引擎要求 Node.js >= 22.12.0(见 package.json)。
组合其它中间件:顺序很重要
官方示例start-basic-static中展示了与自定义日志中间件共存的方式(posts.tsx):
import { logMiddleware } from './loggingMiddleware' import { staticFunctionMiddleware } from '@tanstack/start-static-server-functions' export const fetchPosts = createServerFn({ method: 'GET' }) .middleware([logMiddleware, staticFunctionMiddleware]) .handler(async () => { return axios .get<Array<PostType>>('https://jsonplaceholder.typicode.com/posts') .then((r) => r.data.slice(0, 10)) })logMiddleware在前负责日志/统计等通用横切逻辑,staticFunctionMiddleware殿后负责缓存读写——与"final middleware"约束一致。同样注意:示例中的 handler 使用 GET 方法,且数据来自公开 API(jsonplaceholder),符合"可静态化"的前提。
工作原理:构建期执行 → 静态缓存 → 客户端水合 → fetch 替换
原文档给出了一条清晰的时序,整理如下:
Build-time(构建期/预渲染期)
- 静态预渲染过程中,带
staticFunctionMiddleware的 Server Function 被执行一次; - 执行结果被写入构建产物,成为静态 JSON 文件,键由函数 ID + 参数/载荷哈希推导得出;
- 该结果同时作为正常返回值参与预渲染,用于把页面 HTML 静态生成出来。
- 静态预渲染过程中,带
Runtime(运行时)
- 初始访问时,浏览器直接拿到预渲染好的 HTML,其中已内嵌该 Server Function 的数据;
- 客户端挂载后,内嵌数据完成水合(hydration);
- 此后任何客户端的再次调用,都会被替换为对那个静态 JSON 文件的
fetch请求——不再触碰真实服务器。
也就是说,这套方案天然与prerender配置绑定:只有开启静态预渲染的应用才有"构建期执行"这一环节,Static Server Functions 的缓存文件也是随预渲染一起产出的。预渲染的开启与配置方式见 静态预渲染指南(在tanstackStart({ prerender: { enabled: true, ... } })中配置autoStaticPathsDiscovery、crawlLinks、filter等)。
源码级拆解:client 端与 server 端各做什么
中间件的完整实现位于 packages/start-static-server-functions/src/staticFunctionMiddleware.ts,核心是createMiddleware({ type: 'function' })上的.client()与.server()两个分支:
服务端分支(构建期写入缓存)
.server(async (ctx) => { const response = await ctx.next() if ( process.env.NODE_ENV === 'production' && process.env.TSS_CLIENT_OUTPUT_DIR ) { await addItemToCache({ functionId: ctx.serverFnMeta.id, response: { result: (response as any).result, context: ctx }, data: ctx.data, }) } return response })- 先正常执行后续逻辑(
ctx.next())拿到结果; - 仅当
NODE_ENV === 'production'且存在TSS_CLIENT_OUTPUT_DIR(即客户端构建输出目录已确定)时,才把结果写入缓存; - 缓存内容同时包含
result(handler 返回值)与context.sendContext(发送上下文),因此响应头等上下文信息也能随静态文件一并恢复。
客户端分支(运行时读取缓存)
.client(async (ctx) => { if ( process.env.NODE_ENV === 'production' && // do not run this during SSR on the server typeof document !== 'undefined' ) { const response = await fetchItem({ functionId: ctx.serverFnMeta.id, data: ctx.data, }) if (response) { return { result: response.result, context: { ...(ctx as any).context, ...response.context }, } as any } } return ctx.next() })- 只在生产环境、且运行在浏览器环境(
typeof document !== 'undefined',避免 SSR 阶段误走缓存)时读取; - 命中缓存则直接返回缓存结果;未命中则回退到
ctx.next()走常规 RPC; - 该分支同时维护了一个内存级
staticClientCache(Map<string, any>),减少重复请求(见源码 staticFunctionMiddleware.ts 中的staticClientCache)。
缓存键与产物路径:函数 ID + 参数哈希
这是理解整个方案的关键细节。缓存文件名由两段信息推导:
- 函数 ID(functionId):即 Server Function 在构建时生成的稳定函数 ID。关于 ID 的生成与自定义方式(默认 SHA-256、冲突去重后缀、
serverFns.generateFunctionId自定义等),请参考 Server Functions 指南 中的 "Function ID generation for production build" 一节。 - 参数哈希(hash):对传入的
data(即参数/载荷)做一次"文件名安全化"处理得到的字符串。
最终 URL 由源码中的getStaticCacheUrl生成:
const getStaticCacheUrl = async (opts: { functionId: string; hash: string }) => { const filename = await sha1Hash(`${opts.functionId}__${opts.hash}`) return `/__tsr/staticServerFnCache/${filename}.json` }即:把函数ID__参数哈希拼接后做 SHA-1 哈希,得到最终文件名,产物落在客户端输出目录下的/__tsr/staticServerFnCache/<hash>.json(实际写入时通过path.join(process.env.TSS_CLIENT_OUTPUT_DIR, url)落盘)。
实现细节值得注意(源码注释明确说明):
- 参数哈希并非直接序列化,而是先把 JSON 的对象键排序(
sortedKeysReplacer),再替换掉文件名非法字符(/ \ ? % * : | " < >→-,空白 →_),以保证同一份参数无论键顺序如何都映射到同一个缓存文件,最大化缓存命中率; - 这里使用的 SHA-1仅用于缩短缓存文件名,非加密用途,不应用于任何安全场景(源码 JSDoc 中有明确警示)。
数据落盘前会经过toJSONAsync(..., { plugins: getDefaultSerovalPlugins() })序列化(读取时用fromJSON反序列化),因此非 JSON 原生类型(Date、Map、Set 等 seroval 支持的类型)同样能被正确缓存与还原——这一层正是依赖@tanstack/start-client-core提供的默认 seroval 插件(源码见 staticFunctionMiddleware.ts 的addItemToCache/fetchItem)。
完整落地示例:loader 中的静态 Server Function
e2e 测试目录 e2e/react-start/static-server-functions 提供了可直接对照的最小完整用例,路由posts.tsx展示了"静态化 Server Function + loader 消费"的标准写法:
import { Outlet, createFileRoute } from '@tanstack/react-router' import { createServerFn } from '@tanstack/react-start' import { staticFunctionMiddleware } from '@tanstack/start-static-server-functions' type Post = { id: string title: string } const fetchPosts = createServerFn({ method: 'GET' }) .middleware([staticFunctionMiddleware]) .handler(async () => { return [ { id: '1', title: 'First Post' }, { id: '2', title: 'Second Post' }, { id: '3', title: 'Third Post' }, ] as Array<Post> }) export const Route = createFileRoute('/posts')({ loader: async () => fetchPosts(), component: PostsComponent, }) function PostsComponent() { const posts = Route.useLoaderData() return ( <div> <h2 contenteditable="false">【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).
项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考