跳转到内容

本地开发时必须启动所有微前端吗?

“要改这个功能,请先把整个产品启动起来。”

“要改这个 Remote,请先启动 Host、其他所有 Remote,以及完整产品环境。”

很多项目接下来会变成这样:

Host
├── Remote A
├── Remote B
├── Remote C
├── Remote D
├── Identity Provider
├── 共享 Test Data
└── 多个 Backend Service

只有全部跑起来之后,Remote C 的真正开发工作才能开始。

很自然的问题是:为什么修改 Remote C,开发者电脑上必须同时运行整个产品?

完整的本地 Composition 对某些 Integration Check 很有价值。它可以验证 Host 如何加载 Remote、URL 是否正确、Layout Space 是否合适、共同 Login 在组合产品里是否正常。但它不应该成为每一次业务修改的必要默认环境。

如果一个 Remote 在日常开发中必须依赖完整 Host 和多个其他 Remote 才能工作,那么它的 Deployment 也许比它真正的开发过程更加“独立”。

所以真正的问题不是:

用什么工具可以一次启动多个应用?

更重要的问题是:

如果这是一个自治微前端,为什么日常开发还需要 Host 或其他 Remote?

独立 Deployment 还不等于开发自治

Section titled “独立 Deployment 还不等于开发自治”

自己的 Build 和 Deployment 是独立 Release 的重要条件。

但它们并不能证明 Remote 作为开发单元也真正独立。

很多系统里所谓“独立”看起来是:

声称拥有的独立性
├── 自己的 Build
├── 自己的 Deployment
├── 自己的 Source Code 区域
└── 自己的 Remote Entry

真正的开发自治则更进一步:

实际开发自治
├── 自己的 Mountpoint
├── 自己的业务逻辑
├── 自己的 State
├── 自己的内部 Routing
├── 自己的 Authentication Integration
├── 自己的 API Integration
├── 自己的 Test
└── 可以独立本地启动

Remote 可以技术上单独发布,却依然在每次本地修改时依赖 Host 提供 User State、由 Host 计算的 Permission、来自 Shell Service 的业务数据、Global Store Slice 或内部 Navigation Service。甚至只有其他 Remote 和完整产品环境都准备好之后,它才真正能运行。

这时耦合并没有消失。

它只是从 Build 转移到了 Runtime 和开发流程。

这也不只是组织问题。本地开发周期会非常直接地暴露:真正拥有责任的是谁。如果 Host 必须先准备 State、生成 User Object 或加载业务数据,Remote 才能正常工作,那么 Remote 并没有真正拥有自己的运行前提。

因此,独立 Deployment 很容易制造一种“自治已经实现”的错觉,而日常开发会揭穿它。

为什么 Host 不应该成为 Runtime 生存条件

Section titled “为什么 Host 不应该成为 Runtime 生存条件”

Host 是 Integration Partner。它知道产品 Composition,并提供一个可能的 Mountpoint。

这个角色很重要。

但应该有限。

问题出现在 Host 不只是 Integration,而开始“供养” Remote:

Host
├── 加载 User State
├── 计算 Permission
├── 获取业务对象
├── 管理 Remote State
├── 提供内部 Service
└── 激活 Remote
Remote
└── 只有在这个 Context 中才工作

技术上当然可以这样实现。Mount 时可以传很大的 Object,可以注入中央 Service,也可以开放 Shared State。

但这不是一个中性的技术细节。

每一份传入数据都会变成 Contract。Host 必须知道 Remote 需要哪些信息,必须自己拥有或获取这些信息,还要决定格式,并在变化时协调。Remote 则只在 Contract 被完整满足时才工作。

Host 传递的业务和 Runtime Context 越多,Remote 就越不像一个自治应用,越像 Shell 外包出去的一块 UI。

因此,严格一点的边界很有价值:Host 可以激活 Remote,并把它放进产品 Composition。

但不应该为 Remote 提供那些没有它就无法工作的业务或运行前提。

把 Remote 当成可以独立启动的应用

