原 SSR 落幕之后:我用不到半天把主站从 React CSR 重构到 Vuetify
· AI · 开发
[!NOTE] 本文使用 DeepSeek V4 flash 编写 + 人工精修。
先说结论
这大概是我个人站的第十几次重构了。这篇文章要先纠正一件事:这不是从零写一个网站,是一次重构和迁移。
主站(2x.nz)走到今天绕了一大圈——全站 SSR 落幕、Oracle 服务器被停机、紧急切成 React + React Router 的 CSR,然后问题越积越多。直到 2026 年 8 月 12 日,用不到半天把它迁到了 Vuetify。
时间账很魔幻:原本预计要写一周,写到几个小时的时候觉得只需要一两天,直到半天不到写完——代价是通宵了。
一段绕了很多圈的历史
先把技术栈的变迁摊开:WordPress → Hexo → Hugo → Astro → Zola → Svelte → React(Next.js SSR、React Router SSR)→ 现在到 Vue。 前端主流技术栈基本摸了一遍。但这并不代表 Vue 是最终答案,而是 Vuetify 给了我一个能让我专注做功能的方案。
如果觉得上面的技术栈还不够直观——我只在 GitHub 自己的仓库里搜「blog」,一页都翻不完。搜索结果有十四个之多的博客仓库,有些是毛坯房,有些完善到可以直接上线,甚至有几套能替代现有博客的精美前端。但无论如何,这些日子里确实建了这么多博客仓库。

说回主站这次的重构。最初是全站 SSR(React Router 7 Framework Mode),跑在 Oracle VPS 上。Oracle 服务器被停机后,被迫紧急切换成 React + React Router 的纯 CSR——没有服务端,只能靠客户端渲染兜着。CSR 暴露的问题很多:博客渲染走的是 CSR 而不是 SSG,SEO 和首屏全受影响;UI 也越写越乱。
SSR 当时看着好,其实埋了三个隐患
Oracle 还在的时候 SSR 本质上没什么问题,但回头看,它埋了不少雷:
- 前后端界限模糊:一旦用了 SSR,前端和后端就分不清了。
- 架构变得奇怪:为了性能,你不得不把博客放到 VPS 本地查,不走后端,内建数据库或者 Redis 缓存之类的。前端在干后端的事儿,后端几乎可以扔掉了。
- 拆分困难:想分离的时候,只能一个一个拆。
Oracle 是怎么停机的:一个小插曲
这里有个小插曲:Oracle 被停机,其实不完全是 Oracle 的锅。我之前往绑定 Oracle 的银行卡里充了 12 美元,但 Oracle 只收新加坡元。偏偏创建机子时不小心空置了一个 50GB 的硬盘,它在那挂了一个月,产生了 9.33 新加坡元的消费。卡里没有新币,扣款一直失败;到第 4 次还没扣成功,Oracle 发来了支付被拒绝的表单。我当时不以为然,但也确实往卡里补了新币——可惜已经是第四次,Oracle 不久后直接封停了账户,申诉也无果。

说到底确实是我的问题——让 Oracle 扣款失败了 4 次,它不想处理这种个人纠纷也正常。但另一方面,这次停机虽然丢了 700 多个账号(硬性损失),却让我发现了现有项目的隐患。如果没有发生这件事,这个 SSR 站点在运营几年之后,可能会暴露更大的问题。
能正常跑 SSR 的时候这些还能忍;可一旦没有服务器、只能 CSR,债就全得还。
契机:70 块买来的「充了钱不用就是亏」
真正让我下决心重构的,是 OpenCode Go 的订阅。它不是免费的——70 元/月,新用户 35 元/月。但它给的太多了:所谓「5 小时的份额」,是 63300 次 DeepSeek V4 flash 请求。这量给 10 个人用都用不完。

