首页  |  学习总览  |  ← 上一章:测试与代码质量 阶段九 · 第 9 章,共 10 章
阶段九 · 预计 80 小时

架构设计与模式

这是从「工程师」走向「架构师」的分水岭。架构不是画一张漂亮的分层图,而是在「变化、成本、复杂度」之间持续做权衡。本章讲透 SOLID、分层、设计模式、DDD 思想、Monorepo 与微前端的取舍,以及如何用 ADR 把你的每个决策变成团队资产。

1架构思维:先学会「权衡」再学「招式」

架构的本质是取舍,不是堆技术。

1.1 什么是好架构?

好的架构不是一个「终极正确」的形态,而是让系统在当前阶段可接受的成本应对预期内的变化。判断标准只有一条:需求的变更速度 vs 改动的成本——架构的价值就是让「改动成本的增长曲线」比「系统复杂度」平缓。

权衡维度过度设计的代价设计不足的代价
抽象两层包装查三层文件才能改一行到处重复,改一处漏十处
分层为小功能建五层目录全部逻辑堆在组件里
性能为不可能出现的 10 万 QPS 做集群用户基数起来后重构核心路径
测试为展示组件写 100% 覆盖核心业务逻辑零测试
工具链微前端 + Monorepo + 自研脚手架单仓单应用一个 script 打天下
💡 架构师的决策顺序 先问「问题是什么」→ 再问「现在必须解决吗」→ 然后问「最简单的解法是什么」→ 最后才是「选哪个技术」。80% 的架构问题不是技术问题,是「没想清楚就开始选型」。

1.2 架构演进的真实路径(前端视角)

  1. 阶段一:单文件——HTML 里写 JS,一切简单直接,改动成本低,但规模一上来就失控。
  2. 阶段二:组件化——React/Vue 组件拆分,UI 复用有了抓手,但业务逻辑开始混进组件。
  3. 阶段三:分层——展示层 / 业务层 / 数据层分离,状态管理工具上场,组件变薄。
  4. 阶段四:模块化 + Monorepo——多个应用共享基础设施(设计系统、请求层、工具库),用 workspace 管理依赖。
  5. 阶段五:微前端(可选)——多团队独立交付、独立技术栈,用 Module Federation 等手段组合。
⚠️ 不要跳级 小团队直接上微前端 = 用一个复杂系统解决一个还不存在的问题。架构师的核心能力之一就是「抵抗过度设计」——明确告诉团队「现在不做什么」和「现在做什么」一样重要。
2SOLID 原则:面向对象五大原则(前端同样适用)

不是「背名词」,而是理解每个原则在解决什么痛点。

2.1 S —— 单一职责原则(SRP)

一个模块只应该有一个「改变的理由」。判断方法:给这个模块列「它会被哪些原因修改」,超过一个就该拆。

// 反例:一个组件做了四件事——加载数据、渲染列表、过滤、导出 CSV
function UserListPage() {
  const [users, setUsers] = useState([])          // ① 数据获取
  const [keyword, setKeyword] = useState('')        // ② 搜索
  const filtered = users.filter(u => u.name.includes(keyword))

  const handleExport = () => {                    // ③ CSV 导出(另一个完全无关的职责)
    const csv = users.map(u => `${u.name},${u.email}`).join('\n')
    download(csv)
  }
  return <div>{filtered.map(u => <UserRow key={u.id} user={u} />)}</div>
}

// 正例:拆分——自定义 Hook 管数据,纯函数管过滤,工具函数管导出,组件只做组装
function useUsers() { /* 数据获取逻辑 */ }
export function filterByName(users: User[], keyword: string): User[] { /* 纯逻辑,可单测 */ }
export function toCsv(rows: User[]): string { /* 纯逻辑,可单测 */ }
function UserListPage() {
  const { users } = useUsers()
  const filtered = filterByName(users, keyword)
  return /* 纯渲染 */
}