Section titled “把 Remote 当成可以独立启动的应用”

目标模型应该从 Capability 本身开始:

Capability
├── 业务逻辑
├── UI
├── State
├── 内部 Routing
├── Authentication Integration
├── API Integration
├── Error Handling
└── Bootstrap

Capability 自己负责业务 State、API Boundary、内部 Navigation Path 和 Error Case。Authentication、Configuration 等所需 Infrastructure 也由它自己接入。

这并不意味着它不依赖外部系统。Capability 当然可能需要 API、Identity Provider、Feature Configuration 或受控 Test Data。

开发自治并不是完全隔离。

它意味着:不依赖 Host 作为业务或技术供应服务

一个自治 Remote 可以依赖外部 System。

它不应该依赖 Host 才能作为应用活着。

同一个 Capability 可以通过两个技术入口被激活:

产品运行
Host Mountpoint
└── Mount Remote
本地开发
Remote Mountpoint
└── Mount 同一个 Remote

两个入口启动的是同一个应用。

Capability 自己不应该需要知道,是 Host 激活了它,还是本地 Remote Mountpoint 激活了它。Host Mountpoint 把它放进完整产品。Remote Mountpoint 则为本地开发提供一个很小、可以独立启动的 Application Entry。

这个本地 Mountpoint 不是第二套 Shell。它不会复制 Host,只会执行相同 Bootstrap:初始化 Router、设置本地 Base Path、加载自己的 Configuration、启动 Authentication Integration,并把 Capability Mount 到一个 DOM Target。

它不会伪造 Host Business State,不会复制 Global Product Store,不会创造 Host User Object,也不会复制 Shell 内部 Service。

它只是把同一个 Capability 放在更小的 Runtime Environment 中启动。

Remote Mountpoint 并不是用来替代 Host。

它证明的是:Remote 自己的生存不需要 Host。

Host Mountpoint 与本地 Remote Mountpoint 启动同一个 Capability;业务逻辑、State、Routing、Authentication 和 API Integration 都留在 Remote 内。

Mount Contract 负责激活,不负责供养

Section titled “Mount Contract 负责激活,不负责供养”

Mount Contract 应该有意识地保持小:

Mount Contract
├── Remote Entry
├── Container 或 Route
├── Base Path
├── Mount
└── Unmount

根据 Integration 方式,它甚至可以更小。

关键不在具体函数签名,而在责任。

Mount Contract 用来激活一个应用。

它不是给应用提供业务内容。

User State、Permission、Business Entity、Global Product State、其他 Store Slice、Shell Service、业务 Configuration、Central Event Bus 或内部 Router Object,在这个目标模型里都不应该进入 Mount Contract。

这并不是说任何额外传参在所有场景都绝对禁止。

严格架构立场的价值,是迫使团队看清后果:一旦 Host 传递 Business State,它就必须理解这个 State;一旦 Host 计算 Permission,它就拥有部分 Authorization Logic;一旦 Host 帮 Remote 获取业务对象,就与外部 Capability 的 API 和 Lifecycle 耦合。

所以 Mount Contract 应该描述技术激活,而不是业务供给。

共享 Login,并不需要 Host 持有所有 Auth State

Section titled “共享 Login,并不需要 Host 持有所有 Auth State”

Authentication 是最常被拿来解释“Remote 只能在 Host 里启动”的理由。共同 Login 经常被误解成:所有 Authentication State 必须集中在 Host。

并非如此。

不是:
Host
└── 传 User、Token、Permission
Remote
而是:
Identity Provider 与现有 Browser Session
├── Host 自己集成
└── Remote 自己集成

Host 与 Remote 可以使用同一个 Identity Provider、同一次登录以及同一个 Browser Session。

这并不意味着 Host 必须成为所有 Remote Authentication State 的 Owner。

Remote 仍然可以自己初始化 Authentication Integration、获取所需 Token、发起安全 API Request,并处理 Session 不存在或过期的情况。Capability 内真正的 Permission Decision 也应该留在理解这些业务含义的地方。

