返回博客
2026-08-31 3471 字

第 20 次重构:七天,把一个网站重新做成一个系统

对比三代旧前端与全新 SvelteKit 站点,复盘 AcoFork 如何用全站 HTML 直出、shadcn-svelte Design System 和七天细节打磨完成第 20 次重构。

AI开发

本文由 ChatGPT 撰写,并根据 AcoFork 四个前端仓库的源码、构建配置、静态产物与归档页面对照整理。

第 20 次重构完成了。

“第 20 次”听起来像是又换了一次框架、又重新画了一张首页。但把 svafvuetify-afastro-aio 和现在的 af-frontend-svelte 放在一起看,会发现这次真正变化的并不是语法,而是网站终于有了一套从内容、路由、组件到发布产物都能彼此解释的系统。

这次重构前后持续了七天。七天里做的也不只是把 React、Vue 或 Astro 文件改写成 Svelte:页面被一张张打开,视口被一次次缩到手机宽度,弹窗、滚动、返回、加载、失败、空状态和页面末尾都被反复检查。最终留下来的,是全站 HTML 直出、一套统一的 shadcn Design System,以及大量不容易写进功能清单、却决定网站是否像一个完成品的细节。

三个旧仓库,三次不同的答案

这三个旧仓库并不是三份完全失败的方案。相反,它们分别解决了当时最重要的问题,也把下一次重构的矛盾暴露了出来。

svaf:第一次把 SvelteKit、静态构建和 shadcn 放到一起

早期的 svaf 已经使用 Svelte 5、SvelteKit、adapter-static、Tailwind CSS 4 和 shadcn-svelte。它不是一个等待浏览器临时拼装正文的动态博客,文章和页面同样能够在构建阶段生成 HTML。

早期 SvelteKit 站点

但当时的工程仍然更像“不断增长的页面集合”。生图用户页超过 1300 行,管理页超过 2000 行;博客列表会一次性读取文章原文,文章组件、搜索数据、RSS 和站点地图也没有完全收敛到同一内容来源。shadcn 已经存在,却更接近组件来源,还没有成为约束全站的 Design System。

第 20 次没有否定这条路线,而是重新把它做完整:文章进入独立的服务端内容层,搜索正文拆成按需加载的静态索引,路由文件变成很薄的页面入口,复杂业务再按账号、博客、生图、小说、工具和钱包拆回组件域。

vuetify-af:成熟组件库,以及一条手工维护的 SSR 流水线

vuetify-af 使用 Vue 3、Vue Router 和 Vuetify。它也不是一个简单的纯 SPA:仓库里有独立的服务端入口,使用 Vue Server Renderer 输出 HTML,再由自制脚本读取 SSR manifest、注入 CSS 和模块预加载,逐个路由写出静态页面。

Vue 与 Vuetify 版本的博客页面

这套方案证明了 Vue 应用同样可以拥有实体 HTML,但代价是项目要自己维护一条很长的构建流水线。客户端构建、SSR 构建、预渲染、博客数据、图片尺寸、图标、RSS、Sitemap、LLM 文本和短链接分别由脚本接力完成;固定路由还要在服务端入口里人工维护。

Vuetify 提供了成熟、丰富的组件,却也让站点同时混合 Vuetify、Tailwind 和项目 SCSS。普通页面可以快速拼装,核心业务仍然需要大量定制。统一的是组件库的外观,不一定是整个产品的信息层级和交互边界。

astro-aio:内容直出已经成立,但 React islands 越来越多

第 19 次重构采用 Astro 输出静态内容,把需要交互的部分交给 React islands。这一版已经真正解决了“内容能不能先成为 HTML”的问题:博客、归档和普通页面由 Astro 构建,React 只在需要时接管。

第 19 次 Astro + React islands 站点

问题出现在站点越来越像应用以后。旧仓库里能找到 30 处 React 客户端岛,分布在 21 个文件中。AI 生图、管理端和账号找回直接使用 client:only="react",全局顶栏、公告、反馈、API 诊断和错误恢复也各自成为加载岛。Astro 外壳依然很轻,但静态层和应用层之间的边界逐渐碎成许多小接口。

更明显的是维护成本:旧版最大的 DrawAdmin.tsx 超过 5400 行,全局 CSS 超过 6200 行。它有鲜明的视觉,有完整的功能,却越来越难回答一个简单的问题:一个按钮、弹窗或加载状态,到底应该遵守哪一套规则?

第 20 次:让每一页先成为 HTML,再成为应用

新站回到 SvelteKit,但不是回到旧版 svaf。这一次,SvelteKit 同时承担路由、SSR、静态预渲染和客户端接管,adapter-static 负责把整个站点写成可直接部署的文件。

项目明确启用了 prerenderssrcsr,构建入口覆盖全部正式路由。文章详情通过 entries() 枚举公开 slug,旧链接交接页同样在构建期生成;RSS、搜索索引、Sitemap、llms.txtllms-full.txt 则成为文件系统路由的一部分。

当前生产构建可以直接数出 232 个 HTML 文件。首页、博客列表、文章正文、追番、工具、公告、连接、归档、服务条款、账号、钱包、AI 生图和交互小说都有自己的 HTML。浏览器拿到的不是等待 JavaScript 填充的空容器,而是已经包含标题、正文、导航和基础状态的页面。

当然,全站 HTML 直出不等于关闭 JavaScript 后还能完成登录、生成图片或推进小说剧情。依赖账号和后端数据的能力仍需要客户端请求。真正的变化是:HTML 成为第一交付物,JavaScript 回到它应该负责的位置——搜索、筛选、登录、创作、弹窗和管理操作。

第 20 次 SvelteKit + shadcn-svelte 站点

shadcn 不再只是组件来源,而是一套 Design System

