跳转到内容

Shell 会不会变成新的 Monolith?

微前端架构里仍然存在一个中央应用。它加载 Remote、提供外层框架,并作为产品入口。

于是很容易得出结论:Shell 就是新的 Monolith。

这个结论很自然,但仍然太简单。

一个共同产品必须有地方把各个组成部分组合起来。Platform 必须知道哪个 Remote 被放在哪、顶层 Navigation 如何组成,以及某个部分加载失败时怎么办。Authentication Context、Theme、Locale、Telemetry、Global Notification 等共同平台能力,也不会因为 Frontend 被拆成多个责任区域就消失。

这种“中心性”本身不是架构问题。

共同入口还不是 Monolith。真正让它成为 Monolith 的,是共同变更压力。

因此,“Shell 里有多少代码?”不是正确问题。

真正要问的是:哪些变化、哪些 State、哪些业务决策必须经过 Shell?

Shell 技术上可以很大,同时仍然是稳定 Platform Boundary。反过来,一个只有几份文件的小 Shell,如果每个业务变化都要求增加一个新的 Special Case,也可以成为 Integration Monolith。

Shell 是否 monolithic,不取决于大小,而取决于 Change Radius。

Shell 是 Composition Root,不是 Product Brain

Section titled “Shell 是 Composition Root,不是 Product Brain”

Composition Root 是把应用主要组成部分组合起来的地方。Shell 知道有哪些 Building Block、如何激活它们,以及运行它们需要哪些技术前提。

Shell 可以知道:

  • 原则上有哪些 Remote;
  • 如何加载和 Mount;
  • 哪些 Technical Platform Contract 可用;
  • 当前 Global Product Context 是什么;
  • Load Failure 如何处理;
  • Top-level Navigation 如何组合。

这些知识是把独立开发部分组合成可用产品所必需的。

但 Shell 不应该知道:

  • Remote 如何建模自己的业务 State;
  • 它拥有哪些 Entity;
  • 内部业务 Operation 按什么顺序发生;
  • 某次变化之后它要 Reload 哪些数据;
  • 它内部使用哪些 Store 或 Component;
  • 哪个其他 Remote 应该对某个 Domain Event 做出反应。

可以简化为:

Shell as Composition Root
├── activates Remotes
├── provides Platform Contracts
├── manages technical Lifecycles
└── knows Integration Points
Shell as Product Brain
├── knows Domain State
├── interprets Domain Events
├── orchestrates Product Workflows
├── decides Remote Reactions
└── changes for Domain Features

Shell 可以连接 Remote,但不应该业务控制 Remote。

这不意味着 Shell 必须被动或无足轻重。Composition Root 可以配置 Routing、读取 Remote Manifest、为 Global Navigation 投影 Permission、设置 Technical Error Boundary,并提供 Platform Service。这些事情可能需要相当多代码。

真正重要的不是代码量,而是其中包含什么知识。

如果 Shell 知道的是 Integration Point 与 Technical Lifecycle,它仍然是 Platform;如果它开始理解 Invoice、Planning、Order 以及这些业务对象的后续 Operation,它就在自己建模产品。

可持续 Shell 了解技术 Integration Point;Product Brain 则控制多个 Remote 的业务 State 与流程。

前面的文章已经分别讨论 Authentication、Theme、Global Notification 与 Technical Platform Communication,这里不需要重新解释实现细节。重要的是它们共同的特征:它们描述产品作为 Platform 的能力,而不是某个 Remote 的 Domain Model。

因此,中央 Platform Capability 不自动等于中央 Business Logic。Authentication Context、Theme、Locale、Global Navigation、Notification、Telemetry、Feature Configuration、Technical Error Handling 与 Remote Lifecycle,都可以是 Shell 或 Frontend Platform 的合理职责。

核心条件是:

Platform Capability 只有在业务中立,并且不需要理解某个 Product Area 内部 Model 时,才真正可持续。