2.2 O —— 开闭原则(OCP)

对扩展开放,对修改关闭:加新功能时尽量「新增代码」,而不是「改老代码」。前端最常见的落地方式:策略模式 + 配置表。

// 反例:加一种支付方式就要改 if 链
function pay(order: Order, method: string) {
  if (method === 'wechat') { wechatPay(order) }
  else if (method === 'alipay') { alipayPay(order) }
  else if (method === 'card') { cardPay(order) }
  throw new Error('unknown method')
}

// 正例:策略注册表——新增支付方式 = 新增一个文件,不改 pay 本体
type PayStrategy = { key: string; execute: (order: Order) => Promise<void> }

const strategies: Record<string, PayStrategy> = {
  wechat: { key: 'wechat', execute: wechatPay },
  alipay: { key: 'alipay', execute: alipayPay },
  card:   { key: 'card',   execute: cardPay },
}

async function pay(order: Order, method: string) {
  const s = strategies[method]
  if (!s) throw new Error('unknown method')
  await s.execute(order)
}

再加一个「境外卡」?新增一个策略对象注册进去即可,pay 函数一行不用改。

2.3 L —— 里氏替换原则(LSP)

子类型必须能替换父类型而不破坏行为。前端最实用的版本:「继承/扩展出来的东西,必须遵守基类定下的约定」。违反 LSP 的经典案例:正方形继承长方形(宽高独立设置会让面积逻辑出错)、抛「这个子类不支持」异常的基类方法。

// 反例:ErrorBoundary 吞掉所有错误,包括「不该吞」的
class AllErrorBoundary extends React.Component {
  componentDidCatch(error) { return '发生错误' }   // 连致命错误也吞,页面假死
}

// 正例:区分可恢复/不可恢复,把决定权交还给调用方
class ErrorBoundary extends React.Component {
  componentDidCatch(error) {
    if (error instanceof FatalError) { throw error }   // 致命错误继续抛
    this.props.onRecover?.()
    return '部分功能不可用'
  }
}

2.4 I —— 接口隔离原则(ISP)

不要强迫调用方依赖它不需要的东西。前端表现:组件 props 设计、API 返回类型设计、context 拆分。

// 反例:一个巨型 User 对象传给只显示名字的组件——组件被「无关字段」耦合
<UserCard user={user} />   // UserCard 其实只需要 name 和 avatar

// 正例:按需取字段,组件只声明自己需要的
type UserCardProps = { name: string; avatarUrl?: string }
<UserCard name={user.name} avatarUrl={user.avatarUrl} />

同理:context 拆成多个小 context(主题、语言、用户),避免任何组件被一个大 context 绑架;接口返回类型不要直接复用内部数据模型,按页面需求裁剪。

2.5 D —— 依赖倒置原则(DIP)

高层模块不依赖低层模块,两者都依赖抽象。前端版:业务逻辑不直接 import 具体的 fetch/axios,而是依赖「仓库接口」,实现可以替换(真实 API / Mock / 本地存储)。这正是上一章测试能轻松替换依赖的根基。

// 定义抽象:数据仓库接口(依赖倒置的「抽象」)
export interface UserRepository {
  getUsers(): Promise<User[]>
  getUserById(id: number): Promise<User | null>
}

// 低层实现 A:真实 API
export class HttpUserRepository implements UserRepository {
  async getUsers() { const r = await fetch('/api/users'); return r.json() }
}

// 低层实现 B:内存 Mock(测试 / 演示用)
export class MemoryUserRepository implements UserRepository {
  private data = [{ id: 1, name: 'Tom' }]
  async getUsers() { return this.data }
}

// 高层逻辑只认接口,不认实现
function getUserCount(repo: UserRepository) {
  return repo.getUsers().then(us => us.length)
}
3前端分层架构:让依赖方向清晰可控

核心思想:让「稳定」依赖「稳定」,让「易变」隔离在边缘。

