找回密码
立即注册
搜索
发回帖 发新帖

6633

积分

0

好友

810

主题
发表于 1 小时前 | 查看: 3| 回复: 0

刚开始从事前端开发时,我一直以为“架构”是资深工程师才会在冗长会议上讨论的玄学。那时的我,觉得日常工作无非就是把按钮调成蓝色,再让接口正常跑通而已。

直到后来,我加入了一个项目,里头有个 React 文件竟然膨胀到了 3000 行。完全没人说得清状态是从哪里冒出来的。只是改了一下页头,结账页面就莫名其妙地崩了。正是那一天,我才真正领教了架构的威力。

这篇文章会覆盖我在实际项目中见过的所有主流前端架构模式。我会尽量讲得通俗:每种模式是什么、什么时候该用,以及什么时候千万别碰——这最后一点往往比前者更重要。

所以,不妨先泡杯茶,我们正式开始。

我们所说的前端架构到底是什么?

前端架构本质上就是在回答三个再朴素不过的问题:

  • 我的数据放在哪里?
  • 数据是如何从一个地方流到另一个地方的?
  • 代码该怎么切分到不同的文件和文件夹中?

下文中你将看到的每一种模式,都是对这三个问题的某种解答。有的模式关心数据,有的关心 UI,还有的关心页面究竟如何送达浏览器。带着这个视角去看前端架构,整件事就没那么令人望而生畏了。

我习惯把这些模式分成四组:

  • 数据与状态模式:如 MVC、MVP、MVVM 和 Flux
  • UI 与组件模式:如原子设计、容器/展示组件
  • 代码组织模式:如 Feature Sliced Design、整洁架构和 Monorepo
  • 渲染与交付模式:如 SSR、SSG、Islands、Server Components 和微前端

第一部分:数据与状态模式

1. MVC(Model View Controller,模型—视图—控制器)

这是前端架构模式的祖师爷。早在 Web 诞生之前,它就已经在桌面应用领域站稳了脚跟。

它的核心思路是把应用拆成三个角色:

  • Model 负责保存数据和业务规则
  • View 负责向用户展示数据
  • Controller 负责监听用户操作,并指示 Model 该改什么

MVC架构流程图 User点击Controller Controller更新Model Model通知View View展示给用户

用户点击某个元素时,Controller 捕获这次点击并更新 Model。接着,Model 通知 View 有内容发生了变化,View 随即重绘自己。

像 Backbone.js 和早期的 AngularJS 都使用过这种模式。即便到了今天,Spring MVC 等许多后端框架也依然遵循着它。

适用场景:流程简单的小型应用,或是服务端渲染应用。

短板所在:在大型前端应用里,大量 View 会与大量 Model 互相纠缠。数据箭头四处交错,代码最终沦为“意大利面式代码”。调试随之变成一场噩梦,因为你根本没法判断究竟是谁、在什么时候改了什么。

2. MVP(Model View Presenter,模型—视图—表示器)

MVP 可以看作是对 MVC 的一次微调。在这里,View 变得非常“蠢”,它完全不和 Model 直接打交道。

  • View 只负责展示内容,并把用户事件转交给 Presenter
  • Presenter 包揽全部逻辑,并直接更新 View
  • Model 只负责保存数据

MVP 最大的优点是可测试性。因为 Presenter 包含了所有逻辑却丝毫不碰 UI 代码,所以单元测试写起来非常舒服。这种模式曾经在 Android 应用和基于 GWT 的 Web 应用中流行过一段时间。

适用场景:当你希望逻辑与 UI 彻底分离,以便进行充分的单元测试时。

短板所在:你需要编写大量的额外代码。每一个 View 都要配上一个自己的 Presenter 和对应的接口。

3. MVVM(Model View ViewModel,模型—视图—视图模型)

MVVM 是让“双向数据绑定”这个概念广为人知的模式。

