2、技术架构设计:前端架构模式选择(SPA/SSR/SSG)、目录结构设计、组件树设计、状态管理方案

好,咱们进入第二章。说实话,技术架构设计这步,是很多团队容易「想当然」的地方。我见过不少项目,上来就选 Vue + SPA,结果做到一半发现 SEO 搞不定,或者首屏慢得离谱。嗯,今天咱们就把这事掰扯清楚。

2.1 架构模式选择:SPA、SSR 还是 SSG?

先问个问题:你的用户是谁?你的内容多久变一次?这两个问题想明白了,选型就简单了。

SPA(单页应用),说白了就是前后端分离,所有页面都在浏览器端渲染。我早期做的一个后台管理系统,用的就是 SPA。用户都是内部员工,不需要 SEO,交互又复杂,SPA 再合适不过了。

SSR(服务端渲染),每次请求都在服务端拼好 HTML 再返回。适合对首屏速度要求高、需要 SEO 的场景。比如电商、资讯站。我在项目中遇到过,一个 B2B 网站,客户要求谷歌收录必须前三页,不用 SSR 根本做不到。

SSG(静态站点生成),构建时就把页面生成好,部署到 CDN 上。适合内容不常变的场景,比如文档站、博客、官网。我记得有个客户,他们的官网一年改两次,用 SSG 简直不要太爽,成本低、速度快。

特性 SPA SSR SSG
首屏速度 慢(需下载 JS) 快(服务端直出) 极快(纯静态)
SEO 友好
动态交互
维护成本 高(需 Node 服务)
适用场景 后台、工具类 电商、内容平台 博客、官网

核心建议:别盲目追新。如果你的项目是内部系统,老老实实 SPA。如果是面向 C 端的内容站,优先考虑 SSG,实在不行再上 SSR。你想想看,SSR 要维护 Node 层,出问题排查起来可麻烦了。

避坑指南:我曾经有个项目,选了 Nuxt 的 SSR 模式,结果第三方 SDK 在服务端跑不了,各种 hack。后来发现其实用 SSG + 客户端动态加载就够用了。所以,先想清楚你的「动态」到底有多动态。

2.2 目录结构设计:别让代码变成一锅粥

目录结构这事,看着简单,但真能看出一个团队的工程化水平。我个人习惯按「功能」而不是按「类型」来划分。

什么意思?就是别把所有的组件都扔到 components/ 下,所有的页面都扔到 pages/ 下。那样项目一大了,找文件得翻半天。

我推荐这种结构:

src/
├── modules/              # 业务模块
│   ├── user/             # 用户模块
│   │   ├── components/   # 该模块专用组件
│   │   ├── hooks/        # 该模块逻辑
│   │   ├── pages/        # 该模块页面
│   │   └── store/        # 该模块状态
│   ├── order/            # 订单模块
│   └── product/          # 商品模块
├── shared/               # 共享资源
│   ├── components/       # 通用组件
│   ├── utils/            # 工具函数
│   └── constants/        # 常量
├── layouts/              # 布局组件
├── router/               # 路由配置
└── styles/               # 全局样式

这样设计的好处是:每个模块都是独立的,新人接手只看一个模块就行。而且模块之间耦合度低,以后拆成微前端也方便。

小技巧:我习惯在 modules/ 下每个模块里放一个 index.ts,统一导出该模块的公共内容。这样外部引用时只需要 import { UserCard } from '@/modules/user',清爽得很。

2.3 组件树设计:从原子到页面

组件树设计,说白了就是怎么把页面拆成一个个可复用的积木。我一般用「原子设计」的思路,但会结合实际项目做调整。

  • 原子组件:按钮、输入框、图标。这些是最小单元,不包含业务逻辑。
  • 分子组件:搜索栏、表格、弹窗。由原子组件组合而成,可能有简单的交互。
  • 组织组件:用户列表、订单卡片。包含业务逻辑,但可复用。
  • 页面组件:具体的页面,由组织和分子组件拼装而成。

举个例子,一个商品列表页:

<ProductListPage>              <!-- 页面组件 -->
  ├── <SearchBar>               <!-- 分子组件 -->
  │   ├── <Input />             <!-- 原子组件 -->
  │   └── <Button />            <!-- 原子组件 -->
  ├── <ProductTable>            <!-- 组织组件 -->
  │   ├── <Table />             <!-- 分子组件 -->
  │   └── <Pagination />        <!-- 分子组件 -->
  └── <ProductDetailModal>      <!-- 组织组件 -->
      └── <Modal />             <!-- 分子组件 -->

关键原则:每个组件只做一件事。如果一个组件超过 200 行,或者有超过 3 个 if-else 分支,就该拆了。我在项目中遇到过,一个「多功能」组件维护了半年,最后没人敢动,重构花了三周。嗯,血的教训。

2.4 状态管理方案:别滥用,也别不用

状态管理这块,争议最大。我的观点是:能用 props 传递就别用全局状态,能用局部状态就别用 Redux。

为什么?因为全局状态一多,数据流就乱了。你想想看,一个变量改了,十几个组件重新渲染,性能能不差吗?

我一般这样选:

  • 组件内部状态:useStateref。比如表单输入、弹窗开关。
  • 组件间共享状态:useContextprovide/inject。比如主题、用户信息。
  • 全局复杂状态:用 Pinia(Vue)或 Zustand(React)。比如购物车、权限列表。
  • 服务端状态:用 React Query 或 SWR。比如从 API 获取的数据。

这里有个常见的误区:很多人把 API 返回的数据也放到全局 store 里。其实没必要,用 React Query 这类工具,自带缓存、重试、过期机制,比你自己手写强多了。

我的习惯:在项目中,我会把 store 按模块拆分。比如 useUserStoreuseCartStore。每个 store 只管理自己的状态,互不干扰。这样测试也好写,出问题也好定位。

避坑指南:我曾经接手过一个项目,一个 store 里放了十几个模块的状态,光 actions 就有 50 多个。每次改一个功能,都要担心会不会影响到其他地方。后来花了两个月重构,拆成了 8 个独立的 store。所以,从一开始就做好拆分,别偷懒。

好了,技术架构设计这块,核心就是这些。记住:没有银弹,只有最适合你项目的方案。下一章咱们聊聊构建工具和打包策略,那个坑更多,到时候细说。