3.1 推荐的分层结构(可裁剪)

src/
├── app/            // 应用组装层:路由、Provider、布局、全局样式(依赖所有层)
├── pages/          // 页面级组件:一页一个文件,只做「组装」
├── features/       // 业务功能域:按业务划分(auth / cart / order),各自私有组件+hooks+api
├── shared/         // 共享基础设施:UI 组件库、请求封装、工具函数、常量(被所有层依赖,不依赖任何业务)
│   ├── components/
│   ├── hooks/
│   ├── api/        // 对后端接口的封装,统一错误处理/鉴权/取消
│   └── utils/
└── types/          // 全局类型定义(DTO、领域模型)

两条铁律:

  • 依赖方向从上到下:app → pages → features → shared,禁止 shared 依赖 features,禁止 features 之间互相 import(要共享就下沉到 shared)。
  • features 自治:一个 feature 的内部(组件、hooks、API、状态)自己管,对外只暴露「页面入口」,这样砍功能、换实现都不波及其他域。
⚠️ 最常见的分层腐败 「顺手」在组件里直接写 fetch(绕过 api 层)→ 错误处理逻辑散落 → 改后端地址要全站 grep。规矩:页面组件永不直接碰网络和本地存储,一律走共享的 api/storage 封装。

3.2 数据流方向:单向数据流是前端的「分层」

状态管理(阶段六)解决的是「状态放哪」,分层架构解决的是「数据往哪流」。理想的流转:

用户操作 → 事件 → 业务逻辑(纯函数/Hook)→ 状态更新 → 视图渲染 → 用户看到
     ↓(需要数据时)
  api 层请求 → 数据转换(DTO → 领域模型)→ 进入状态
💡 数据转换放哪? 后端返回的 snake_case 字段、字符串数字、嵌套结构,别直接塞进组件。在 api 层或业务层做「映射」(mapper):raw => domain。好处:后端字段变化只改一处,领域模型稳定,组件永远用「自己语言」的数据。
4设计模式:前端高频实战清单

GoF 23 种模式不用全背,前端真正高频的不到十种。

4.1 创建型:工厂 / 单例

// 工厂模式:把「创建对象的细节」藏起来
// 比如按类型创建不同图表配置
function createChartConfig(kind: 'line' | 'bar' | 'pie'): ChartConfig {
  const base = { animation: true, responsive: true }
  switch (kind) {
    case 'line': return { ...base, type: 'line', smooth: true }
    case 'bar':  return { ...base, type: 'bar', stacked: false }
    case 'pie':  return { ...base, type: 'pie', donut: false }
  }
}

// 单例模式:全局唯一实例(但注意:模块级导出本身就是单例,通常不需要 class)
// ES Module 天然单例——真正需要手动单例的是「避免重复初始化」的场景
let socket: WebSocket | null = null
export function getSocket(): WebSocket {
  if (!socket || socket.readyState === WebSocket.CLOSED) {
    socket = new WebSocket('wss://api.example.com/ws')
  }
  return socket
}

4.2 结构型:适配器 / 装饰器

// 适配器模式:统一「长得不一样但语义相同」的接口
// 例:不同后端接口返回的日期格式不一样,适配成统一格式
interface OrderDTO { create_time: string }     // 后端 A:'2026-08-20 09:00'
interface OrderDTO2 { createdAt: number }    // 后端 B:时间戳

function adaptOrder(raw: OrderDTO | OrderDTO2): Order {
  if ('create_time' in raw) return { id: raw.id, createdAt: new Date(raw.create_time) }
  return { id: raw.id, createdAt: new Date(raw.createdAt) }
}

// 装饰器模式(函数式版本):给函数加能力而不改它
function withRetry<T>(fn: () => Promise<T>, times = 3): () => Promise<T> {
  return async () => {
    let lastErr: unknown
    for (let i = 0; i < times; i++) {
      try { return await fn() } catch (e) { lastErr = e }
    }
    throw lastErr
  }
}
const loadWithRetry = withRetry(loadData)   // 原函数没动,能力变强了

