news 2026/9/15 12:44:25

TanStack Start Static Server Functions:构建期预执行 + 静态缓存的全栈数据加速方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TanStack Start Static Server Functions:构建期预执行 + 静态缓存的全栈数据加速方案

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(构建期/预渲染期)

    1. 静态预渲染过程中,带staticFunctionMiddleware的 Server Function 被执行一次;
    2. 执行结果被写入构建产物,成为静态 JSON 文件,键由函数 ID + 参数/载荷哈希推导得出;
    3. 该结果同时作为正常返回值参与预渲染,用于把页面 HTML 静态生成出来。
  • Runtime(运行时)

    1. 初始访问时,浏览器直接拿到预渲染好的 HTML,其中已内嵌该 Server Function 的数据;
    2. 客户端挂载后,内嵌数据完成水合(hydration);
    3. 此后任何客户端的再次调用,都会被替换为对那个静态 JSON 文件的fetch请求——不再触碰真实服务器。

也就是说,这套方案天然与prerender配置绑定:只有开启静态预渲染的应用才有"构建期执行"这一环节,Static Server Functions 的缓存文件也是随预渲染一起产出的。预渲染的开启与配置方式见 静态预渲染指南(在tanstackStart({ prerender: { enabled: true, ... } })中配置autoStaticPathsDiscoverycrawlLinksfilter等)。

源码级拆解: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;
  • 该分支同时维护了一个内存级staticClientCacheMap<string, any>),减少重复请求(见源码 staticFunctionMiddleware.ts 中的staticClientCache)。

缓存键与产物路径:函数 ID + 参数哈希

这是理解整个方案的关键细节。缓存文件名由两段信息推导:

  1. 函数 ID(functionId):即 Server Function 在构建时生成的稳定函数 ID。关于 ID 的生成与自定义方式(默认 SHA-256、冲突去重后缀、serverFns.generateFunctionId自定义等),请参考 Server Functions 指南 中的 "Function ID generation for production build" 一节。
  2. 参数哈希(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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 12:42:42

不写后端,三步跑起一个类抖音短视频应用

不写后端&#xff0c;三步跑起一个类抖音短视频应用 【免费下载链接】douyin Vue3 Pinia 仿抖音&#xff0c;Vue 在移动端的最佳实践 . Imitate TikTok &#xff0c;Vue Best practices on Mobile 项目地址: https://gitcode.com/GitHub_Trending/do/douyin Douyin-Vu…

作者头像 李华
网站建设 2026/9/15 12:42:19

用 Twilio/SendGrid 为 IoT 地理围栏触发函数添加短信与邮件通知

用 Twilio/SendGrid 为 IoT 地理围栏触发函数添加短信与邮件通知 【免费下载链接】IoT-For-Beginners 12 Weeks, 24 Lessons, IoT for All! 项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners 本文围绕 IoT-For-Beginners 运输项目第 4 课&#xff08…

作者头像 李华