MVVM架构流程图 View与ViewModel双向绑定 ViewModel与Model读写数据 Model与Server进行API调用

  • Model 是来自服务器的数据
  • View 是 HTML 模板
  • ViewModel 是专门保存视图状态的特殊对象

它的魔力全在“绑定”上。ViewModel 一变,View 自动更新;用户在输入框里敲字,ViewModel 也会自动同步。你完全不需要写任何一行胶水代码来同步两端。

Vue.js、Angular、Knockout 和 Svelte 都深受这一理念影响。只要你用过 Vue 的 v-model 或 Angular 的 ngModel,你就已经用过 MVVM 了。

适用场景:表单密集型应用,比如管理后台、仪表盘和各种企业工具,这些地方有大量输入项需要与数据直接挂钩。

短板所在:双向绑定上手极快,但在超大型应用里,很难追踪到底是哪一次变更触发了哪一次更新。一个小小的改动,就可能在整个界面上引发连锁反应。

4. Flux 与 Redux(单向数据流)

Facebook 当年踩中的,正是我上面提到的那个坑。因为太多对象同时更新太多其他对象,他们的通知计数显示出了错误的数字。于是,他们创造了 Flux 与 Redux(单向数据流)。随后 Redux 登场,将 Flux 进一步简化。

规则极其严格:数据只能朝一个方向流动。

Redux架构数据流图 View分发Action Action经过Middleware处理异步操作 成功后Reducer生成新状态 Store保存 state 并通知View订阅更新

  1. View 派发一个 action。action 就是个普通对象,例如 { type: "ADD_TO_CART", payload: item }。
  2. reducer 接收 action 和当前的 state,然后返回一个全新的 state。
  3. store 负责保存这个新 state。
  4. 所有订阅了 store 的组件都会收到新数据,并重新渲染。

那 API 调用该放在哪里?Reducer 必须是纯函数,因此没法直接调 API。这项工作得交给 middleware。如果你用过 Redux Thunk 或 Redux Saga,就会明白我在说什么。Thunk 上手简单,能覆盖多数场景;Saga 基于生成器函数,在处理重试逻辑、请求取消或并行任务等复杂流程时更加得心应手。

适用场景:中大型应用,尤其是多个界面需要共享同一份数据时,比如电商购物车、用户会话或实时交易看板。

短板所在:样板代码。对一个简单应用来说,只是切换一个复选框的状态却要写上十个文件,实在有点离谱。Redux Toolkit 已经解决了大量此类痛点,所以如果你还在手写老式 Redux,真心建议试试 RTK。

如今 Zustand、Jotai、Recoil 等更轻量的状态库也纷纷涌现。它们同样遵循中心化 store 的理念,但代码量要少得多。至于服务端数据,目前大多数团队更倾向于使用 React Query 或 SWR,而不是把所有东西都一股脑塞进 Redux。

第二部分:UI 与组件模式

5. 基于组件的架构

这几乎是所有现代前端框架的基石。React、Vue、Angular、Svelte 和 Solid 都在遵循这套思路。

理念很简单:把 UI 构建成一棵由小型独立模块组成的树。每个模块都有自己的标记、样式和逻辑。你将小模块组合成更大的模块,就像搭乐高积木一样。

一个优秀的组件通常满足以下几条:

  • 专注做好一件事
  • 通过 props 或 inputs 接收数据
  • 不越界去直接操控其他组件
  • 不改动任何东西就能在另一个界面里复用

坦白讲,后面要聊的所有模式,几乎都构建在它之上。只要组件设计得好,你的架构问题就已经解决了一半。

6. 原子设计(Atomic Design)

Brad Frost 从化学中借来了这套思路。你的 UI 会按照五个层级来构建:

原子设计层级图 Atoms包含按钮输入框标签 Molecules包含搜索框 Organisms包含页头 Templates包含页面布局 Pages填充真实内容

  • 原子(Atoms) 是最小的元素,例如按钮、输入框、标签和图标。
  • 分子(Molecules) 是由原子组成的小型组合。比如,搜索框由一个输入框和一个按钮构成。
  • 有机体(Organisms) 是更大的区块。比如,页头包含 logo、导航菜单和搜索框。
  • 模板(Templates) 是带有空白插槽的页面布局。
  • 页面(Pages) 是填入真实内容后的模板。

