原文链接:https://medium.com/interview-series/the-frontend-architecture-guide-you-need-before-your-frontend-interviews-22f815a3f981
翻译:谢杰
审校:谢杰
如何构建大型应用的前端架构?
你带着对组件的十足信心走进了面试。你熟悉 React hooks,理解组件的生命周期,也能清楚地说明什么时候应该拆分组件,什么时候该将它们保留在一起。但就在这时,面试官靠在椅背上,问了你一句:
“你会如何构建一个大型应用的前端架构?”
突然之间,你意识到组件只是冰山一角。
前端架构涵盖了一系列高层次的决策,这些决策决定了你的应用如何运行、如何扩展,以及如何随着时间演进。可以把组件看作是一座房子里的各个房间,而架构则是那张蓝图,决定了这些房间如何连接、水管如何铺设,以及整个结构如何经受住几十年的考验。
状态管理:组织你的应用内存
状态管理往往是从“你会写代码”过渡到“你能构建架构”的关键环节。问题的核心不只是选择 Redux 还是 Context,更在于理解你的应用需要管理哪些类型的 state,以及每种 state 应该存放在哪里。
设想你正在构建一个 dashboard 应用,你需要处理用户偏好设置、实时通知、缓存的 API 数据和表单 state。与其把所有的 state 一视同仁,从架构角度出发,应该意识到每种 state 都有不同的特性,适合存放在不同的位置。
下面是对状态层级的理解方式,分为 4 个状态层级:
第 1 层:服务器状态(API Data)
- 特点:可缓存,可能过期,需要刷新
- 存储方式:React Query、SWR、Apollo
- 示例:用户资料、产品目录
- 模式:拉取一次,全局缓存,自动刷新
第 2 层:全局 UI 状态
- 特点:多个功能共享,同步更新
- 存储方式:Context API、Zustand、Redux
- 示例:主题、语言、弹窗打开状态
- 模式:单一数据源,全局可访问
第 3 层:本地组件状态
- 特点:临时性,仅作用于组件内部
- 存储方式:
useState、useReducer
- 示例:表单输入、开关状态、悬停效果
- 模式:随组件创建和销毁而存在
第 4 层:URL 状态
- 特点:可分享、可收藏、持久化
- 存储方式:URL 参数、路由路径
- 示例:搜索筛选条件、分页信息、选中的标签页
- 模式:使视图可被链接的状态
这里的架构原则是 按特性分层。服务器 state 需要缓存和同步,全局 state 需要全局访问,本地 state 需要封装性,而 URL state 则需要可共享性。只有将每种 state 放在正确的层级上,它们才能自然而然地具备各自应有的行为特征。
数据流:构建可预测的路径
架构中一个非常关键的问题是:“你的应用中数据是如何流动的?”很多开发者在这里会感到迷茫,因为他们从未真正去可视化整个数据流。架构上的挑战在于:让数据流动变得 可预测,从而让你能够清晰地理解和掌控整个应用。
反面示例:双向混乱

在这种模式下,数据在各个方向流动。组件可以直接修改共享对象,其他组件则读取这些变化,而你根本无法确定这些操作的执行顺序。Component B 可能读取的是过时的数据,Component C 可能会覆盖 Component A 的修改,一切都变得混乱不堪。
架构模式:单向数据流