4.3 行为型:策略 / 观察者(发布订阅)/ 状态机

// 策略模式已在 SOLID-O 里演示(支付方式注册表),这里演示观察者

// 观察者/发布订阅:事件的发布与订阅解耦(EventEmitter 是浏览器原生实现)
type Handler = (payload: unknown) => void

class Bus {
  private map = new Map<string, Handler[]>()
  on(event: string, h: Handler) { const arr = this.map.get(event) ?? []; arr.push(h); this.map.set(event, arr) }
  off(event: string, h: Handler) { this.map.set(event, (this.map.get(event) ?? []).filter(x => x !== h)) }
  emit(event: string, payload?: unknown) { (this.map.get(event) ?? []).forEach(h => h(payload)) }
}

// 状态机:把「一堆 if」变成「状态 × 事件」的转移表——处理复杂流程的利器
// 例:订单状态机
type State = 'created' | 'paid' | 'shipped' | 'done' | 'cancelled'
type Event = 'PAY' | 'SHIP' | 'COMPLETE' | 'CANCEL'

const transitions: Record<State, Partial<Record<Event, State>>> = {
  created:   { PAY: 'paid', CANCEL: 'cancelled' },
  paid:      { SHIP: 'shipped', CANCEL: 'cancelled' },
  shipped:   { COMPLETE: 'done' },
  done:      {},
  cancelled: {},
}

function transition(state: State, event: Event): State {
  const next = transitions[state][event]
  if (!next) throw new Error(`非法转移: ${state} + ${event}`)
  return next
}
💡 状态机的价值 所有「多状态 + 多事件」的业务(订单、审批流、上传任务)用状态机表表达,非法流转直接抛错,比散落的 if/else 可读性强一个量级,且天然可测试(表格逐行测)。

4.4 React 特有模式:Render Props / HOC / 自定义 Hook

三者都是「逻辑复用」的手段,但定位不同:

模式本质场景现状
Render Props组件接收「返回 JSX 的函数」把渲染权交给调用方被 Hook 取代,很少用
HOC函数接收组件返回新组件需要「包装」组件时(鉴权、埋点)被 Hook 取代,仅剩少量场景
自定义 Hook把状态逻辑提取成以 use 开头的函数绝大多数逻辑复用现代 React 首选
// 自定义 Hook 是 React 生态最重要的「设计模式」
function useDebouncedValue<T>(value: T, delay = 300): T {
  const [debounced, setDebounced] = useState(value)
  useEffect(() => {
    const t = setTimeout(() => setDebounced(value), delay)
    return () => clearTimeout(t)
  }, [value, delay])
  return debounced
}

// 组合出更大粒度的 Hook:搜素 + 防抖 + 请求
function useSearch<T>(query: string, fetcher: (q: string) => Promise<T>) {
  const debounced = useDebouncedValue(query, 300)
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(false)
  useEffect(() => {
    if (!debounced) { setData(null); return }
    setLoading(true)
    fetcher(debounced).then(setData).finally(() => setLoading(false))
  }, [debounced, fetcher])
  return { data, loading }
}
5DDD 思想(不搬全套,只取对前端有用的部分)

领域驱动设计的核心不是代码,是「用业务语言组织代码」。

5.1 限界上下文(Bounded Context)

一个词在业务里不同场景含义不同。比如「订单」在下单域(含购物车明细)和售后域(含退款状态)是完全不同的模型。强行统一成一个「大订单模型」会让两端都被对方的概念污染。前端落地点:features 目录就是你的限界上下文——每个 feature 有自己的模型、自己的 api 封装、自己的状态,不跨域共享模型。

💡 检验方法 如果两个页面共享的「模型类型」里有对方用不到的字段,就该考虑拆分上下文。共享的应是「基础设施」,而不是「业务模型」。