第 20 次重构最直观的变化是“统一”,但统一并不是把所有按钮涂成同一种颜色。

新站以 shadcn-svelte 的 Rhea 风格、neutral 基色和 Lucide 图标作为基础,UI 目录已经覆盖 Button、Card、Dialog、Sheet、Sidebar、Tabs、Table、Select、Field、Tooltip、Skeleton、Toggle 和 Chart 等 24 类原语、140 个文件。背景、前景、卡片、弹层、主次操作、静音文字、危险色、边框、输入框、焦点环、侧栏和圆角都由同一组 Design Token 控制。

这意味着首页、博客、钱包、账号、生图工作台、管理后台、交互小说和工具页不再各自发明按钮、卡片和弹窗。删除操作使用同一种危险语义,加载与空状态有共同的层级,移动端 Sheet、桌面 Dialog、侧栏和顶部导航也能共享边界。

第 19 次主要靠一张超过 6200 行的全局 CSS 维持视觉;第 20 次的全局样式只有约 150 行,更多规则跟随可复用组件存在。复杂度并没有凭空消失,但它从一张巨型样式表和一个五千行组件,重新分配成按业务域组织、按职责拆开的组件树。

更轻,但不拿跑分讲故事

从四个仓库现有的构建快照看,新版资源边界确实明显收缩。

与第 19 次 Astro + React 版本相比,新站完整静态产物中的未压缩 JavaScript 从约 4.49 MB 降到 2.17 MB,减少约 52%;CSS 从约 395 KB 降到 181 KB,减少约 54%。与 Vue/Vuetify 版本相比,JavaScript 总量从约 5.88 MB 降到 2.17 MB,CSS 从约 327 KB 降到 181 KB。

这些数字不是 Lighthouse,也不等于每个用户的首屏下载量。各页面会按需加载不同 chunk,网络、缓存和设备也会改变真实体验。因此更准确的结论是:新版在页面更多、功能更多的情况下,没有用更重的客户端包袱换取整洁;同时,Markdown 解析、代码高亮、目录生成和多数内容组织都被前移到了构建阶段。

博客搜索也是一个典型例子。列表首屏只携带文章摘要,只有用户真正开始搜索时才请求独立的正文索引。浏览器、搜索引擎、RSS 阅读器和 AI 抓取工具分别得到适合自己的静态输出,不再被迫下载同一份应用数据。

七天不是迁移语法,而是精雕细琢

如果只看技术栈,第 20 次重构可以被压缩成一句话:从 Astro + React islands 迁移到 SvelteKit + shadcn-svelte。但真正花掉七天的,恰恰不是这句话。

这七天里,页面被反复放进真实桌面和手机视口中检查:公告弹窗会不会上下溢出,按钮文字是否真正居中,删除操作有没有醒目的危险色与二次确认,账号找回审批是否套了多余的卡片,钱包面板是否和顶部对齐,博客返回后能不能回到原来的滚动位置,目录高亮是否跟随正文,移动端目录会不会挡住评论框。

连动画也经历了同样的过程。文章卡片需要有上浮进入的节奏,但动画不能让第二篇文章直接黑掉;页面末尾的分页、工具卡片和法律条款不能因为永远到不了触发线而保持透明;异步加载的追番卡片更不能在滚动锚点变化后变成空白。所谓“精雕细琢”,就是把这些只能在真实使用时出现的问题,一个个复现,再一个个收掉。

移动端也不是把桌面布局缩小。公告、灯箱、TOC、管理后台、钱包、友链列表、邮件模板和交互小说都重新确认了自己的滚动容器、边距和操作区。许多提交只改变几行代码,却决定了用户看到的是产品,还是一个勉强能运行的页面。

新功能不是堆入口,而是补齐闭环

第 20 次重构把不少原本分散、隐藏或半成品的能力变成了明确页面:

  • 账号中心统一承接登录、注册邮件、密码重置、OAuth、MFA 与 Passkey;
  • 钱包补齐余额、充值和转赠;
  • AI 生图拥有工作台、作品、精选、邮件、酒馆、TTS、LoRA 提交和完整管理端;
  • 交互小说形成作品广场、阅读、故事、创作、导入与管理链路;
  • 工具集收纳封面生成、图片转换、Tier List 和 B 站封面提取;
  • 公告中心、旧站归档、友链与赞助、隐私诊断和账号找回都有独立入口;
  • Email Preview 可以把统一品牌邮件导出为单文件模板;
  • Redirect Preview 可以预览短链接中间页,而不真的发生跳转;
  • 服务健康守卫在依赖后端的工具不可用时先阻断操作,并提供可复制的诊断与反馈入口;
  • RSS、Sitemap、搜索索引和面向 AI 的 llms 文件从同一内容源生成。

新站也没有假装所有旧功能都必须继续占据主导航。部分旧工具和论坛式页面退出了新的信息架构,历史链接则由兼容入口与旧站归档接住。重构不是把旧站每个按钮原封不动搬过来,而是保留业务结果,把入口重新放到合理的位置。

第 20 次不是终点,只是终于有了继续生长的基线

比较四个仓库后,很难得出“某个框架终于赢了”的结论。SvelteKit、Vue、Astro 和 React 都曾经解决过真实问题,也都在站点规模变化后暴露了新的边界。

第 20 次真正确定的是一组更长期的原则:让内容先成为 HTML,让交互只在需要时出现;让按钮、弹窗和状态来自同一种设计语言;让页面目录本身表达路由,让静态端点和正文共享内容源;让旧链接仍然有去处,也让未来再增加一个页面时,不必重新发明整个网站。

七天之后,AcoFork 不只是换了一张脸。它终于从一组功能很多的页面,变成了一套可以继续维护、继续扩展,也值得慢慢打磨的系统。

评论