这种架构模式构建了一个清晰的数据回路:数据从 store 向下通过 props 传递给组件,用户事件向上传递为 action,action 流向 reducer,reducer 更新 store,然后这个循环再次开始。你可以追踪任意一份数据在这个循环中的流动路径,准确地理解它是如何变化的。
关键点在于:数据永远不会在组件之间横向流动。如果 Component B 需要 Component A 的数据,它们都会从同一个 store 中获取。这使得整个应用变得可预测,因为数据总是沿着一条清晰的路径流动。
模块边界:划分归属权
这往往是面试中开始变得有趣的地方。面试官可能会给你看一个不断扩展的代码库,并问你:“你会如何组织这些代码?”他们问的不是文件夹的命名方式,而是架构层面的边界划分。
想象一座城市。建筑物不是随意摆放的,而是划分为住宅区、商业区和工业区。每个区域都有各自的规则,规定了可以建设什么、以及各个区域之间如何交互。你的代码库同样需要这种“分区管理”。
扁平无序的结构
src/components/
├── Button.jsx
├── Input.jsx
├── UserProfile.jsx
├── ProductCard.jsx
├── ShoppingCart.jsx
├── AdminDashboard.jsx
├── PaymentForm.jsx
├── LoginModal.jsx
└── (还有 200 个文件……)
所有内容都堆在一个大文件夹中。购物车模块引入了后台管理组件,登录弹窗依赖了支付表单......
整个项目变成了架构上的“意大利面”,模块之间相互依赖,混乱不堪。
架构模式:限界上下文
src/
├── core/ (领域逻辑层)
│ ├── entities/ ← 纯业务对象
│ └── services/ ← 业务逻辑
│ ↑
│ │(core 永远不依赖上层模块)
│ │
├── features/ (功能层)
│ ├── auth/ ← 登录认证功能
│ │ ├── components/
│ │ ├── hooks/
│ │ └── state/
│ │
│ ├── products/ ← 商品目录功能
│ │ ├── components/
│ │ └── hooks/
│ │
│ └── cart/ ← 购物车功能
│ ├── components/
│ └── hooks/
│ ↑
│ │(功能模块可依赖 core 和 shared)
│ │(功能模块之间不得相互依赖)
│ │
└── shared/ (共享 UI 层)
├── components/ ← 通用 UI 组件
├── hooks/ ← 可复用 hooks
└── utils/ ← 工具函数
架构规则明确了依赖的方向,依赖流向如下:
- Features → Core(允许)
- Features → Shared(允许)
- Features → 其他 Features(禁止)
- Shared → Core(允许)
- Core → 无(完全自包含)
这些边界带来了极大的灵活性:你可以将购物车功能提取为独立的包,单独测试某个功能模块,或者将其迁移到另一个应用中。清晰的边界造就了灵活的架构设计。
如何保持功能模块的独立性
当 Feature A 需要使用 Feature B 的某些内容时:
错误做法:
Feature A → 直接 import → Feature B
(导致模块强耦合)
正确做法:
Feature A ← 通过 props 接收 ← 父组件
↓
父组件同时渲染两个模块
↓
Feature B → 通过接口暴露 → 父组件
(功能模块保持独立,由父组件负责协调)
这种方式确保了功能模块之间不会直接依赖,而是通过上层组件进行解耦和协调,从而保持架构的清晰与可维护性。
性能架构:分层设计
当面试官提问有关性能的问题时,他们想了解的是你是否具备从架构层面思考 render 表现的能力,而不仅仅是一些优化技巧。真正的问题是:你是否能设计出默认情况下就具备良好性能的系统。
性能金字塔
┌─────────────┐
│ Static │ ← 渲染一次,永不更新
│ Content │ (配置、布局等)
└─────────────┘
┌─────────────────┐
│ Slow-Changing │ ← 每几分钟更新一次
│ Data │ (聚合统计、总数等)
└─────────────────┘
┌───────────────────────┐
│ Medium-Speed Updates │ ← 每 10–30 秒更新一次
│ (用户操作) │ (用户资料、设置等)
└───────────────────────┘
┌───────────────────────────────┐
│ High-Frequency Updates │ ← 每 1–3 秒更新一次
│ (实时数据) │ (实时指标、通知等)
└───────────────────────────────┘
架构上的原则是:按更新频率分离关注点。高频数据更新频繁,但只影响 UI 的局部;低频数据更新较少,但可能触发较大的 render;静态内容则永远不更新。
对高开销组件的隔离模式
┌─────────────── Dashboard ───────────────┐
│ │
│ ┌─── Live Metrics ───┐ │
│ │ 更新频率:1 秒 │ ← 小组件,快速更新 │
│ │ 渲染开销:低 │ │
│ └────────────────────┘ │
│ │
│ ┌── Expensive Chart ──┐ │
│ │ 更新频率:60 秒 │ ← 大组件,更新慢 │
│ │ 渲染开销:高 │ │
│ │ 已使用 memo 缓存 │ │
│ └─────────────────────┘ │
│ │
│ ┌─── Static Config ───┐ │
│ │ 更新频率:从不更新 │ ← 永远不变 │
│ │ 渲染开销:为零 │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────┘
当实时指标组件每秒更新时,只有这一个小组件会重新渲染;高开销的图表组件因已 memo 化而不会重新计算;静态配置部分则完全不会重新渲染。这样的架构设计确保即使在实时数据频繁更新的场景下,整个 dashboard 依然流畅运行。
Error Boundaries:为异常而设计
一个更高级的架构面试问题是:“当系统出错时会发生什么?”这个问题考察的是你是否考虑了 失败场景,而不仅仅是理想路径(happy path)。
没有架构保护:级联故障
┌─────────────── Application ───────────────┐
│ │
│ ┌──────── Feature A ───────┐ │
│ │ 正常工作 │ │
│ └──────────────────────────┘ │
│ │
│ ┌──────── Feature B ───────┐ │
│ │ 出错了! │ │
│ └──────────────────────────┘ │
│ ↓ │
│ 【整个应用崩溃】 │
│ ↓ │
│ 所有内容都无法显示,即使是正常模块 │
│ │
└───────────────────────────────────────────┘
一个错误会导致整个应用崩溃。用户失去对所有功能的访问,哪怕其中一些模块原本运行正常。
架构模式