Shell 可以接受 Notification Intent,却不需要知道之前改变了哪个 Entity。它可以提供 Authentication Context,却不决定某个具体业务 Operation 的最终 Authorization。它可以支持 Navigation,却不建模 Remote 的 Business Process。

问题不是共享能力本身,而是业务知识逐渐附着在共享能力上。

Notification Queue 最后开始解释 Domain Event。Global Navigation 开始成为业务 Workflow。Feature Configuration 变成 Domain-specific Special Case 集合。Authentication Context 被扩展成中央 Entity-level Authorization Engine。

外表的平台职责没有变,但内部语义已经变了。

当 Platform Responsibility 滑向业务职责

Section titled “当 Platform Responsibility 滑向业务职责”

Global Events 文章已经说明:中央 Event Channel 不会自动消除业务耦合。对 Shell 来说,还有第二个结果:它可能开始拥有整个 Reaction Graph。

一个“Planning saved” Event 一开始可能很无害。真正危险的是 Shell 开始从它推导跨 Domain 后果:

Planning Remote reports "planning saved"
Shell decides:
├── reload school districts
├── update statistics Remote
├── increase badge
├── change navigation
└── open success dialog

此时 Shell 已经不只是显示 Notification。它理解业务原因,判断意义,并控制其他 Product Area 的后续动作。

中央中介没有减少 Coupling,只是把 Coupling 收集到了 Host。Planning Remote 依然和 Statistics、Navigation 以及其他 Projection 相互依赖,只是 Dependency 不再出现在业务变化真正发生的位置。

当 Shell 必须知道一个 Remote 为什么做了某件事,以及另一个 Remote 应该如何反应时,它就在变成 Integration Monolith。

Shell 可以展示 Technical Notification Intent,但不应该决定某个业务变化后哪些 Domain Projection 需要 Invalidated。Shell 可以 Mount 一个 Remote,但不应该继续执行它的 Business Process。

当每个产品变化都必须经过 Shell

Section titled “当每个产品变化都必须经过 Shell”

一个非常明显的 Warning Signal 是这样的常规流程:

Remote develops Domain Feature
Shell needs new Route, Event,
State or Special Case
Shell Release and Integration Test
Remote can be released

对于基础性扩展,这可能完全合理。新增 Remote 可能需要新的 Mount。新的 Global Platform Capability 需要有人提供。共同 Product Contract 的有意变化也可能要求协调修改。

问题在于,这个流程变成普通 Local Feature Development 的默认路径。

一个“可以独立 Deploy”的 Remote,如果每次业务修改都必须先改 Shell,就并不真正独立。

合理触发 Shell 修改的例子包括:

  • 新的 Global Platform Capability;
  • 新的 Top-level Navigation Level;
  • 新 Integration Model;
  • Shared Product Contract 变化;
  • 引入全新的 Remote。

而这些变化通常应该留在负责 Remote 内部:

  • 新 Form Field;
  • 新 Domain Operation;
  • Validation Rule 变化;
  • 新内部 Projection;
  • Filter Logic 变化;
  • 对 Local Domain State 的新反应。

不是每次 Shell Change 都是架构错误。真正重要的是“什么变化触发了 Shell Change”。

如果因为 Platform 改变而改 Shell,它就在履行职责;如果一个 Domain 新增 Validation Rule 也必须改 Shell,责任边界很可能已经偏移。

Bundle Size 与 Lines of Code 都不是判断架构 Monolith 的好指标。

Shell 可以技术上很复杂。Routing、Remote Resolution、Telemetry、Authentication、Error Handling、Compatibility Layer 本来就可能很多。只要这些 Mechanism 稳定,并且 Local Domain Change 不会不断触碰它,Shell 仍然可以形成健康 Platform Boundary。

另一个 Shell 可能只有几份文件,却让几乎所有 Product Change 都相互耦合。

Change Radius 至少可以从四个维度观察。

Local Domain Change 是否要求改 Shell Code?

