资讯详情

TanStack Solid Start 执行模型全解:同构优先、执行边界控制与安全实践

📅 2026/9/15 15:33:05 | 华诺云谱 👁 阅读
TanStack Solid Start 执行模型全解:同构优先、执行边界控制与安全实践
TanStack Solid Start 执行模型全解同构优先、执行边界控制与安全实践【免费下载链接】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本文基于仓库文档 execution-model.md 展开系统讲解 TanStack Start 在 Solid 生态中的执行模型为什么所有代码默认同构、如何通过createServerFn/createServerOnlyFn/createClientOnlyFn/createIsomorphicFn/ClientOnly/useHydrated精准控制代码在服务端与客户端的执行位置并结合仓库源码说明这些 API 的底层实现原理最终给出环境变量安全、水合一致性等实战反模式与决策框架。读完本文你将能正确判断一段业务代码应该运行在哪一侧并能独立排查服务端代码泄漏到客户端 bundle、水合不匹配等典型问题。核心原则默认同构Isomorphic by Default理解 TanStack StartSolid 版应用的第一个关键认知是所有代码默认同构——除非被显式约束否则代码会同时被打进服务端 bundle 和客户端 bundle并在两端运行。// ✅ 这段函数同时运行在服务端和客户端 function formatPrice(price: number) { return new Intl.NumberFormat(en-US, { style: currency, currency: USD, }).format(price) } // ✅ 路由 loader 也是同构的 export const Route createFileRoute(/products)({ loader: async () { // SSR 期间在服务端运行客户端导航时在浏览器运行 const response await fetch(/api/products) return response.json() }, })关键理解路由loader是同构的——它既在服务端运行也会在客户端运行而不是只在服务端运行。这一点是整个执行模型中最容易出错、也最容易引发安全事故的认知盲区。执行边界两个运行时环境TanStack Start 应用运行在两个环境中它们拥有不同的能力与资源服务端环境ServerNode.js 运行时可访问文件系统、数据库、环境变量SSR 期间——首次页面渲染在服务端完成API 请求——服务端函数server functions在服务端执行构建期——静态生成与预渲染pre-rendering。客户端环境Client浏览器运行时可访问 DOM、localStorage、用户交互水合之后——客户端接管首次服务端渲染的页面导航期间——路由 loader 在客户端侧运行用户交互——事件处理器、表单提交等。两个环境的能力差异决定了执行控制 API 的取舍服务端有敏感数据与资源客户端有 DOM 与交互而纯逻辑格式化、业务计算则可以在两端安全运行。执行控制 API 全景仓库文档给出了两类控制 API 的速查表本文将其合并为一张完整对照表并补充服务端行为列API使用场景客户端行为服务端行为createServerFn()RPC 调用、数据变更通过网络请求发往服务端直接执行createServerOnlyFn(fn)工具函数仅服务端抛出错误直接执行createClientOnlyFn(fn)浏览器工具函数直接执行抛出错误createIsomorphicFn()按环境提供不同实现使用.client()实现使用.server()实现ClientOnly依赖浏览器 API 的组件渲染子节点渲染 fallbackuseHydrated()依赖水合状态的行为水合后返回true始终返回false服务端专属执行import { createServerFn, createServerOnlyFn } from tanstack/solid-start // RPC服务端执行客户端可调用客户端调用会变成网络请求 const updateUser createServerFn({ method: POST }) .validator((data: UserData) data) .handler(async ({ data }) { // 只在服务端运行但客户端可以调用它 return await db.users.update(data) }) // 工具函数仅服务端客户端调用即崩溃 const getEnvVar createServerOnlyFn(() process.env.DATABASE_URL)createServerFn是服务端代码的主要入口在客户端 bundle 中调用它会转换为对服务端的网络请求在服务端它被直接执行。这是 TanStack Start 实现同构调用服务端逻辑的核心机制。客户端专属执行import { createClientOnlyFn } from tanstack/solid-start import { ClientOnly } from tanstack/solid-router // 工具函数仅客户端服务端调用即崩溃 const saveToStorage createClientOnlyFn((key: string, value: any) { localStorage.setItem(key, JSON.stringify(value)) }) // 组件水合之后才渲染子节点 function Analytics() { return ( ClientOnly fallback{null} GoogleAnalyticsScript / /ClientOnly ) }useHydrated HookuseHydrated返回一个 Solid accessorsignal用于判断客户端是否已完成水合为依赖水合状态的行为提供更细粒度的控制import { useHydrated } from tanstack/solid-router function TimeZoneDisplay() { const hydrated useHydrated() const timeZone () hydrated() ? Intl.DateTimeFormat().resolvedOptions().timeZone : UTC return divYour timezone: {timeZone()}/div }行为特征SSR 期间始终返回false首次客户端渲染返回false水合之后返回true且后续所有渲染都保持true。这在需要根据浏览器端数据时区、locale、localStorage做条件渲染、同时为服务端渲染提供合理回退时非常有用。源码级原理查看 packages/solid-router/src/ClientOnly.tsx 可以看到useHydrated的实现——模块级globalHydrated变量配合Solid.createSignal(globalHydrated !Solid.sharedConfig.context)初始化并在onMount中置为true。其中Solid.sharedConfig.context正是服务端渲染上下文的标志因此在 SSR 时初始值恒为false这正是文档所述行为的直接来源。而ClientOnly组件本身同文件 ClientOnly.tsx就是基于useHydrated()配合Solid.Show实现水合为真时渲染 children否则渲染fallback ?? null。按环境提供不同实现createIsomorphicFnimport { createIsomorphicFn } from tanstack/solid-start // 每个环境使用不同的实现 const getDeviceInfo createIsomorphicFn() .server(() ({ type: server, platform: process.platform })) .client(() ({ type: client, userAgent: navigator.userAgent }))createIsomorphicFn允许你为同一函数定义两份实现服务端 bundle 中编译为.server()的实现客户端 bundle 中编译为.client()的实现。从源码看packages/start-fn-stubs/src/createIsomorphicFn.ts其类型系统通过ServerOnlyFn/ClientOnlyFn接口保证链式调用的类型安全调用.server()后返回的类型只剩client()方法可接反之亦然。架构模式实战渐进增强Progressive Enhancement构建无 JavaScript 也能工作的组件再用客户端功能增强function SearchForm() { const [query, setQuery] createSignal() return ( form action/search methodget input nameq value{query()} onChange{(e) setQuery(e.target.value)} / ClientOnly fallback{button typesubmitSearch/button} SearchButton onSearch{() search(query())} / /ClientOnly /form ) }服务端与未水合时渲染原生提交按钮表单仍可用水合后替换为交互式搜索按钮。这正是ClientOnly的典型用法服务端渲染 fallback、客户端渲染完整交互组件。环境感知存储Environment-Aware Storageconst storage createIsomorphicFn() .server((key: string) { // 服务端基于文件的缓存 const fs require(node:fs) return JSON.parse(fs.readFileSync(.cache, utf-8))[key] }) .client((key: string) { // 客户端localStorage return JSON.parse(localStorage.getItem(key) || null) })RPC 与直接函数调用的取舍理解何时使用 server function、何时使用 server-only function 是正确建模的关键// createServerFnRPC 模式 —— 服务端执行客户端可调用 const fetchUser createServerFn().handler(async () await db.users.find()) // 客户端组件中的用法 const user await fetchUser() // ✅ 网络请求 // createServerOnlyFn客户端调用即崩溃 const getSecret createServerOnlyFn(() process.env.SECRET) // 客户端用法 const secret getSecret() // ❌ 抛出错误常见反模式Anti-Patterns环境变量泄漏// ❌ 泄漏到客户端 bundle const apiKey process.env.SECRET_KEY // ✅ 仅服务端可访问 const apiKey createServerOnlyFn(() process.env.SECRET_KEY)对 Loader 的错误假设// ❌ 错误地假设 loader 只在服务端运行 export const Route createFileRoute(/users)({ loader: () { // 这段代码在服务端和客户端都会运行 const secret process.env.SECRET // 已暴露给客户端 return fetch(/api/users?key${secret}) }, }) // ✅ 用 server function 承载服务端专属操作 const getUsersSecurely createServerFn().handler(() { const secret process.env.SECRET // 仅服务端 return fetch(/api/users?key${secret}) }) export const Route createFileRoute(/users)({ loader: () getUsersSecurely(), // 同构地调用 server function })值得强调的是即使不涉及密钥在 loader 中直接使用相对 URL如fetch(/api/...)也是危险的同构 loader 在 SSR 阶段没有可靠的 base URL。正确做法是在 loader 中调用 server function或把 fetch 放进显式的环境边界内。水合不匹配Hydration Mismatch// ❌ 服务端与客户端渲染内容不同 function CurrentTime() { return div{new Date().toLocaleString()}/div } // ✅ 渲染保持一致 function CurrentTime() { const [time, setTime] createSignalstring() createEffect(() { setTime(new Date().toLocaleString()) }) return div{time() || Loading...}/div }水合不匹配的根源是同一组件在服务端与客户端渲染出不同内容。解法是让首帧渲染内容确定如占位符待水合后再通过 effect 更新为真实值——与服务端渲染输出保持一致避免 React/Solid 水合时丢弃或重渲染 DOM。手动检测 vs API 驱动的环境判断// 手动自行处理逻辑分支 function logMessage(msg: string) { if (typeof window undefined) { console.log([SERVER]: ${msg}) } else { console.log([CLIENT]: ${msg}) } } // API框架处理环境分流 const logMessage createIsomorphicFn() .server((msg) console.log([SERVER]: ${msg})) .client((msg) console.log([CLIENT]: ${msg}))手动typeof window检测虽然有效但无法让编译器做死代码消除dead code elimination——两端 bundle 都会保留完整的分支逻辑。而createIsomorphicFn让编译器能按环境裁剪掉另一侧实现这正是从源码结构看官方推荐 API 驱动方式的深层原因。架构决策框架选择 Server-OnlycreateServerFn/createServerOnlyFn当访问敏感数据环境变量、密钥文件系统操作数据库连接外部 API key。选择 Client-OnlycreateClientOnlyFn/ClientOnly当DOM 操作浏览器 APIlocalStorage、geolocation用户交互处理分析/追踪analytics/tracking。选择 Isomorphic默认 /createIsomorphicFn当数据格式化与转换业务逻辑共享工具函数路由 loader天然同构。安全考量Bundle 分析始终验证服务端专属代码没有被包含进客户端 bundle# 分析客户端 bundle npm run build # 检查 dist/client 中是否存在服务端专属的 import环境变量策略客户端可见使用VITE_前缀例如import.meta.env.VITE_API_URL服务端专属通过createServerOnlyFn()或createServerFn()的 handler 内访问process.env永不暴露数据库 URL、API key、任何密钥。补充一个常被忽略的坑不要在模块顶层读取process.env。这有两个层面的错误——一是安全层面模块级读取可能被内联进客户端 bundle二是运行时正确性层面在 Cloudflare Workers 等边缘运行时中env 是按请求注入的模块加载时读取会得到undefined即使在服务端。正确的做法是始终在.handler()或其他按请求执行的函数内部读取环境变量。错误边界优雅地处理服务端/客户端执行错误function ErrorBoundary(props) { return ( ErrorBoundaryComponent fallback{divSomething went wrong/div} onError{(error) { if (typeof window undefined) { console.error([SERVER ERROR]:, error) } else { console.error([CLIENT ERROR]:, error) } }} {props.children} /ErrorBoundaryComponent ) }底层原理Start 编译器如何实现执行边界了解为什么这些 API 能按环境裁剪有助于写出更正确的代码。仓库中 packages/start-plugin-core/src/start-compiler/config.ts 定义了编译器对四类工厂函数的查找配置lookup config工厂函数Kind说明createServerFnRoot编译为 RPC 调用客户端/直接执行服务端createIsomorphicFnIsomorphicFn按环境替换为.server()/.client()实现createServerOnlyFnServerOnlyFn服务端保留原实现客户端替换为抛错createClientOnlyFnClientOnlyFn客户端保留原实现服务端替换为抛错编译器在两端 bundle 中做差异化改写这也是为什么 packages/start-fn-stubs/src/envOnly.ts 中createServerOnlyFn/createClientOnlyFn的运行时实现只是恒等函数(fn) fn——真正的边界逻辑在编译期完成客户端 bundle 中调用会抛错服务端保留原样。同样ClientOnly的 JSX 变换ClientOnlyJSX只在服务端构建时启用见 compiler.ts 与 config.ts服务端渲染时移除 children、渲染 fallback客户端则保留 children。这与上文useHydrated的运行时行为相互印证构成了编译期裁剪 运行时信号的双层保障。如果希望以更粗粒度约束整个文件还可以使用文件名后缀约定如db.server.ts或在文件顶部添加import tanstack/solid-start/server-only/import tanstack/solid-start/client-only标记让 import protection 在构建期拒绝跨环境导入详见 start-plugin-core 的 envOnly 测试。小结TanStack Solid Start 的执行模型可以浓缩为一条原则与一组工具同构优先isomorphic by default提供了灵活性与开发效率而createServerFn/createServerOnlyFn/createClientOnlyFn/createIsomorphicFn/ClientOnly/useHydrated这套执行控制 API 在需要精准控制时提供了明确边界。理解 loader 的同构本质、环境变量的暴露规则、水合一致性的要求是构建安全、高性能、可维护的 TanStack Start 应用的基石。本文涉及的执行模型文档位于 docs/start/framework/solid/guide/execution-model.mdSolid Router 侧的水合组件实现可继续阅读 packages/solid-router/src/ClientOnly.tsx编译期边界机制可深入 packages/start-plugin-core/src/start-compiler/compiler.ts。【免费下载链接】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),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。