因此本地开发完全可以保持同一原则:

Remote Mountpoint
└── 启动 Remote
└── Remote 使用自己的 Authentication Integration

产品环境中使用的 Authentication Integration,也必须能够通过 Remote Mountpoint 初始化,而不是要求 Host 先生成 User State 或 Permission 再传进来。

共享 Infrastructure 不等于 Host 传值。

业务数据也是同样道理。

不利于自治:
Host 加载 Customer Data
→ 传给 Remote
更自治:
Remote 加载自己需要的 Projection
→ 通过自己的 API

Host 不应该理解其他 Capability 的业务内部。Remote 应该自己加载自己需要的数据。它的 API Boundary 属于自己的责任,或者属于它所代表 Capability 的责任。

让 Host 直接传数据,短期确实很方便。也许可以省一个显眼的 API Request,或者复用 Host 已经拿到的数据。

但与此同时,Lifecycle、责任和本地开发环境也被耦合起来。

为了独立启动,Remote Mountpoint 就不得不重新模拟 Host 的数据传递。恰恰在这里,Runtime Coupling 会暴露出来:Remote 模拟的不是自己的 External Dependency,而是一个原本替它准备业务内容的 Host。

日常开发、本地 Integration、产品验证

Section titled “日常开发、本地 Integration、产品验证”

不同问题需要不同 Runtime Environment。把三个模式明确分开,可以避免“每次都启动完整产品”成为条件反射。

Remote Mountpoint
└── Remote
├── 自己的业务逻辑
├── 自己的 APIs
├── 自己的 Authentication Integration
└── 自己的 Test Data

在最内层开发循环里,只启动当前 Remote 和真正必要的 External Dependency。

这个模式适合业务修改、UI 工作、本地 State、Error Handling、API Integration 和 Capability 内 Test。Feedback Loop 很短,也最能暴露 Remote 实际负责什么。

2. 有针对性的本地 Integration Check

Section titled “2. 有针对性的本地 Integration Check”
Host
└── 当前 Remote 以 Development Mode 运行

这里测试的不再只是 Capability,而是它与产品的具体 Integration。

例如:Remote 是否正确加载和 Mount;是否能从产品 URL 进入;Base Path 和 Deep Link 是否正确;Host 提供的 Layout Space 是否适合;共同 Login 在 Composition 中是否正常;Remote Loading Failure 如何表现。

Host + 当前 Remote 的工具模式在这里很实用。其他区域可以从已有 Preview Version 加载,或者直接 Disable;并不需要把所有东西都启动为本地 Development Server。

Host 加 Development Remote,是合理的 Integration Mode。

但不应该因此成为正常开发模式。

Preview 或 Test Environment
├── Host
├── 真实 Remote Version
├── 真实 Infrastructure
└── 选定的产品级 Flow

这个模式用来验证真正的产品 Composition。真实 Remote Version、共同 Infrastructure 和选定 Product-Wide Flow 才是关注点。

完整 Product Landscape 没必要永久复制在每个开发者电脑里。Preview 或 Test Environment 往往比一堆本地、配置各不相同的 Service 更接近真实,也更可复现。

三个模式不是质量等级。

它们回答的是不同问题。

三种分离的验证层:日常开发只通过 Remote Mountpoint 启动当前 Remote;本地 Integration 将 Host 与当前 Development Remote 组合;产品验证使用包含真实 Host、Remote 和 Infrastructure 的 Preview/Test Environment。

当你要验证的就是 Host Integration 时,本地需要 Host。

听起来理所当然,但很多项目把顺序反过来了:因为 Host 总是运行,所以每一项开发都自动在完整 Composition 中进行。Capability Development 与 Product Integration 的边界因此消失。

修改一个 Validation Rule、结果展示或内部 State 时,Host 通常不会额外提供什么信息。

新增入口 Route、修改 Mount Behavior 或处理 Remote Loading Error 时,Host 才真正有价值。

因此 Host 并不是“本地开发不需要的东西”。

