跳转到内容

前端战术设计

前端架构很少因为团队“不知道正确术语”而失败。

ViewModel。Store。Mapper。Command。Query。Anti-Corruption Layer。Feature Boundary。Event。

这些词很快就能解释清楚。

真正困难的是下一个问题:

代码到底放在哪里?

不是架构图里。不是会议室里。也不是 Blog Post 里。

而是在真实项目中:

  • 哪个文件负责加载数据?
  • 哪个文件负责映射 DTO?
  • 哪个文件了解 ViewModel?
  • 哪个文件发起 Command?
  • 哪个文件显示 Toast?
  • 哪个文件决定 Navigation?
  • 哪个文件可以让 Store 重新加载?
  • 又有哪些职责绝不能放进 Component?

这个栏目之所以叫 前端战术设计,就是因为它从这些具体问题开始。

不是从宏大的架构愿景开始。

而是从代码中的切分开始。


这个栏目不会再解释一遍为什么 ViewModel、Store、Anti-Corruption Layer 或 Event 可能有价值。

这些基础内容已经有对应文章:

  • ViewModel Aggregation 解释为什么 ViewModel 是有意识的 UI 决策,而不是改了名字的 Backend DTO。
  • Event-driven Projection 解释为什么 UI State 可以理解为由 Event 形成的投影,而不是一个可以随意修改的数据箱。
  • Anti-Corruption Layer 解释为什么外部模型不应该未经转换就穿过整个前端。

这个栏目做的是另一件事。

它拿起这些思想,然后继续追问:

如果真正写成可运行的 Code Slice,它会是什么样?

Pattern 描述思想。

前端战术设计展示切分。


很多前端一开始都很简单。

Page Component 加载数据。

然后加上 Loading。

然后是 Error Handling。

再加一个 Mapper。

再来一个 Toast。

保存之后需要 Navigation。

列表还要 Reload。

然后出现一个 Sonderfall。

再来第二个 Sonderfall。

最终 Component 不再是 UI 构件,而是一个带 Template 的小型 Use-Case Orchestrator。

或者 Store 逐渐变成所有东西的收集站:

  • HTTP 调用
  • Mapping
  • Command
  • Query State
  • Form State
  • Notification
  • Routing
  • Reload Logic
  • 业务决策
  • 技术 Side Effect

它会工作。

直到有一天不再工作。

前端战术设计试图重新让这些职责变得可见。

不是通过增加更多抽象。

而是通过更有意识的切分。


这个词有意借鉴了 Domain-Driven Design。

在经典 DDD 中,战术设计通常讨论 Entity、Value Object、Aggregate、Repository 或 Domain Service 等具体构件。

前端并不完全一样。

Angular 前端不是 Backend 的缩小版,也不是一个为了让目录看起来更“业务化”就机械复制 DDD 术语的地方。

但其中有一个思想非常有价值:

业务语义必须在代码中拥有明确形态。

对这个栏目来说,这意味着:

  • Read Flow 不只是一个 GET
  • Create Flow 不只是一个 POST
  • ViewModel 不只是换了名字的 DTO。
  • Command 不只是一个顺便调用 HTTP 的 Store Method。
  • Toast 不是保存后“刚好”在某处出现的细节。
  • Routing 也不是必须藏在 Button Click Handler 里的副作用。

这些示例在思想上借鉴了前端领域中的 DDD 实践,其中也包括 Manfred Steyer 在 Angular 生态中推动的一些方法。

但这里不会把它们当作教条直接复制。

它们只是工具箱。


本栏目中的 Code Slice 基于现代 Angular Stack:Angular 21NgRx 21

这个版本选择并不是随意的。

本栏目不是为了说明如何勉强维持历史 Angular 应用,而是希望展示:今天如何用现代工具组织面向业务的前端 Flow:

  • Standalone API
  • Signals
  • NgRx Signal Store
  • 现代 NgRx Pattern
  • 在合适场景下使用函数式 Provider、Guard 和 Resolver
  • 现代 Control Flow 语法
  • 清晰的 Feature Boundary
  • 显式 ViewModel
  • 分离的 Read Flow 与 Command Flow

但这里并不是版本营销。

Angular 21 和 NgRx 21 只是确定工具箱。

真正的问题仍然是架构问题:

职责在哪里?


没有一种切分方式适用于所有项目。

如果业务读取投影复杂、多个 Component 需要消费同一个 State,或者一个 Slice 很可能长期增长,那么拥有独立 Mapper、ViewModel 和测试的 Read Store 可能非常合理。

而在一个很小、非常稳定的 Backoffice Dialog 中,同样的结构也可能只是 Overengineering。

因此,这个栏目不会宣称:

“正确做法就是这样。”

而是:

“可以这样切——现在我们终于可以围绕一段具体结构展开讨论。”

真正需要考虑的始终包括:

  • 团队规模与经验
  • 业务复杂度
  • 预期生命周期
  • 变化压力
  • 测试策略
  • 现有架构
  • 额外结构本身的成本

架构不是目的本身。

但缺少结构也不等于简单。


CRUD 听起来很技术。

但在前端里,它们通常是带有 State、决策和 Side Effect 的业务 Flow。

有些响应不应该属于那个包含 Button 的 Component。

一个成功的 Command 可能触发 Notification、Navigation 或让 Read Store 失效。关键问题不只是它会发生,还包括这类响应应该在哪里被建模

如果一种切分要在团队里真正工作,它必须能够被反复识别。

因此这里也会讨论结构、命名和测试。


前端架构经常在两个极端上失去作用。

太抽象:

“我们需要清晰边界。”

没错。

但具体哪个文件应该移到哪里?

太技术:

“我们使用 NgRx Signal Store 的 withMethodswithHooks。”

也没问题。

但这个 Store 到底负责什么?

这个栏目希望待在两者之间。

具体到足以真正创建文件。

抽象到足以理解切分背后的理由。

也足够开放,可以在真实项目里被质疑和调整。