这套模式是大多数设计系统的支柱。如果你的公司正在 Storybook 里维护共享 UI 库,那么它很可能就是按照这种方式组织的。

适用场景:构建设计系统,或者多个团队需要共享同一套 UI 组件时。

短板所在:人们常常为了“这玩意儿到底算分子还是有机体”这类问题争得不可开交。我的建议是别太较真,把这些层级当作指导原则,而不是法律条文。

7. 容器组件与展示组件

Dan Abramov 在 React 早期把这种模式发扬光大。其理念是把每项功能拆成两类组件:

容器与展示组件模式流程图 API向UserListContainer提供数据 UserListContainer通过props传递数据 UserList负责渲染多个UserCard

  • 容器组件知道“事情该怎么做”。它们负责获取数据、管理状态并处理事件。
  • 容器组件与展示组件 中的展示组件则只管“事情长什么样”。它们只接收 props 并渲染 UI。

例如,你可以有一个负责调用 API 的 UserListContainer,以及一个只负责展示卡片的 UserList。

最大的好处是,展示组件会变得极易复用,也极易测试。

补一句:React Hooks 出现之后,这套模式的边界模糊了不少。现在我们通常会把容器逻辑迁移到类似 useUsers() 这样的自定义 hook 里,再让组件去调用它。内核没变,只是外壳换了。

8. 复合组件与 Render Props

这两个模式虽然体量不大,却是优秀组件库的标配。

复合组件允许父组件与子组件共享状态,而无需手动层层传递 props。想想 HTML 里 <select> 标签的工作方式,就是那个意思。

<Tabs>
  <Tabs.List>
    <Tabs.Tab>Profile</Tabs.Tab>
    <Tabs.Tab>Settings</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel>Profile content</Tabs.Panel>
  <Tabs.Panel>Settings content</Tabs.Panel>
</Tabs>

Tabs 父组件负责记录当前激活的标签页,并通过 context 共享这份状态。Radix UI 和 Headless UI 就经常采用这种模式。

Render props 和无头组件则恰恰相反:它们给你逻辑,UI 长什么样你自己决定。TanStack Table 和 Downshift 这类库就是如此。它们负责搞定排序、筛选和键盘支持,而你只需要编写标记。

第三部分:代码组织模式

9. Feature Sliced Design

我们大多数人在项目初期,都会习惯性地采用下面这种结构:

src/
  components/
  hooks/
  services/
  utils/

这种组织方式叫“按类型划分”。对小型应用来说完全够用,但随着应用增长,components 文件夹里迟早会堆上 200 个文件。到那时,你根本分不清哪个文件属于哪项功能,更别提维护了。

Feature Sliced Design 正是为了解决这个问题而生。它同时按“功能”和“层级”来组织代码。

Feature Sliced Design分层结构图 app初始化 pages页面 widgets区块 features用户操作 entities业务数据 shared公共UI工具

  • app 包含 providers、路由和样式等全局设置
  • pages 是完整的界面
  • widgets 是页面上大型、独立的区块
  • features 是加入购物车或登录等用户操作
  • entities 是用户、产品或订单等业务对象
  • shared 包含 UI kit、辅助函数和 API 配置等可复用内容

黄金法则只有一条:层级只能向下导入,不能反向依赖。feature 可以使用 entity,但 entity 永远不能反过来使用 feature。仅凭这一条规矩,就能规避大量由循环依赖带来的混乱。

适用场景:有多名开发者参与的中大型应用。

短板所在:学习曲线。新开发者往往需要一段时间,才能搞清楚每样东西到底该放在哪里。

10. 整洁架构(分层架构)

这一理念源自后端领域和“Bob 大叔”,但它同样非常适合前端,尤其是业务逻辑繁重的应用。