如果 Remote 只是多显示一个字段或增加内部 Operation,就不应该自动产生新的 Host Special Case。否则 Shell 拥有的已经不是 Integration Knowledge,而是 Domain Knowledge。

Remote 的变化是否经常要求重新测试整个 Integrated Application?

一定程度的 Integration Test 很合理。真正危险的是 Local Change 如果离开完整 System Setup 就无法可靠验证,因为 Shell 承担了业务 State 与流程。

Shell 是否必须和 Remote 一起 Release?

独立 Bundle 没有太大意义,如果它真正 Activation 前仍要等待协调 Host Release。技术分离存在,但经济上有价值的 Independent Changeability 已经丢失。

Remote Team 是否在普通 Product Work 中经常需要中央 Shell Team 的 PR、Review 或 Approval?

如果是,中央 Codebase 同时成为中央 Decision Path。Coupling 不只存在于 Repository,也存在于 Ownership 与 Queue。

Monolith 不只表现为源码结构,也表现为共同 Change、Test、Release 与 Decision Path。

Local Remote Change 与 Shell-coupled Change 的 Change Radius 对比。

Global Store 也可以形成逻辑 Monolith

Section titled “Global Store 也可以形成逻辑 Monolith”

Remote Communication 文章已经说明:共同拥有的 Domain Store 会制造 Horizontal Coupling。对 Shell 来说,更进一步的问题是:如果 Shell 成为这个 State Model 的 Owner,会发生什么?

一个有问题的 Shell Store 可能是:

Shell Store
├── Authentication
├── Theme
├── Planning
├── Orders
├── Invoices
├── Filters
├── Selection
├── Search Results
└── Remote Status

并不是 Shell 里的所有 State 都有问题。Authentication Status、Theme、Locale、Global Notification Queue 以及 Remote 的 Technical Loading/Error State 都可能是合理 Platform State。根据产品,Global Navigation Representation 也可能属于这里。

但 Selected Invoice、Current Planning、Order Item、Domain Filter、Editing Draft、Domain Entity 或 Business Workflow 是另一回事。

判断标准是 State 的语义,不是使用了哪一个 Store Library。

如果 Shell 拥有多个 Remote 的 Domain State,它就成了 Shared Product Model 的 Owner。Action 和 Selector 变成 Global Contract。一个 Slice 的变化影响多个 Team。Remote 越来越像对一个中央 State 的 View,即使 Bundle 仍然可以单独发布。

一个 Global Store 可以让技术上分开的 Remote 重新变成逻辑 Monolith。

关键边界不是“Store / No Store”,而是 Platform State 与 Domain Product State。

合法 Platform State 与中央 Domain Product State 的区别。

Shell 必然会 Orchestrate 一些流程。关键是它 Orchestrate 的是什么。

Technical Orchestration 负责把应用组合起来。Shell 可以加载 Remote、管理 Mount/Unmount、处理 Load Failure、组合 Global Navigation、提供 Platform Context、展示 Technical Fallback。

Business Orchestration 则在组合 Business Process。

Shell 不应该在 Order Approval 后继续推进 Order,也不应该在 Planning Save 后决定哪些 Domain Projection 要更新,更不应该协调多个 Remote 的业务步骤或理解具体哪个 Entity 被修改。

这条区别很重要,因为两者表面形式可能完全一样:中央 Code 监听一个 Event,再触发其他动作。但语义完全不同。

Remote Load Failure 后重新 Mount,是 Technical Lifecycle Management。Planning Save 后决定哪些 Domain 重新计算,是 Business Workflow。

Business Process Logic 应该存在于负责该业务的 Domain,或者合适的 Backend / Domain Boundary 中。不能因为 Browser Shell 恰好看得见所有产品部分,就自动把它放进 Shell。

Monolith 也可能藏在 Shared Library 里

Section titled “Monolith 也可能藏在 Shared Library 里”

把中央 Business Coupling 从 Shell File 搬进其他 Library,并不会消除它。

例如:

Shell
└── platform-orchestrator.ts
├── knows invoices
├── knows planning
├── knows orders
└── coordinates Remotes

换成:

shared-platform-state
├── planning slice
├── invoice slice
└── order slice

仍然是中央业务模型。

把 Code 分散到多个 File/Library 可以改善组织方式,却不会自动改变 Responsibility Boundary。

Integration Monolith 不会因为 Domain Knowledge 被拆到多个 Shared Library 就变成 Modular Architecture。

仍然应该问:谁拥有知识?Change 时谁必须修改?哪些 Team、Test 与 Release 被耦合?

所谓 Platform API 也可能业务上中央化。“Platform”这个名字本身没有架构含义。只要 API 包含 Domain-specific Operation 并协调多个 Product Area,它就在形成 Product Center——无论它实现成 Service、Store、Library 还是 Host Module。

独立 Deployment,却没有独立 Changeability

Section titled “独立 Deployment,却没有独立 Changeability”

即使所有 Remote 都技术上 Dynamic Loading,Shell 仍然可以成为 Bottleneck。

典型信号包括:

  • 每个 Team 都经常需要修改 Shell;
  • Shell Review 阻塞 Local Product Work;
  • 很多 Team 共同拥有 Code,但没人明确拥有其中的 Domain Knowledge;
  • Shell Release 要求大范围 Regression Test;
  • 一个有问题的 Shell Release 影响整个产品;
  • Remote 等待中央 Approval;
  • 多个互不相关的 Change 被打包进同一个 Release。

架构图仍然展示“独立 Remote”,Backlog 里却几乎每第二个 Remote Ticket 都还要加一个 Shell Ticket。

这种条件下,Dynamic Remote Architecture 失去关键价值。

Independent Deployment 只有在 Independent Changeability 与 Activation 也存在时才有价值。

Team 可以随时把 Bundle 上传 Registry,但如果新 Feature 仍然必须等 Shell Change、Central Acceptance 与 Coordinated Product Release 才能使用,那么这种技术自由在经济上意义很小。

中央 Shell 可能技术上很稳定,组织上却仍然是 Needle Eye。Platform Team 尤其容易无意进入这种角色:因为它们的 Interface 对全产品可见,所以把更多 Integration Logic 放进去一开始总显得合理。每个 Special Case 都不大,但长期会形成所有 Product Change 必经的 Hand-off Point。

结果是更长 Queue、更大 Regression Test 与越来越强的 Change Anxiety。中央稳定最终用共同变慢来购买。

Thin Shell 常被误解成“尽量少写代码”。这太技术化。

Thin Shell 不一定 File 少,也不一定 Bundle 小。

Thin Shell
├── little Domain Knowledge
├── small stable Contracts
├── clear Platform Ownership
├── low Change Frequency from Domain Features
└── no cross-domain State Coordination

Thin Shell 的“薄”指的是业务知识少,而不是代码少。

它完全可以实现 Routing、Authentication、Telemetry、Theme、Navigation、Remote Resolution、Error Boundary 与 Technical Lifecycle。这些能力可能很复杂,也可能形成不小 Codebase。

只要它们仍然是中立 Platform Capability,普通 Product Feature 不需要修改它,Shell 仍然业务上很薄。

反过来,一个非常小的 Shell 也可以 monolithic。几个 Switch、Event Handler 或 Global Selector 就够了,只要每个新 Feature 都需要增加 Domain-specific Branch。

“Thin”描述的是 Knowledge Reach,而不是 Physical Size。

Shell 认识 Contract,不认识 Domain Model

Section titled “Shell 认识 Contract,不认识 Domain Model”

一个可持续目标模型不是让 Platform 与 Domain 完全隔离,而是让两者只通过很小的 Integration Contract 接触。

Shell / Platform
├── Composition
├── AuthContext
├── ThemeContext
├── Navigation Contract
├── Notification Intent
├── Telemetry
└── technical Remote Lifecycles
Remote
├── own Domain Model
├── own State
├── own Use Cases
├── own API Gateways
└── own Release

