从 Vite SPA 到 TanStack Start SSR:一次全栈框架迁移的踩坑

test12026-09-110 次阅读

为什么要从 SPA 切到 SSR

纯 Vite + React 的 SPA 开发体验很好,但有两个硬伤一直绕不开:首屏白屏时间随包体增大而变长,以及 SEO 几乎为零——搜索引擎拿到的是空 div。当项目里出现"内容要给外部看到"的页面(官网、帮助中心、可被收录的列表),SSR 就成了必选项。

我选 TanStack Start 而不是 Next.js,是因为它和我们已经用的 TanStack Router 是一套体系,路由、数据加载、SSR 可以打通,心智负担小。

第一个坑:服务端和客户端的边界

SPA 里所有代码默认都在浏览器跑,随便用 window、document 都没事。SSR 下组件会在服务端先渲染一遍,这段环境里没有 window。最常见的崩溃是模块顶层直接读 localStorage,或者服务里直接操作 DOM。

// 错误:服务端渲染时 window 不存在
const token = window.localStorage.getItem("token");

// 正确:放到 effect 或只在客户端执行的分支里
useEffect(() => {
  const token = window.localStorage.getItem("token");
  if (token) connect(token);
}, []);

原则很简单:任何带浏览器全局对象的逻辑,要么进 useEffect,要么用 "use client" 之类边界隔离,别让它跑到服务端。

第二个坑:数据获取要交给 loader

SPA 习惯在组件里 useEffect 发请求,SSR 下这会导致"先渲染空壳再补数据",丧失 SSR 意义。TanStack Start 用 route loader 在服务端就把数据取好,跟着 HTML 一起到客户端。

export function loader() {
  return fetchPosts(); // 服务端执行,结果注入 HTML
}

export default function Posts({ props }: { props: { data: Post[] } }) {
  return <ul>{props.data.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

第三个坑:部署与 CORS

我们前端部署在 EdgeOne Pages,后端是独立的 Laravel(api.yfsns.cn),跨域是绕不开的。SSR 的 server 端请求走的是服务端网络,不经过浏览器,所以"前端调后端"其实分两种:服务端 loader 里直接 fetch 后端域名(不带 Origin,无需 CORS);浏览器里客户端组件再请求时,才需要后端配 CORS 白名单。两种场景别混为一谈,否则会卡在 CORS 上反复排查。

小结

从 SPA 到 SSR 最大的成本不是写代码,而是重新理清"哪些逻辑在服务端、哪些在客户端"。边界想清楚,TanStack Start 的 loader + Router 组合能让首屏和 SEO 一步到位,迁移过程比想象中顺。

T

test1

文章作者

为什么要从 SPA 切到 SSR 纯 Vite + React 的 SPA 开发体验很好,但有两个硬伤一直绕不开:首屏...

分类
技术
发布时间
2026-09-11
字数
约 1375 字
阅读
0 次