5.2 实体 / 值对象 / 聚合(简化版)

概念定义前端例子
实体有唯一标识、有生命周期、状态会变User(id 不变,active 状态变)
值对象没有标识,由「值」本身定义,不可变Money(金额+币种)、Address(直接比较值)
聚合一组对象 + 一个唯一入口(聚合根),外部只能通过根操作Order(含 items,只能通过 order.addItem 改)
// 值对象:不可变 + 值相等
class Money {
  constructor(readonly amount: number, readonly currency: 'CNY' | 'USD') {}
  add(other: Money): Money {           // 返回新对象,不修改自身
    if (other.currency !== this.currency) throw new Error('币种不一致')
    return new Money(this.amount + other.amount, this.currency)
  }
  equals(other: Money): boolean {
    return this.amount === other.amount && this.currency === other.currency
  }
}

// 聚合根:订单只能通过方法修改,外部碰不到内部数组
class Order {
  private items: OrderItem[] = []
  addItem(item: OrderItem) {
    if (this.status !== 'created') throw new Error('已提交的订单不可修改')
    this.items.push(item)
  }
  total(): Money { return this.items.reduce(...) }
}
✅ 前端最实用的 DDD 落地点 ① 用「值对象」消灭魔法数字(金额、时长、日期区间);② 用「聚合根 + 方法」替代「到处 push/setState 修改对象」;③ 用「限界上下文」给 features 划边界。这就够了,不用背整套 DDD 术语。
6Monorepo:多应用共享的基础设施

什么时候需要、怎么搭、代价是什么。

6.1 为什么需要 Monorepo

  • 共享代码:多个前端应用(管理后台 / C 端 / 商家端)共用设计系统、请求层、工具库——不共用就复制粘贴,改 bug 要改三遍。
  • 原子提交:改了公共库 + 使用方,一次提交一起变,不会出现「库升了但引用方没跟上」的中间状态。
  • 统一工具链:一套 lint / test / build / CI 配置管理所有包。

6.2 pnpm workspace + Turborepo 实战