心态很朴素:真金白银买了,不用就是亏。 于是「把主站重写一遍」从「哪天有空再说」变成了「就现在」。
点我购买 OpenCode Go
这次到底写了什么
说是重构迁移,其实迁移的东西比写的还多:
- 存量博客全量搬过来:旧站的 markdown 文章一个没丢。构建期
import.meta.glob 读 src/content/posts/*.md,改文章即生效。
- AI 生图完整复刻:9 种生成模式、我的/精选瀑布流、全局灯箱、插队券、峰谷计费、媒体令牌——照着旧项目逐项搬。
- 账号系统:Auth Worker 登录 / 注册 / GitHub / 孤儿账号补录 / 故障公告,安全细节全按规矩来。
- 追番 / 工具集 / 友链赞助 / 关于。
- 部署管线重做:
edit 分支开发 → workflow 构建 → page 分支 → Cloudflare Worker,再接上 Pages CMS——浏览器写文章、点「部署」就上线。
真正从零写的是「壳」:布局、主题、路由、组件体系。而这部分恰恰是 Vuetify 最擅长的。
为什么这次迁移这么顺利:Vuetify 足够完善
这是我手搓 UI、用过各种组件库之后最强烈的感受:Vuetify 几乎把做网站要用的每个组件都做好了,而且 UI 比较现代,自带动画、图片懒加载、骨架屏。
你不需要像 shadcn 那样手动引入骨架屏组件、精调微调——只要引用一个动态加载的组件,默认就有骨架屏。这让我能专注实现功能,而不是去磨交互和 UI。
对比之前的经历:
- 手搓 UI 的坑:在没有完美 UI 组件库的情况下,手搓经常出现黑底黑字、白底白字,或者一个组件暗色正常、亮色不正常。原因有二:①组件库不成熟,或者干脆没用;②测试覆盖不全——因为我个人只用暗色模式,亮色模式经常出问题。
- React 的虚拟 DOM:占了性能大头;不管网站多大,所有用户进来都要下载那个巨大的 bundle;还有奇怪的局部刷新,偶尔不跟手,默认也没有动画。当然这是没用好组件库的锅(motion、GSAP 能缓解),但 Vuetify 原生就带这些过渡动画,不用额外接。
自洽的另一面:踩的坑
自洽的反面是:当你依赖某个工具类时,得先确认它真的生成了 CSS。 这次最大的一个坑:
settings.scss 里关了 Vuetify 自带 utilities($utilities: false),改用 unocss-preset-vuetify 补上。结果那个 preset 的 .border 规则只生成 border-style 和 border-width,没写 border-color——于是全站所有带 border 类的元素,边框色 fallback 到 CSS 初始值 currentcolor:暗色主题下全是实心纯白描边,像给眼盲用户做的高对比模式。
排查「这一堆白框到底哪来的」花了一阵——不是哪个组件写的,是 CSS 层漏了一个属性。修法是 main.scss 顶层补一句全局 border-color。
另一个反复出现的坑是按钮 variant:outlined 的边框是 currentColor 满不透明度,暗色下是纯白实线。Vuetify 自己的最佳实践里,中强调次要按钮用 tonal(半透明底、无边框),低强调用 text。给 AI 写清楚这条,之后所有按钮都长对了。
但 unocss-preset-vuetify 的坑不止 .border 一个。真正逼我把它换掉的,是 dev 和 build 生成的 CSS 不一样——构建全程不报错,只有把 dev 渲染和 build 产物逐元素对比才看得出来。具体两处:
- build 会把
<v-col> 这类组件标签名当成 class 候选,preset 顺手生成一版旧 Vuetify 网格的 .v-col 规则,放进优先级更高的 CSS 层,压掉了 Vuetify 4 用 CSS 变量算列宽的新网格——生产环境卡片被压成 84px 宽,封面 5:7 的排版变成两块等宽。
bg-primary、bg-surface 这些颜色工具类在 dev 静默不生成,元素回落到 Vuetify 自己的纯色版;build 却生成了带强调不透明度的版本——同一页面的 chip、工具栏文字,dev 和 preview 颜色不一致。
这类 bug 的共同特征:不报错,只能比产物 CSS 才看得见。排查办法是写个脚本,把两侧 DOM 的计算样式逐元素 diff——不查不知道,一查才发现 build 里多了一堆 dev 里没有的规则。
所以最后原子类整体迁到了 Tailwind CSS v4——它没有 dev/build 分叉。但 Vuetify × Tailwind 组合又踩了一串新坑:
- CSS
@layer 顺序:Tailwind 的 preflight 会把 button 的 padding 重置成 0,压掉 Vuetify .v-btn 的 16px 内边距,按钮全挤扁。得在 public/layers.css 显式声明 base → vuetify → utilities 的层序——先于 Tailwind 的 preflight 加载,组件样式才能盖过通用重置、工具类又能盖过组件。
- Vuetify 字号不在 Tailwind 默认档:
text-title-large 是 22px、text-headline-medium 是 28px,而 Tailwind 的 xl 是 20px、3xl 是 30px——差 2px 标题就大了两号。得在 @theme 里补精确字号和行高。
- preflight 把
h1–h6、p 的 margin 归零:Vuetify 组件不重置这些,旧站一直保留浏览器默认外边距,所以要在 base 层 margin: revert 找回来。
v-img 在 flex row 里会被撑大:Vuetify 4 的 .v-responsive 默认 flex-grow: 1,给封面图定宽必须写 flex: 0 0 <宽>,否则图片 grow 满剩余空间,追番卡片能高出一倍。
现在的状态是:dev 和 preview 逐元素一致,原子类想用就用,不再有「静默失效」——代价是这一类「必须写进文档的坑」又多了一页。
场外技术:怎么让 AI 在半天里干完这个活
重构很耗 token、也很耗脑子。除了选对框架,方法也占了很大一半:
- 单元式推进:把任务拆成单元,规划好 AI 先做什么、后做什么。先做骨架,让你对项目有信心——而不是核心功能做完了,才发现这个选型不行。
- 别微操 AI:看到它好像走偏了就赶紧 ESC 停掉去指正,是双输。既耗 token(AI 会重新审视你的提问和它做过的一切),又影响效率(AI 内部处理很快,就好比 RPC 调用就是比 HTTP 调用快)。我更喜欢用
/goal 定一个目标,让它一直跑,跑完提醒我再验收。写坏了大不了 git 回滚重新改。得益于多年的坑,这半天里几乎没有需要全量回滚的情况,基本都是小毛病让它改。
- 让 AI 自进化:让 AI 自己管理它的 AGENTS.md,遇到一个坑就记下来——这比在同一个会话里让它分散注意力强。就算 DeepSeek V4 flash 有原生的 100 万上下文,我也不建议用到 99.9% 再压缩。更建议每个单元开一个会话,跑完就散;不用担心它下次要重新审视整个项目,只要让它记住关键要点和坑、存进记忆,下次聊天会自动加载、自动避开。
- 时间分配:没有 deadline,初期我给它定的开发周期是一周。前几个小时随性开发——给 AI 定目标,然后跟朋友聊天、打游戏、看视频,跑完再验收。最后几个小时几乎没有娱乐,一直盯着 Agent 跑、快速频繁地测边缘 bug。前期做完 90%(骨架 + 核心),最后 10% 是排空边缘 bug,保证上线后不是被用户反馈 bug 回头重改,而是你作为各式各样的用户,已经把所有 bug 排空了。

找准重心
一开始用 SSR 完全是为了 SEO 的噱头——希望网站在搜索引擎上排第一,用户搜论坛一篇文章的标题就能出现那个帖子。做完 SSR 之后,SEO 的可见度确实很高,而且到现在还在持续——比如在必应搜「二叉树树官网」,我的站就排在前排:

但现实很骨感:Oracle 被停机、被迫降级 CSR 后,我也就不在意 SEO 了——本身 SEO 对我也没什么效果,就算后续掉了我也不会去管它。毕竟 SEO 不是我们的目标客户关心的事,它也就不是我们的开发重心。
本站收益最高的业务是 AI 生图。把 AI 生图打磨好,顺便服务一下传统的看博客用户、把 RSS 做好,这个项目就是成功的。搜索排第几、被不被收录、甚至被屏蔽,都不影响——99% 的用户都是老客户,他们会记住域名,而不是依赖搜索进来的人。这也是我早早就弃掉谷歌广告的原因:直接收益早就 10000% 地超过了广告。
化繁为简:回到一个仓库
最早期我做了一个「很神奇」的原子化方案:把壳单独一个仓库、博客数据一个仓库、友链和赞助一个仓库。看似管理简单,实则非常麻烦。
之前用 SSR 的时候,博客文件直接存在服务器上,没有这个问题。Oracle 被停机、紧急切到 CSR 之后,问题就出来了:用户点进博客页面,需要等一两秒的骨架屏——HTTP 请求真的很慢,要 SSL 握手,还要 TCP 的三次握手。这不仅是 SEO 问题,更是真实的等待。
更奇葩的是,前后端分离之后,一个静态博客的前端,要去请求后端构建出来的博客索引。截图里就是那套分离的样子:

没有 SSR 时,分离意味着博客不可能是 SSG,而博客在每一次发版时就定下来了,本就不该作为 CSR。我甚至研究过 monorepo、路由嫁接,实现在两个仓库写不同内容再拼到一起。
最终我决定:一个仓库,直接内嵌博客内容,完全融进新站源码,而不是外挂仓库或奇怪的 monorepo。仓库从 2KB 涨到 120MB,但也不影响用户体验,这是值得的。并且通过控制代码分支和部署分支分离,可以做到:在任意地方编写博客,点击部署就自动上线,完全不碰代码。
之前我在某篇文章里说过,这种编辑方式会导致你写了一半代码又写了一半博客,git 回滚就都没了——那其实是个错误的管理方式。写博客的时候,就不应该让写博客的工具碰到你的代码,只让它碰到博客的 markdown 原文件和图片,这就够了。
那友链和赞助为什么还是分仓库?
你可能会问:既然博客都收回来了,为什么友链和赞助还是单独一个仓库?三个原因:
- 为了那套自动审批的 GitHub 工作流。之前给友链/赞助写了很高级的自动审批,如果合并进来,这套工作流要为新仓库重新适配。
- 它们不依赖强首屏渲染。友链和赞助本身不是需要首屏就渲染出来的东西,看一眼首屏也没问题。
- 现在合并没有收益。数据量太大了,合进来也没什么收益。
后续有优化计划:让申请更简单——不再让用户手动创建 PR,而是创建一个人类可读的 issue,由后端自动解析并创建 JSON。另外,如果后期要做分流(把这个静态站点部署到不同的 CDN 上),外部依赖源越多越致命——所以迟早会合并,但不是现在。
做产品
最后落到「做产品」三个字:
- 摸清你的核心用户。
- 摸清这些核心用户需要什么。
- 仔细耐心打磨它——而不是把它当成像上班一样应付上司的表面工作。
可复用的判断
- 重构比从零快,前提是旧资产能接住:存量博客、生图后端、账号系统都是旧项目搬过来的——迁移的关键是「旧的东西能不能在新壳里活下来」。
- 别让模型猜框架:选一个 API 高度一致的框架(Vuetify 这种),AI 的猜对率直线上升。
- 坑要落地成文:让 AI 自己管理 AGENTS.md,踩过的坑下次自动避开。
- 别微操 AI:用
/goal 让它在单元里跑完再验收,比时刻盯着、随时打断强得多。
- 安全细节宁可多写:不暴露限流阈值、图片用短时效媒体令牌、GitHub 回调令牌接完即抹。
- 部署要一键:workflow + Pages CMS,写文章和上线之间只隔一个「部署」按钮。
不到半天能把这个量级的重构推完,靠的不是模型有多强,是「模型 + 自洽框架 + 文档化的坑 + 旧资产迁移 + 正确的方法」这几件套。这套组合可以复制到下一个项目。