{"ok":true,"data":{"slug":"guoyu-2-rebuild","title":"果语 2.0 从 Vue 2 到 Vite + React 19 的重构手记","summary":"旧站在 Spring Boot 上跑了半年，如今决定把它推倒重来。这篇文章记录了技术选型的取舍、迁移映射，以及一个个人博客到底需不需要后端。","date":"2026-10-13","category":"技术笔记","tags":["重构","Vite","React","架构"],"cover":"/images/covers/cover-rebuild.svg","featured":true,"draft":false,"body":"旧站叫葱舍，Vue 2 + Spring Boot + MySQL 的全栈组合（也就是果语 1.0）。它跑得很稳，但我在写新文章的时候总要面对一个问题：**为了写一篇随笔，我得先启动一个数据库。**\n\n这次重构的目标其实只有一句话——把「发布一篇文章」的成本，从「启动三个服务」压缩到「新建一个文件」。\n\n## 先说结论\n\n新站砍掉了后端。内容以 Markdown 文件的形式躺在 `content/posts/` 里，构建时被打包进静态资源。\n\n```text\n果语 2.0 技术栈\n────────────────────────────────────────\n构建      Vite 7          冷启动 300ms 以内\n框架      React 19 + TypeScript\n样式      Tailwind CSS 3.4 + 设计令牌\n组件      shadcn/ui（源码进仓，不锁定）\n路由      react-router-dom\n内容      Markdown + 自研 frontmatter 解析\n部署      静态托管（dist/ 即全部）\n```\n\n## 为什么不是 Next.js\n\n如果你只想做 SEO 优先的内容站，Next.js 的 SSG 是标准答案。我没有选它，原因很具体：\n\n1. **个人博客是单作者场景**，RSC 的服务端能力大部分用不上；\n2. **构建体积可控**，`dist/` 是一堆静态文件，`base: '/'` 丢到任何托管都能跑；\n3. **心智负担低**，从 Vue 2 迁过来时，我需要的是「一个我完全掌控的构建产物」，而不是一层框架抽象。\n\nSEO 的短板我用别的方式补：每路由独立 metadata、构建期生成 `sitemap.xml` 与 `feed.xml`、文章页注入 `BlogPosting` 结构化数据。这三件事加起来，成本大约一百行代码。\n\n> 架构没有标准答案，只有适不适合。个人站最重要的指标是「三个月后我还愿意维护它吗」。\n\n## 迁移映射表\n\n| 旧站 1.0（Vue 2） | 果语 2.0（Vite + React） |\n|---|---|\n| `views/Home.vue` | `pages/Home.tsx` + `PostCard` |\n| `views/Docs.vue` + `ArticleSearch` | `pages/Posts.tsx` + `⌘K` 命令面板 |\n| `views/ArticleDetail.vue` | `pages/PostDetail.tsx` + TOC + Markdown |\n| `views/Album.vue` / `AlbumDetail.vue` | `pages/Album.tsx` + `MasonryGrid` + `Lightbox` |\n| `views/Settings.vue` | `ThemeToggle`（light / dark / system） |\n| MySQL 文章表 | `content/posts/*.md` |\n| `components/HeatMap.vue` | P2：贡献热力图 |\n| `components/MusicPlayer.vue` | P2：音乐播放器 |\n\n### 一个典型的构建配置\n\n```ts\n// vite.config.ts\nimport path from \"path\";\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  base: \"/\",\n  plugins: [react()],\n  resolve: {\n    alias: { \"@\": path.resolve(__dirname, \"./src\") },\n  },\n});\n```\n\n内容层用 `import.meta.glob` 在构建期静态收集所有文章，不需要任何后端接口：\n\n```ts\nconst modules = import.meta.glob(\"../../content/posts/*.md\", {\n  query: \"?raw\",\n  import: \"default\",\n  eager: true,\n}) as Record<string, string>;\n```\n\n## 保留了什么\n\n重构不等于推倒一切。旧站有几个设计我原封不动地搬了过来：\n\n- **个人信息卡**（头像 / 名言 / 社交 / 点击复制）——它是我这个站的辨识度所在；\n- **歌词栏与音乐播放器**——列入 P2，等主题系统稳定后再做；\n- **相册的分类概念**（风景 / 美食 / 随心 / 回忆），以及真实的照片资产；\n- **Footer 的运行时长**，一个很朴素但让人愿意常来的小细节。\n\n而被砍掉的是：JWT 鉴权、自建后台、私信、天气与 LeetCode 小组件。它们依赖后端和第三方接口，对一个博客来说收益极低。\n（后来它们又以更轻的形式回来了：一个零依赖的 Node 后台、同源 `/api/*` 代理——但那是后话，主体的通路始终只有 `.md` 文件。）\n\n## 接下来\n\n- [x] 设计令牌与暗色模式\n- [x] Markdown 内容层与阅读体验\n- [ ] 接入 Giscus 评论\n- [ ] 部署到静态托管并配置自定义域名\n"}}