# pnpm-workspace.yaml —— 声明工作区
packages:
  - apps/*          // 应用:web、admin、docs
  - packages/*      // 共享包:ui、utils、request、config
// apps/web/package.json —— 用 workspace: 协议引用本地包
{
  "dependencies": {
    "@company/ui": "workspace:*",
    "@company/request": "workspace:*"
  }
}
// turbo.json —— 定义构建/测试的缓存与调度
{
  "tasks": {
    "build":   { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "test":    { "dependsOn": ["^build"] },
    "lint":    { "dependsOn": ["^build"] }
  }
}
// 运行:turbo run build --filter=@company/web(只构建 web 及其依赖)
⚠️ Monorepo 的代价(必须提前知道) ① 工具链复杂度上升(workspace 解析、依赖提升、turborepo 缓存失效问题);② 权限边界模糊(谁能改公共包?改坏了影响所有应用);③ git 仓库变大、CI 要会做增量缓存。小团队 3 个应用以内、共享代码少于 2 个包时,不要上 Monorepo
7微前端:要不要上,想清楚再上

微前端解决的是「组织问题」,不是「技术问题」。

7.1 微前端解决的真问题

  • 多团队独立交付:不同团队不同节奏发版,互不阻塞。
  • 遗留系统渐进改造:老系统(jQuery)和新系统(React)共存,一个模块一个模块迁移,不用推翻重来。
  • 技术栈异构:确实需要不同栈并存(极少数情况)。
❌ 微前端不解决的问题 性能(多个运行时共享页面只会更慢)、代码共享(用 Monorepo 更直接)、「代码太乱」(那是工程问题不是架构问题)。如果你的团队是「一个团队维护一个系统」,微前端几乎总是错误选择——它引入的复杂度(子应用生命周期、样式隔离、路由协调、通信机制、构建配置)远超收益。

7.2 主流方案对比

方案原理适合注意
iframe浏览器原生隔离极简集成、完全隔离体验割裂(跳转、弹层、通信麻烦)
qiankun基于 single-spa,HTML Entry + JS 沙箱 + 样式隔离国内主流、文档全、上手快沙箱有坑(动态 script、Webpack 5 兼容)
Module Federation(Webpack 5 / Rspack)构建时把子应用打成「远程模块」,运行时按需加载Webpack 系、追求运行时共享依赖依赖版本协商是难点
💡 架构师建议 真需要微前端时,先问自己四个问题:每个子应用有独立团队吗?有独立的发版周期吗?有独立的部署环境吗?共享的数据/依赖多吗?前三个回答是「否」或最后一个回答是「是」,就回到 Monorepo 或单体。
8技术选型与 ADR:让决策可追溯

架构师的每个决策都该是「文档化论证」而非「个人偏好」。

8.1 技术选型五步法

  1. 明确问题:我们在解决什么痛点?不解决它会怎样?
  2. 列候选:3 个以内,别把所有流行库都拉进来比。
  3. 定评估维度并打分:社区活跃度 / 团队熟悉度 / 学习成本 / 长期维护性 / 与现有栈契合度。给每个维度加权。
  4. 做小样验证:用候选技术各做一个 2~3 天的 PoC(Proof of Concept,概念验证),验证「文档里吹的」在真实场景是否成立。
  5. 写 ADR 存档:记录决策、理由、备选、代价。三个月后有人问「为什么用 X」,直接看文档,不用考古。

8.2 ADR(Architecture Decision Record)模板

# ADR-0017:状态管理从 Redux 迁移到 Zustand

状态:已接受(2026-08-20,评审人:王工/李工)

背景
业务页面大量使用「局部状态 + 少量全局状态」模式,Redux 的样板代码
(action 类型、reducer、selector 的重复声明)占开发成本约 15%,
且团队新人上手周期长。我们不需要时间旅行调试、跨组件中间件等高级特性。

决策
新业务模块使用 Zustand;存量 Redux 代码不重写,随迭代自然替换。

理由
- Zustand 样板代码减少约 70%,set/get 心智模型更简单
- 支持 selector + useShallow,性能优化零成本
- 无 Provider 包裹,组件测试更简单
- 可持久化中间件满足当前需求
- 团队 4 人已有 Zustand 实战经验(1 个季度)

备选方案
- Jotai:原子模型更细,但团队不熟,且我们无「极细粒度共享状态」需求
- 维持 Redux:无迁移成本,但样板代码问题持续存在

代价
- 两套状态方案并存 2~3 个季度,新增代码需遵循「新用 Zustand」约定
- 存量 Redux 维护者只剩 1 人,需在交接文档中注明

验证
试点模块(商品详情页)迁移后,新增代码行数下降 30%,
测试用例不变的前提下测试时间下降 25%。
✅ ADR 的三个纪律 ① 轻量——一个 markdown 文件,别搞成评审报告;② 有「代价」章节——没有代价的决策是没想清楚的;③ 定期回访——过期的 ADR 要标记「已废弃」并说明为什么,避免后人被错误指引。
9坑清单:架构设计的 10 个深坑

都是架构师用线上事故和返工换来的教训。

  • 为「未来可能」的需求做架构:YAGNI(You Aren't Gonna Need It)——90% 的「可能需求」永远不会来,架构复杂度却留下了。为当下明确需求设计,为未来留「可演进性」而非「提前实现」。
  • 抽象过早:只有一份实现时抽象 = 猜需求。等第二、三个真实调用方出现再抽象(Rule of Three)。
  • 跨层直连:组件直接 fetch、直接改全局 state,绕过分层。规矩:依赖方向只能向下。
  • 共享可变状态:两个模块通过「全局变量」通信,改起来互相踩。解法:事件总线(解耦)或显式状态管理。
  • 巨型组件:500 行的组件 = 5 个职责 + 0 个测试。解法:按 SRP 拆 Hook + 纯函数 + 子组件。
  • 用继承组织业务:JS 的继承链三层以上基本失控。解法:组合优先(props/Hook/依赖注入)。
  • 不写 ADR:三个月后团队没人知道「为什么这么设计」,只能继续在错误地基上加盖。解法:每个架构决策一个 markdown。
  • 为微前端而微前端:单团队单体应用上微前端,复杂度翻倍、性能下降。解法:先回答组织问题。
  • Monorepo 依赖提升失控:不同包依赖同一个库的不同版本,构建产物膨胀。解法:统一版本策略(shared deps 放根、pnpm.overrides 强制版本)。
  • 架构评审流于形式:评审只看 PPT 不看代码、不验证假设。解法:ADR + PoC 演示 + 上线后回访数据。
10实战项目(选一个做透)

从「会写代码」到「能设计系统」,必须亲手做一次完整决策。

项目 A:给「电商管理后台」做架构重构(推荐)

  1. 找一个你写过的(或开源的)后台项目,按本章分层结构重构:app / pages / features / shared 四层,依赖方向只向下。
  2. 把散落的 fetch 全部收敛到 shared/api 层,统一错误处理与鉴权。
  3. 挑一个复杂业务域(如订单),用「状态机」重写其状态流转,非法转移抛错。
  4. 把「订单」相关的模型、组件、状态抽成独立 feature(限界上下文实践)。
  5. 为这次重构写一份 ADR,含背景、决策、理由、备选、代价、验证数据。
✅ 通关标准:新同事按目录结构就能说出「这个页面数据从哪来、状态放哪」;砍掉一个功能不影响其他域;所有业务规则有单测。

项目 B:从零设计一个「插件化」的富文本编辑器

  1. 用「核心 + 插件」架构:核心只做文档模型 + 渲染 + 命令总线,插件(加粗/表格/图片)通过注册表接入(OCP 实战)。
  2. 命令总线用「策略注册表」实现(可撤销栈 + 命令模式)。
  3. 文档模型用「状态机」管理(只读/编辑/脏标记)。
  4. 每个插件独立成模块,带自己的测试;核心不依赖任何插件。
  5. 写 ADR 说明「为什么插件用注册表而不是继承」。
✅ 通关标准:删掉任意一个插件,核心零改动、编译通过、测试全绿;新增插件只需一个文件 + 一行注册。
11通关自检题

每题都要能「举例子」回答,才算真懂。

1

什么是「好架构」的判断标准?「过度设计」和「设计不足」各举一个你见过的真实例子。

2

SOLID 五原则各解决什么问题?用你自己的代码各举一个违反的例子和修正方法。

3

为什么说「策略模式 + 注册表」是开闭原则的标准落地?新增一种支付方式需要改哪些代码?

4

前端分层架构的依赖方向为什么必须「向下」?shared 被 features 反向依赖会发生什么?

5

适配器模式与装饰器模式的区别是什么?各举一个前端场景。

6

发布订阅模式和直接调用函数有什么区别?什么时候该用事件总线、什么时候该用显式状态管理?

7

用状态机重写一个「if 层层嵌套」的订单流程,非法转移会怎样?为什么状态机天然可测试?

8

实体、值对象、聚合根的区别?为什么值对象要不可变?

9

什么信号出现时该考虑 Monorepo?什么信号出现时该考虑微前端?各自的代价是什么?

10

ADR 必须包含哪几个章节?为什么「代价」章节必不可少?

11

技术选型五步法是什么?为什么「PoC 小样验证」比看文档和社区讨论更可靠?

12

如果有人提议给 10 人团队的新项目上微前端,你会问哪些问题来说服或阻止?把对话流程写出来。