每个功能模块都被包裹在独立的 error boundary 中,就像船只上的防水舱隔。当 Feature B 出现故障时,它只会显示一个备用 UI,而 Feature A 和 Feature C 依然正常运行。导航栏和页脚始终保持渲染。整个应用实现了 优雅降级,而不是完全崩溃。
可扩展性:为多团队协作而设计
架构面试中的终极问题通常是某种形式的:“你会如何扩展这个系统?”这里说的并不是服务器扩容,而是考察你是否能够设计一个前端架构,让今天一个团队能高效开发,而明年五个团队仍能协同合作。
Monorepo架构的问题
单一代码仓库
↓
┌────────────────────────────────────┐
│ Team A、B、C 全部在此开发 │
│ │
│ 面临的问题: │
│ • 合并冲突频繁 │
│ • 部署相互阻塞 │
│ • 共用构建流水线 │
│ • 团队之间无法独立作业 │
└────────────────────────────────────┘
当多个团队共用同一个代码库时,彼此之间频繁“踩脚”:Team A 的 bug 修复无法上线,因为 Team B 的新功能导致构建失败;Team C 无法开始开发,因为还在等 Team A 的代码评审通过。
架构模式:联邦式应用
┌────────────────────────────────────────────────────────┐
│ Shell Application │
│ (由平台团队维护) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Module A │ │ Module B │ │ Module C │ │
│ │ (Team A) │ │ (Team B) │ │ (Team C) │ │
│ │ │ │ │ │ │ │
│ │ • 独立代码仓库│ │ • 独立代码仓库 │ │ • 独立代码仓库 │ │
│ │ • 独立部署 │ │ • 独立部署 │ │ • 独立部署 │ │
│ │ • 独立测试体系│ │ • 独立测试体系 │ │ • 独立测试体系 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 每个团队独立开发,通过 Shell 应用集成 │
└────────────────────────────────────────────────────────┘
Shared Packages(可选):
Design System(由设计团队维护)
API Client(由平台团队维护)
Common Utils(由平台团队维护)
每个团队负责一个功能模块,边界清晰。可以独立部署、使用不同的发布节奏,甚至在需要时使用 不同版本的依赖库。Shell 应用负责协调和集成,但不包含具体业务逻辑。这种微前端架构模式是处理大型、多团队协作项目的有效解决方案。
更高的视角:架构为何重要
当面试官问你关于前端架构的问题时,他们是在考察你是否能跳脱出当前正在编写的代码,去思考更长远的系统设计。组件只是表层——是用户能看到的部分。而架构则是隐藏在底层的支撑结构,它决定了你的应用能否增长、扩展、演进,并在未来依然稳定运行。
这就像城市规划。任何人都可以盖一栋房子,但城市规划师要考虑的是成千上万栋房屋如何连接到道路、基础设施和服务网络上。他们为城市的增长做规划,预判基础设施故障时的影响,并设计能运行数十年的系统。
这正是前端架构的本质:在做出每一个决策时都深思其长期影响。你今天选择的状态管理模式,将决定明年添加新功能的难易程度;你画下的模块边界,将影响团队之间是能独立开发还是相互阻塞;你实现的错误处理策略,将决定一个 bug 是否会导致整个应用崩溃,或只是被用户轻描淡写地忽略。
这些面试真正想考察的,并不是你是否掌握某个具体模式或技术,而是你是否具备 系统性地思考、构建可持续软件的能力。这正是“写代码”和“设计系统”之间的本质区别。如果你想查看更多关于如何准备前端面试的资料和讨论,可以访问云栈社区的面试求职板块。