它是一个应该被有意识使用的 Integration Partner。

External Dependency 并不自动等于 Host Dependency

Section titled “External Dependency 并不自动等于 Host Dependency”

可以独立启动的 Remote 并不意味着完全不需要 Infrastructure。根据 Capability,它可能需要 API、Identity Provider、Feature Configuration 或受控 Test Data。对应 Service 又可能依赖 Database、Object Storage 或其他技术组件。

本地开发中,这些 Dependency 可以真实存在,也可以有针对性地替代。API Mock、模拟 Error Response、本地或共享 Identity Provider、临时 Capability Environment 都是合理方式。

关键区别是:

合理:
Remote 模拟自己的 External System
对所谓自治很可疑:
Remote 必须模拟 Shell,因为平时 Shell 在供养它

Remote 可以模拟自己的外部系统。

它不应该必须模拟 Host。

这也避免另一个误解:开发自治并不意味着每个 Remote 都必须是一个可以独立销售的完整产品,更不意味着所有 Infrastructure 都必须本地复制。

真正重要的是:这个 Dependency 在业务和运行责任上到底属于谁。

本地能否独立启动,会暴露什么架构问题

Section titled “本地能否独立启动,会暴露什么架构问题”

Local Startability 是非常实用的架构测试。

如果为了一个小修改,必须先启动 Host、更多 Remote 和多个中央 Service,那么每一项 Dependency 都值得被质疑:

启动 Host
→ 启动其他 Remotes
→ 初始化 Global Product State
→ 准备中央 Test Data
→ 完整走一遍 Login Flow
→ 通过 Product Navigation 进入 Capability

原因通常不在 Start Command 本身。

也许业务逻辑在 Host。也许 Shell 持有业务 State、计算 Permission 或提供内部 Service。也许 Remote 读取其他 Store Area,没有自己的 API Boundary,或者依赖 Host 内部 Navigation Object。

还有一种可能:多个 Remote 实际上共同组成同一个 Capability。

这时未必每个 Remote 实现都“错”,但架构声称的业务独立性与 Runtime Reality 并不一致。

当然,也不是每个 Dependency 都自动证明 Boundary 糟糕。一个 Remote 可以独立启动,业务切分仍然可能有问题。Local Startability 是很强的自治信号,但不是唯一证明。

反过来,如果 Remote 离开 Host 就根本无法合理运行或进行业务验证,那么它大概率也不是一个自治 Application Unit。

所有为了本地启动而不得不从 Host 重新构造的东西,都应该被视为潜在 Runtime Coupling,并被明确解释。

本地开发自治不是一个纯舒适性功能。

它是清晰边界可以被观察到的结果。

一个独立 Remote Mountpoint 不只是让启动更快。它迫使架构回答:Capability 真正拥有的责任是什么?如果它自己拥有业务 State、API Boundary 与 Authentication Integration,就可以通过一个很小的 Application Entry 被激活。

Host 仍然重要。它把 Remote 集成到产品,并且在有针对性的 Integration Check 中需要存在。

但它没必要参与 Capability 内每一次日常修改。

共同 Login、共同 Infrastructure 和技术 Standard 都可以继续存在,而不需要 Host 把 User State、Permission 或业务 Entity 传给 Remote。

Local Mountpoint 不会复制第二套 Shell。

它只是把同一个 Capability 放进一个更小 Runtime Environment。

Remote 自己的 External System 可以被模拟;Host 不应该必须被模拟。

所以一个微前端,不应该只做到“可以单独 Build 和 Deployment”。它还应该能够通过自己的 Mountpoint 作为一个独立应用启动、开发和做业务验证。

Local Startability 不会自动证明业务边界一定正确。

但它会非常直接地暴露 Runtime Coupling。

如果 Remote 的正常本地启动必须依赖 Host、其他 Remote 和它们的 State,那么就应该重新问:这里得到的到底是一块自治 Capability,还是一个只是技术上被拆出去的产品片段?

Host 可以 Mount 一个 Remote。但它不应该负责让这个 Remote 活着。