战术级前端设计
架构最终还是要落实到代码里。
战略层面的设计可以帮助我们确定领域边界、职责归属,以及哪些部分应该一起演进、哪些部分最好能够相对独立。但当这些原则进入真实项目之后,还会出现另一类问题。
数据到底由哪里加载?ViewModel 在哪里创建?谁负责发起 Command?Mapping、Navigation 和 Notification 应该放在哪里?哪些职责最好一开始就不要进入 Component?
这些问题,就是这里所说的 战术级前端设计 所关注的内容。
为什么叫“战术级”?
Section titled “为什么叫“战术级”?”这个名称有意借鉴了 Domain-Driven Design 中的 Tactical Design。
在 DDD 中,战略设计更关注领域、边界以及不同上下文之间的关系,而战术设计则进一步讨论边界内部的具体实现形式,例如 Entity、Value Object、Aggregate 或 Repository。
Frontend 并不需要照搬这些概念。一个 Angular 项目也不会因为目录里出现了更多 DDD 名称,就自然拥有更好的架构。
真正值得保留的是背后的思想:
架构最终需要在代码中找到具体的表达形式。
在 Frontend 中,这些形式可能完全不同。Read Flow 可以负责为界面建立一个领域导向的 Projection;ViewModel 可以明确表达 View 真正需要的数据;Command 可以描述一次状态变化以及它产生的结果;Mapping 可以避免外部数据模型直接渗透到整个 Frontend。
Notification、Navigation 和 Invalidation 也应该有一个能够解释清楚的位置,而不是恰好在哪个 HTTP 请求完成之后,就顺手写在哪里。
因此,这里的“战术级”指的是:在已经确定的领域边界之内,如何进一步划分代码中的职责。
从架构想法到 Code Slice
Section titled “从架构想法到 Code Slice”很多 Frontend 问题并不是因为团队不知道正确的架构术语。
更常见的情况是,职责会随着需求增加,一点一点移动到当时最方便的位置。
一个 Page Component 一开始可能只负责加载数据。之后加入 Loading State,然后是 Error Handling、Mapping、Toast、保存后的 Navigation、列表 Reload,再加上一两个特殊情况。
慢慢地,这个 Component 就开始承担接近一个完整 Use Case 的编排工作。
Store 也可能经历类似的过程。原本只是管理状态,之后逐渐加入 HTTP、Mapping、Commands、Form Logic、Routing,甚至领域决策。
这种结构往往可以运行很长一段时间。也正因为如此,在问题真正变得昂贵之前讨论职责边界很有价值。
这里的示例不会只展示一个孤立的 Pattern,而是展示小而完整的 Code Slice。重点不仅是 Store、Mapper 或 Command 是否存在,而是它们之间如何协作,以及每个部分的职责应该在哪里结束。
如果想先了解单个 Pattern,可以参考 ViewModel Aggregation、Event-driven Projection 和 Anti-Corruption Layer。
这里会再往前一步:让架构概念真正成为可以运行的代码结构。
没有通用答案
Section titled “没有通用答案”这些示例基于一个现代 Angular 技术栈,包括 Angular 21、NgRx 21、Signals 和 NgRx Signal Store。
版本决定的是我们可以使用哪些工具,并不会替我们做架构决策。
如果一个领域 Slice 会长期演进、Read Projection 较复杂,而且多个 Component 会共享相同状态,那么独立的 Read Store、Mapper、ViewModel 和 Tests 可能非常合理。
但对于一个很小而且长期稳定的 Backoffice Dialog,同样的结构可能只是额外负担。
因此,这些示例并不是在说:
“正确的实现方式就是这样。”
更准确地说,它们提供了一个足够具体的设计,让我们可以真正讨论它。
一个切分是否合适,仍然取决于领域复杂度、预期生命周期、变化频率、团队经验、测试策略,以及项目已有的 Architecture。
好的 Architecture 通常需要一定的结构。
但结构本身并不等于质量。
Code Slices
Section titled “Code Slices”最初的示例从常见的 CRUD Flow 开始,因为这些场景很适合观察 Frontend 中读取和修改数据时所承担的不同职责。
Retrieve Slice:不把加载逻辑放进 Component 展示从 Infrastructure 到 ViewModel 的 Read Flow。
Create Slice:把写操作建模为 Command Flow 不把创建操作只理解为一个 POST,而是把它看作一次具有结果和后续影响的状态变化。
Update Slice:Form State 不是 DTO 讨论 Edit State、Backend Model 和 Command 之间的边界。
Delete Slice:删除也可能是一项领域决策 展示为什么一个看起来简单的 Delete Flow 也可能不只是一次 HTTP 调用。
另外,还有一些反应会跨越单个 Slice。Notification Flow、Command 成功后的 Routing 和 Success 后的 Reload 与 Invalidation 关注的是:这些结果应该在哪里被建模,而不是重新回到最初触发操作的 Component 中。
最后关注的还是职责
Section titled “最后关注的还是职责”Tactical Frontend Design 位于两个常见极端之间。
一边是“我们需要清晰的边界”这样的架构原则。这当然重要,但当团队真正开始实现 Feature 时,还需要知道具体的职责应该放在哪个文件、哪个 Slice 或哪个层次中。
另一边则是“我们使用 NgRx Signal Store、withMethods 和 withHooks”这样的技术选择。它同样可能完全正确,但仍然没有回答这个 Store 到底应该负责什么。
这里希望讨论的是两者之间的部分:足够具体,可以真正写出代码;同时又保留足够的抽象,让我们理解为什么要这样切分。
这些示例不是最终答案。
它们只是足够具体的设计,让我们可以在真实项目中验证、讨论,并在需要时重新调整。