整洁架构分层图 UI层包含React组件 应用层包含hooks与用例 基础设施层负责API客户端存储 领域层承载纯业务规则

  • 领域层(Domain layer) 包含纯粹的业务规则。没有 React、没有 API,只有普通的 TypeScript。比如一个计算折扣和税费后最终价格的函数。
  • 应用层(Application layer) 包含用例,也就是连接 UI 与领域层的 hook 或 service。
  • 基础设施层(Infrastructure layer) 负责与外部世界通信,包括 API client、local storage 和 analytics。
  • UI 层(UI layer) 就是 React 或 Vue 组件。

最大的价值在于:业务逻辑不绑架在任何框架上。如果公司明天决定从 React 迁移到别的技术,你的领域代码可以原封不动地搬过去。

适用场景:银行应用、保险应用、交易平台,或任何业务逻辑复杂且容不得闪失的应用。

短板所在:对简单的营销网站来说,这纯属过度设计。何必为了一个茶摊,去盖一栋五层大楼呢?

11. 事件驱动架构(发布—订阅)

有些场景里,两个模块需要通信,却压根不知道对方的存在。这时候,事件总线就能派上用场。

事件驱动架构图 CartWidget向EventBus发布加入商品事件 EventBus分别通知HeaderBadge更新角标 Analytics记录分析 ToastMessage弹出提示

一个部分发布诸如“商品已添加”这样的事件,其他任何关心该事件的部分都可以订阅并作出响应。购物车 widget 无须知道页头徽标、analytics tracker 和 toast 消息是否存在。

你可以用微型 emitter、浏览器自带的 CustomEvent API,或者引入 mitt 这样的库来实现它。在微前端场景中,不同应用之间也经常靠这种方式互相通信。

适用场景:松耦合 widget、插件系统和微前端。

短板所在:当事件满天飞时,数据流就彻底失控了。请务必把所有事件名称集中记录在一个地方。未来的你一定会感谢现在的自己。

12. Monorepo 架构

Monorepo 架构 指的是,用一个 Git 仓库来容纳多个应用和多个共享 package。

Monorepo项目结构图 Monorepo包含apps/web apps/admin apps/mobile以及packages/config packages/ui packages/utils

例如,面向客户的网站、管理面板和移动应用可以全部放在同一个 repo 里。它们共享同一套 UI 库、utils 以及 ESLint 和 TypeScript 配置。

Nx、Turborepo、pnpm workspaces 等工具让这变得相当轻松。它们足够智能,只会构建和测试真正发生变化的那部分代码。

适用场景:拥有多个需要共享大量代码的应用时。

短板所在:CI 设置和访问控制会变得棘手。规模极大的 Monorepo 还需要良好的工具链支撑,否则构建速度会逐渐失控。

第四部分:渲染与交付模式

这一部分讨论 HTML 究竟如何抵达用户的浏览器。它会直接决定页面的速度和 SEO 表现。

13. SPA 与 MPA

多页应用(Multi Page Application,MPA)是经典的 Web 模式。每次点击都会从服务器加载一个全新的 HTML 页面。老式 PHP 网站、JSP 应用和大多数 WordPress 站点都遵循这种方式。

单页应用(Single Page Application,SPA)只在开始时加载一次 HTML 页面,之后的一切都由 JavaScript 接管。点击链接只会替换局部内容,而不会触发整页刷新。用 Create React App 或普通 Vite React 搭建的应用都是典型的 SPA。

SPA 体验顺滑,可首次加载慢,对搜索引擎也不够友好。也正因如此,才有了接下来这些模式的出现。

14. CSR、SSR、SSG 与 ISR