Shell 知道 Contract、Technical Lifecycle 与 Product-wide Capability。

它不知道 Remote 内部 Entity、Use Case、Store Structure、Concrete Data Model 或 Domain Follow-up Operation。

Shell 了解 Integration Point,而不是 Remote 内部 Domain Model。

这并不消除所有依赖。组合式产品必然拥有 Shared Convention 与 Contract。目标不是绝对 Independence,而是把依赖控制在正确位置。

Platform Contract 也可以演进。Platform 可以新增 Capability、扩展 Contract、替换 Integration Model。只是相对于普通 Domain Change,这些变化应该更少、有明确 Owner,并且被有意识 Versioning。如果 Backward Compatibility 能够降低各 Team 迁移成本,它就很有价值。

Platform Contract 可以变化,但不能成为每天每个 Domain Change 的 Hand-off Point。

Shell 是否已经成为 Integration Monolith,很少能从一个 Class 直接看出来。更有价值的是问:

  • Team 能否不改 Shell 就开发、测试、发布 Local Domain Change?
  • Shell 是否必须理解一个 Change 的业务原因?
  • 是否汇总多个 Domain 的 State?
  • 是否知道 Domain-specific Event 或 Entity?
  • 是否决定哪个 Remote 应该对 Domain Change 做出反应?
  • Remote Change 是否经常要求 Shell Release?
  • Local Change 是否必须触发整个 Application Test?
  • 几乎所有 Team 是否都经常修改 Shell Repository?
  • Global Store 是否包含多个 Remote 的 Domain State?
  • 如果替换某个 Remote,Shell 是否会留下针对它的业务 Special Case?

一个“是”还不能证明 Monolith。真实产品确实存在 Integration Requirement,有些 Change 必须协调。

但“是”越多,Integration Monolith 越近。

尤其值得警惕的是 Domain Knowledge 与 Organizational Dependency 同时出现:Shell 理解某个 Domain Event,而修改它还要经过多个 Team Approval,那么 Coupling 同时在技术与组织层生效。

Platform 负责连接,Domain Knowledge 保持分布

Section titled “Platform 负责连接,Domain Knowledge 保持分布”

共同 Shell 可以非常有价值。它把产品连接起来,一致提供 Platform Capability,简化技术 Integration,保证统一 Product Experience,并把 Global Technical Rule 放到清晰位置执行。

因此 Monolithic Shell 的替代方案不是“不要 Shell”。

更好的替代方案是:一个业务中立、有清晰 Responsibility 和小 Contract 的 Platform。

Centrality 不是问题。无限增长的 Domain Responsibility 才是。

这样的 Platform 可以刻意中央运营,有明确 Owner、有高质量要求,也可以提供全产品关键 Infrastructure。正因为它重要,更应该保护其 Responsibility Boundary。

越多 Domain Knowledge 搬进 Shell,Coordination、Regression Test、Coupled Release、Onboarding Cost 与 Team Conflict 越大;Failure Blast Radius 也越大。Change Anxiety 会随之增长,因为 Local Change 也可能在中央 Integration Model 中产生意外后果。

业务中立 Platform 则能支持 Local Change、Limited Test、Independent Activation、Clear Ownership 与 Stable Integration Contract。Remote 更容易被替换,因为内部 Model 不会继续活在 Host 中。

当中央稳定只能通过所有人一起变慢来维持时,Shell 在经济意义上就已经成为 Monolith。

可持续 Shell 组合 Application、管理 Technical Lifecycle,并通过小而稳定的 Contract 提供 Shared Platform Capability。它知道 Remote 的 Integration Point,但不知道 Remote 内部 Domain Model。

它是 Composition Root,不是 Product Brain。质量不体现在 File 少,而体现在 Change Radius 有界。Independent Deployment 只有在 Independent Changeability 同样存在时才真正有价值。

Shell 可以组合产品,但它不应该自己变成产品。