Web页面渲染方式对比图 用户请求页面后 CSR在浏览器构建HTML SSR在每次请求时服务端生成HTML SSG在构建时一次性生成HTML ISR在固定时间后后台重新生成HTML Islands只对可交互部分加水合

  • 客户端渲染(Client Side Rendering,CSR):服务器发送一个几乎为空的 HTML 文件和一个很大的 JavaScript bundle。浏览器运行 JS 并构建页面。适合无需考虑 SEO 的登录后仪表盘。
  • 服务端渲染(Server Side Rendering,SSR):服务器针对每次请求生成完整 HTML 并发送给浏览器。用户可以很快看到内容,随后 JavaScript 加载并让页面具备交互能力,这个步骤称为 hydration。适合产品详情页或新闻动态等频繁变化且依赖 SEO 的页面。
  • 静态站点生成(Static Site Generation,SSG):所有 HTML 页面都在构建时一次性生成,并通过 CDN 提供服务。速度极快,成本也极低。适合博客、文档和落地页。
  • 增量静态再生成(Incremental Static Regeneration,ISR):这是 SSG 与 SSR 的结合。页面是静态的,但会在固定时间后或由你手动触发时在后台重新生成。适合拥有成千上万商品的电商网站——它们的价格偶尔会变,但绝不会每秒都变。

Next.js、Nuxt、Remix 和 SvelteKit 允许你在同一个应用里同时使用这些方式,并为每个页面单独选择。正是这种灵活性,让这些元框架变得如此流行。

15. Islands 架构

先抛个问题:如果博客页面上只有评论框需要 JavaScript,凭什么让整页都背上下载 JS 的负担?

Islands 架构正是为了回答这个问题。

Islands架构页面加载状态图 静态HTML页面中 Header和Footer为静态内容 搜索岛和评论岛被独立水合并具备交互能力

页面的大部分内容都是静态 HTML,只有少数几个部分可以交互。这些部分被称为“岛屿”(islands)。每个岛屿独立加载自己的 JavaScript,其他所有内容都是零 JS 的普通 HTML。

Astro 让这种模式广为人知。Fresh 和带插件的 Eleventy 也支持它。

适用场景:博客、营销页面、文档和新闻网站等内容密集型站点。

短板所在:不适合几乎所有内容都需要交互的应用,例如 Figma 克隆版或聊天应用。

16. React Server Components

这是目前最前沿的重要模式,彻底改变了我们看待 React 的视角。

React Server Components系统架构图 Server区域中页面级服务端组件读取数据库并将props传递给Browser区域中的客户端组件

现在,组件被严格划分为两类:

  • Server Components 只在服务器上运行,可以直接读取数据库。它们的代码永远不会被发送到浏览器,因此 bundle 体积会更小。
  • Client Components 在浏览器中运行。你需要在文件顶部使用 "use client" 来标记它们。只有这类组件能够使用 useState、useEffect 和事件处理器。

Server Components 可以渲染 Client Components,并向它们传递 props。这样一来,你就能把繁重的数据处理留在服务器上,只把少量可交互的部分发送给浏览器。

Next.js App Router 完全围绕这一理念构建。

适用场景:重视性能和 SEO 的新 Next.js 项目。

短板所在:这套心智模型太新了。许多开发者会困惑于“什么代码在哪里运行”。一些较老的库目前也无法在 Server Components 中良好工作。

17. 微前端(Micro Frontends)

不妨脑补一下 Flipkart 或 Amazon 这类巨头的场景。数百名开发者同时在同一个网站上工作。如果所有人把代码塞进同一个巨型前端仓库,那每次发版都无异于一场战役。

微前端用微服务改造后端的方式来改造前端,从而解决这个问题。

微前端架构拓扑图 Shell应用负责路由和鉴权 分别连接React团队负责的产品微前端 Vue团队负责的购物车微前端 Angular团队负责的支付微前端以及React团队负责的个人资料微前端

网站被拆分成由不同团队负责的小型应用。一个团队负责产品页,另一个负责购物车,还有一个负责支付。每个团队都可以独立部署,无须等待其他团队。理论上,每个团队甚至可以使用不同的框架。

一个壳应用会负责加载这些微前端,并统一处理路由、登录、布局等公共问题。

常见的构建方式包括:

  • Webpack 或 Rspack 中的 Module Federation,这是目前最常见的选择。
  • Single SPA,用于将多个应用挂载到一起的框架。
  • Web Components,作为与框架无关的封装。
  • iframe,虽然很传统,但仍被用于实现强隔离。

适用场景:拥有许多独立团队的大型组织。

短板所在:复杂度直线飙升。共享依赖、版本不匹配、样式一致性和性能都会变得更难处理。如果团队只有五名开发者,请千万不要这么干。结构良好的单体应用永远更香。

18. Backend for Frontend(BFF)

这一模式处于前端和后端之间,但通常由前端团队负责维护。

BFF架构模式系统流程图 Web App连接Web BFF Mobile App连接Mobile BFF 两个BFF层分别聚合下游的用户服务订单服务和产品服务

不同客户端需要的数据各不相同。移动应用想要小巧的响应体,Web 应用则想要更丰富的返回。与其让每个客户端调用五个微服务并自行拼接数据,不如为每种前端专门创建一个小型服务器。该服务器从各个服务收集数据,并准确返回前端所需的内容。

你可以使用 Node.js、Next.js API routes 或 GraphQL 来构建 BFF。

适用场景:拥有许多后端服务和多种客户端时。

短板所在:又多了一层需要构建、部署和监控的服务。

那么,该选择哪种模式?

这是全文最关键的部分,请放慢节奏。

你永远不会只选择一种模式。真实的应用总是多种模式的组合。下面以一个典型电商项目中的真实组合为例:

  • 产品页面采用 Next.js、SSR 和 ISR
  • 数据处理繁重的商品列表页采用 React Server Components
  • 文件夹结构采用 Feature Sliced Design
  • 共享 UI 库采用原子设计
  • 购物车和用户会话采用 Redux Toolkit 或 Zustand
  • 服务端数据采用 React Query
  • 通过 BFF 层整合产品、价格和库存服务
  • 使用基于 Turborepo 的 Monorepo,与管理面板共享代码

感受到了吗?每种模式都在精准解决一个特定痛点。

下面是我常用的快速参考:

  • 小型个人项目:基于组件的架构配合简单的 hook,仅此而已。
  • 博客或营销网站:使用 SSG,或者采用 Astro 的 Islands 架构。
  • 登录后的管理仪表盘:SPA 配合 CSR、React Query,也许再加上 Zustand。
  • 电商或内容平台:Next.js 配合 SSR、ISR 和 Server Components。
  • 银行系统等复杂业务应用:引入整洁架构和 Feature Sliced Design。
  • 拥有众多团队的大型公司:先采用 Monorepo。只有当团队确实需要独立部署时,才考虑微前端。

一些我历经教训才明白的事

从简单开始。 不要第一天就引入 Redux、微前端和整洁架构。只有当你真正感受到某个问题带来的痛苦时,才添加能够解决它的模式。

一致性胜过完美。 一个始终如一地遵循普通模式的团队,永远比每个人都各自为政、遵循自己“完美模式”的团队走得更远。

把它记录下来。 准备一份简短的 README 或 ADR(架构决策记录),解释为什么选择某种模式。六个月之后,没人会记得当初的原因。

模式是工具,不是宗教。 适合你的问题就使用,不适合就舍弃。

结语

前端架构的意义,绝不是为了在面试时搬弄几个玄乎的术语,而是为了确保六个月之后,你和团队依然能够毫无畏惧地修改代码。

如果你理解了本文的核心理念,那么你已经走在了许多开发者前面。下次开始一个项目时,只需问自己那三个问题:我的数据存放在哪里?它如何流动?我的代码如何划分?正确的模式自然会浮出水面。




上一篇:法国拟征数据流量税:平台每传 1GB 缴 5.5 欧分
下一篇:AI 写的单元测试为什么总是一把过:它只会证明“对”,找不到“错”
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-11-11 17:16 , Processed in 0.080320 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表