跳转到内容

一个产品能承受多少 UI 自治?

微前端承诺技术自治。团队可以使用不同 Framework、独立开发应用,并根据业务区域选择最合适的技术 Stack。

但这种自由并不只在 Domain Dependency 或 Global Platform Capability 面前停止。它同样会在 UI 表面遇到边界。

对用户来说,一个产品并不是 Host、Remote、Framework 与 Deployment Unit 的集合。他们看到的是 Form、Button、Table、Dialog 和 Navigation。如果一个区域看起来或操作起来与下一个完全不同,用户不会看到“成功的技术解耦”,只会看到一个不一致的产品。

因此会出现一个技术上看似很小、组织上却很重要的问题:

每个 Remote 可以拥有多少视觉与技术 UI 自治,而又不让共同产品分裂?

答案并不一定是强制所有 Remote 使用同一个 Framework 和 Component Library。那会重新取消很大一部分技术自治。Angular Remote 可以使用 Angular Material,React Remote 可以使用 Material UI 或其他 Library,另一个 Remote 也可以使用 Utility Class。

这些技术差异本身不一定有问题。真正的问题是:它们什么时候开始被用户看见或感觉到?

Remote 拥有技术实现。产品拥有身份与行为。

技术 Boundary 对架构很重要,但对 Product Experience 应该尽可能不可见。

用户不应该从页面外观判断“这一页是 Angular,下一页是 React”。也不应该明显感觉两个 Form 来自不同 Component Library,或者某个 Remote 是独立发布的。

Visible Remote Boundary 可能表现为:

  • 切换 Product Area 后颜色变化;
  • Heading Typography 突然不同;
  • Spacing 忽紧忽松;
  • Button 的 Radius、Height 或 State 不一致;
  • Form 在不同时间点 Validation;
  • Error Message 出现在不同位置;
  • 一个区域用 Button Save,另一个自动保存;
  • Keyboard / Focus Behavior 不同。

每一个差异单独看都很小,但叠加起来,产品会像一组松散拼接的 Application。

因此 Technical Autonomy 不等于 Visual Arbitrary Freedom。Remote 可以自己决定如何在当前 Tech Stack 中实现共同 Product Contract,但不能随意重新定义产品在这个区域应该长什么样、如何工作。

UI 自治结束于用户开始看见架构的地方。

Angular、React 与 Vue Remote 通过各自 Theme Adapter 把同一 Product Contract 转换成共同视觉身份与 Interaction Pattern。

保持一致 UI 的技术原则并不复杂。

Remote 首先使用当前 UI Stack 对应的 Component Library,使用其 Public API、官方 Theming Model,并把它连接到共同 Product Token。Spacing、Color、Typography、Radius 与 State 不应该在每个 Use Case 里重新发明。

如果产品约定 Utility Layer,Remote 使用其定义好的 Design Vocabulary。即使必须写 Custom CSS,也仍然从 Semantic Product Token 中取值。

一个合理优先顺序可以是:

  1. 使用现有 UI Library Component。
  2. 使用公开 Component API。
  3. 使用官方 Theming Mechanism。
  4. 使用定义好的 Design Token。
  5. 使用约定 Utility Class。
  6. 基于 Semantic Product Token 写有限 Local CSS。
  7. Free Value 或 Own Component 作为需要明确理由的例外。

这不是不可变法律,而是一种合理的 Burden of Proof。

Own Component 可以有正当理由:业务上特殊的 Visualization、Library 真正缺失某项能力、Browser Workaround,或者它本来就是有意建设的 Product-wide Component。

较弱的理由则是:“这样更快”“我不喜欢这个间距”“就几行 CSS”。

在这样的规则下,UI Consistency 并不是特别复杂的架构问题。Tool 已经存在,Integration Point 可明确,Rule 也能被文档化并部分自动化。

困难的不是技术模型,而是长期一致地执行它。

真实项目里,不一致 UI 很少来自一个明确决定:“我们不要 Design System。”它通常是逐步长出来的。

一个 Team 觉得某个 Button 的 Spacing 不太合适,于是加一个 Local Rule。另一个 Team 把 Card 圆角调大一点。第三个 Remote 重新写一个 Form Control,因为看起来比理解现有 Component API 更快。

任何单次变化都不会立刻破坏产品,而且局部看都很好解释。

问题在重复。

一个独立 Spacing 变成多个 Spacing。一个本地 Color 变成 Local Palette。一个自己写的 Button 之后还要支持 Loading、Error、Disabled 和 Keyboard State。几年后,官方 UI Platform 旁边长出第二套分布式 UI Platform。

这套第二平台没有名字、没有 Roadmap、没有 Owner。它由 Local Selector、Override Component、Copied Style 和 Implicit Convention 组成,却仍然必须在每次视觉或技术变化时一起维护。

UI Consistency 很少因为“没有 Design System”失败,更常见的是 Design System 的规则被当作可选项。

Design System 不是一份建议目录,而是 Product Contract。如果只有在方便时才遵守,那么产品实际上并没有共同 Contract。

“If you’re writing CSS, you’re probably doing something wrong”

Section titled ““If you’re writing CSS, you’re probably doing something wrong””

一个长期项目里形成过一句故意夸张的话:

If you’re writing CSS, you’re probably doing something wrong.

它不是说 Custom CSS 永远错误。没有项目专有 CSS 的 UI 并不总是可能,也不自动更好。

这句话真正作用是把 Burden of Proof 反过来。

共同 UI/Theming Contract 是默认路径。Component Library、Public API、Design Token 与约定 Utility Class 是预期方案。如果偏离,应能解释现有方式为什么真的不够。

否则反过来的局面很快出现:每一个 Local Deviation 都要求 Product Contract 再次自我证明。

为什么 Button 不能高两像素?为什么 Card 一定要同样 Radius?如果 13px 在这个屏幕看起来不错,为什么不能用?

错误的问题是:Team 为什么必须遵守共同 Contract?

正确的问题是:这个具体例外为什么对产品必要?

应该由例外向 Product Contract 证明自己,而不是 Product Contract 向每一个例外证明自己。

这并不反对设计工作,恰恰是在保护它。Design System 只有在新需求首先尝试用其语言解决时才真正有价值。如果发现真实 Gap,可以有意识地把它纳入 Product Contract,而不是把它藏成 Local Special Case。

Custom CSS 经常只被当成 Maintainability 问题。长 Stylesheet、高 Specificity、难追踪 Override 确实有风险,但对多个 Remote 的产品来说,更关键的是:

Custom CSS 可以绕过中央 Theming Contract。

一个 Local Style 今天可能看起来正确,却已经和产品未来变化脱钩。

.remote-card {
color: #2457d6;
font-size: 15px;
padding: 13px;
border-radius: 7px;
}

这些值不会自动跟随新 Corporate Identity、Dark Mode、Typography Scale、Compact Density、新 Accessibility Requirement 或 Design System Migration。

Card 技术上能工作,但已经不再完整参与 Product Theme。

连接 Theme 的 Custom CSS 不一样:

.remote-card {
color: var(--product-color-text);
background: var(--product-color-surface);
font: var(--product-typography-body);
padding: var(--product-space-card-padding);
border-radius: var(--product-radius-card);
}

它仍然是 Custom CSS,但值不是本地发明,而是绑定到产品 Semantic Role。

中央 UI 变化时,这个 Card 可以一起变化。Remote 仍拥有具体 Rendering,却没有开辟平行 Design Path。

因此问题不是“有没有 Custom CSS”,而是有没有脱离 Theme 的 Custom CSS。

更精确的规则是:

Custom CSS 可以扩展 Product Contract,但不能绕过它。

使用 Product Theme、Semantic Token 与 Adapter 的正常路径,与 Hard-coded CSS 绕过 Theming Contract 的捷径。

业务特殊 Visualization 当然可能需要自己的 Layout Rule。复杂 Editor、Diagram 或 Planning Component 未必能只靠 Standard Component 拼出来。但 Color、Typography、Focus State、Spacing 与 Contrast 仍然可以来自共同 Contract。

业务特殊性可以证明 Own Component 合理,却不会自动证明 Own Visual Language 合理。

成熟 Component Library 提供的不只是“某种样子”,还承担了一部分长期维护责任。

Button 不只是带文字的彩色矩形。它需要 Focus State、Keyboard Behavior、Disabled、Loading 与 Contrast。Form Field 需要 Label、Error、State Transition 和 Form Integration。Dialog 需要 Focus Management、Escape Behavior 以及对 Background 的合理处理。

Angular Material、Material UI 等 Component System 提供 API、Convention 与统一 Theming Model。它们不会自动解决所有 Accessibility/UX 问题,但能显著减少产品自己建设和维护的基础设施。

如果持续 Override 这些机制,产品就会逐步把维护责任重新拿回来。

一个 Local Component 起初可能只几行 Code,后来还要处理 Variant、Validation、Responsive、Keyboard Control 和 Browser Update。每一个偏离都会增加不再由 Library Update 覆盖的 Maintenance Surface。

越多覆盖成熟 UI Library,产品就越多接管原本由 Library 负责的维护工作。

合理使用 Library 并不意味着产品必须看起来像官方 Demo。Theme 可以调整 Color、Typography、Density 等参数,以表达 Product Identity。

重要的是这些调整发生在设计好的 Extension System 内。Configured Library 仍然容易升级;由大量 Local Override 拼起来的 Library 则越来越像一个隐形 Fork,即使源码从未真正 Fork。

Utility Class 可以很好地支持 Product Contract。

它们提供有限 Design Vocabulary,让定义好的 Spacing 与 Size 直接可用,也减少大量随意命名的 Local CSS Class。Team 可以组合 UI,而不必为每个小 Layout Decision 创建新抽象。

但前提是 Utility 自己也绑定 Product Contract。

Utility First 不等于 Token First。

Utility System 同样可能允许任意值。gap-[13px] 并不会比 gap: 13px 更符合 Product Design;只是写法更短,Value 仍然是本地发明。

Color、Shadow、Radius、Font Size 也一样。使用 Utility Class 的 Team 完全可以像传统 CSS 一样创造自己的一套 Design Language。

Tailwind、Bootstrap Utility 等不会自动阻止 Local Design Drift。它们可以让“正确路径”更方便,但无法替代 Discipline。

区别在于允许使用的 Vocabulary。

如果 Semantic Product Token 通过 Utility 暴露,那么“Primary Action”之类的 Class 可以指向 Theme 中对应 Role;具体颜色变化时,语义仍然稳定。

如果只暴露 Technical Raw Value,Code 会传播“blue level 6”“spacing 4”之类 Implementation Term,而不是解释它们为什么存在。

Tool 可以让 Contract 更容易被遵守,却不能替你决定一个 Local Deviation 是否业务必要。

共同 Theme 不应该绑定某个特定 Framework,否则 Product Identity 就会依赖某个 Remote 的技术实现。

共同层应该提供 Semantic Product Token:

:root {
--product-color-primary-action: ...;
--product-color-danger: ...;
--product-color-surface: ...;
--product-color-text: ...;
--product-color-text-muted: ...;
--product-typography-heading: ...;
--product-typography-body: ...;
--product-typography-label: ...;
--product-space-card-padding: ...;
--product-space-content-gap: ...;
--product-space-section: ...;
--product-radius-control: ...;
--product-radius-card: ...;
--product-control-height: ...;
}

这些名称首先描述的是 Meaning,而不是当前 Concrete Value。

blue-600gray-100spacing-4 更接近 Technical Representation。它们可以作为 Design System 内部 Raw Token,但作为公开 Product Contract 通常不够稳定。

Primary Action 今天也许是蓝色,换 Corporate Identity 后可能变色,但它仍然是 Primary Action。因此 primary-actiondangersurfacetext-mutedcontrol-heightcontent-spacing 比某个具体颜色等级更有长期意义。

Semantic Token 也允许多个 Product Context。Dark Mode 可以给同一 Role 不同 Value。Compact Mode 可以减小 Control Height。High-contrast Theme 可以改 Color,而无需要求每个 Remote 重新解释 Component。

产品决定一个 Value 代表什么。Remote 决定当前 UI Stack 如何技术映射它。

这样 Contract 保持 framework-neutral。Angular Remote 可以映射到 Angular Material Theme;React Remote 映射到自己的 Component Theme;Utility Stack 映射进配置。

技术形式可以不同,Semantic Role 保持共同。

每个 Remote 一个 Adapter,而不是一套自己的身份

Section titled “每个 Remote 一个 Adapter,而不是一套自己的身份”

不同 UI Stack 需要不同 Integration,这是技术自治的自然结果,不是错误。

一个模型可以是:

Product Tokens
├── Angular Material Theme
├── Material UI Theme
├── Tailwind Configuration
├── Bootstrap Configuration
└── limited Custom CSS

每条分支都把同一个 Product Contract 翻译成当前 Stack 的语言。Adapter 可以技术上完全不同,也不需要相同 API 或内部机制。

真正有问题的是:每个 Remote 自己定义 Color、Typography、Density、Radius、Form State、Error Message 与 Interaction Pattern。那不是多个 Technical Adapter,而是多个 Product Identity。

Own Technical Theme 是允许的,Own Product Language 不是。

Host 或 Product Platform 决定 Active Product Context,包括 Theme、Density、Language 或其他 Global Setting。Remote 通过稳定 Contract 获得 Context,再翻译进自己的技术环境。

Host 不需要知道 Remote 的 Component Library,也不需要控制内部 Theme Implementation。它提供 Meaning 与 Context,而不是 Component-specific Instruction。

边界就很清楚:Platform 拥有 Product Theme,Remote 拥有 Adapter。

更直接地说:

每个 Remote 可以有自己的 Theme Adapter,但不能有自己的 Corporate Identity。

Dynamic Remote 应该能够脱离真实 Host 独立开发和测试,例如通过 Mini Host 本地提供同样的 Theme / Token Contract。

关键是 Mini Host 不能制造第二套视觉现实。随便编 Test Color 或简化 Local Style 一开始很方便,却意味着开发时看到的条件与 Integrated Product 不同。Contrast、Spacing、Density、Theme Switch 等问题会延迟到 Integration 才暴露。

Mini Host 应该使用与真实 Host 相同的 Semantic Role 与 Contract Structure。它可以提供不同定义好的 Test Theme,例如 Light、Dark、Compact、High Contrast。

Mini Host 不替代 Product Platform。它为开发和测试提供同一套 Platform Contract。

一个产品可以拥有完全相同的 Color、Spacing 和 Font,却依然让人感觉不一致。

Remote A 在 Field Blur 时 Validation;Remote B 只在 Submit 后显示 Error;Remote C 每次 Keypress 都改变 Error Message。

三者可以使用同一 Design Token,用户却必须在每个 Product Area 重新学习。

Save 也是一样:一个 Remote 使用明确 Primary Button,一个 Auto-save,另一个把操作藏在 Toolbar Icon 后面。每种做法单独看都可能有理由,但没有共同 Product Context 就会制造矛盾期待。

因此 Design System 不只需要 Visual Token,也需要共同 Interaction Pattern。

例如:

  • Form Validation;
  • Save / Cancel;
  • Loading State;
  • Error Handling;
  • Notification;
  • Confirmation Dialog;
  • Destructive Action;
  • Focus / Keyboard;
  • Navigation;
  • Empty State;
  • Table / Filter。

不是每个细节都必须完全相同。Complex Editor 与简单 Master-data Form 的 Interaction 本来就可能不同。共同 Pattern 定义的是默认行为与用户期待,必要时可以有意识偏离。

例如 Destructive Action 不应该在一个 Remote 立即执行、另一个必须 Confirmation Dialog、第三个依赖几秒 Undo,而这些差异完全没有业务解释。

Design Token 统一 Value。Interaction Pattern 统一 Behavior。

Design Token 统一视觉值;共同 Interaction Pattern 才进一步统一 Validation、Save、Error 与 Focus Behavior。

产品拥有的不只是 Color 与 Typography,也拥有对 UI 行为的基本期待。

这里同样适用 Burden of Proof:业务需求可以证明不同 Pattern 合理,仅仅 Team Preference 不应该。

Modal、Overlay 与 Toast 属于 Interaction Pattern,但它们有一个特别之处:它们不是 Remote 内局部 UI,而是覆盖整个产品。

没有显式 Contract 时,Owner 会随机产生。第一个打开 Overlay 的 Remote 顺便决定 Stacking Context、Focus Behavior 与 ARIA Semantics,而下一个 Remote 也需要 Dialog。一个只把 Modal Render 在自己 DOM Area 内的 Remote,技术上是局部的,产品上却不是:Focus Trap、Escape、Background Inertness 影响整个页面。

因此与 Global Notification 一样,Global UI Surface 是 Platform Capability,而不是“碰巧先 Render 的 Remote”的 Ownership。Platform 提供共同 Modal/Overlay/Toast Contract,包括 Focus Management 与 Semantic Structure;Remote 通过 Contract 请求显示内容,但不决定 Global DOM Surface 放在哪里,也不单独管理 Close 后 Focus Return。

Global UI Surface 需要显式 Platform / Product Ownership,不能由某个 Remote 偶然拥有的一块 DOM 决定。

当多个 Framework 需要同一个 Visual Building Block 时,Web Component 是自然候选,因为可以提供原则上 framework-neutral 的共同实现。

对于这些 Component 可能很合理:

  • 强 Brand Identity 的跨 Framework Element;
  • 小型 Global Platform Component;
  • 清晰封装的 Specialized Component;
  • DOM 与 Behavior 确实必须完全一致的 Building Block。

Brand Component、跨 Framework Control 或高度 Specialized Visualization,用一个 Web Component 提供,可能比每个 Framework 分别重写更不容易 Visual / Functional Drift。

但这不是免费抽象。

Web Component 需要自己的 Property/Event Contract,要与不同 Framework 的 Form/State Model 集成。Accessibility、Focus 与 Keyboard Behavior 仍然由实现负责,也经常需要额外 Framework Adapter、Test Stage 与 Release Process。

共同 Component 同样要 Versioning、Documentation 和长期维护。它是 Platform Product,不只是中性的技术 Container。

Web Component 可以统一某些共同 Building Block,但不会自动替代完整 Design System。

Shadow DOM 尤其不会解决组织协调或 Visual Identity。一个封装良好的 Component 一样可以使用错误 Color、Spacing 或 Interaction。Encapsulation 阻止外部影响,不会自动生成好 Product Contract。

Local UI Autonomy 有真实收益。

Team 可以更快决定、充分利用 Framework Strength,并且不必等待 Central Component Roadmap。短期减少 Coordination,可能提高速度。

但如果只看单个 Team,这个 Cost Calculation 不完整。

更多 Local Freedom 可能需要多个 Theme Adapter、更多 Visual Regression Test 与重复 Accessibility Review。Variant 数量增长,UX Governance 更复杂,Migration 要跨不同 Library 与 Special Case 规划。

Team 通过 Local UI Freedom 节省的 Coordination,产品可能以后用 Harmonization Cost 重新支付。

大规模变化时区别最明显。

如果新 Corporate Identity 一直通过 Product Token 与 Adapter 连接,那么大量变化可以中央准备,各 Remote 更新 Adapter、验证结果、修少量例外即可。

如果存在大量 Free Color、Local Typography Rule 与 Overridden Component,同样变化会变成分布式 Migration。每个 Remote 都要 Search、Evaluate、Change,Visual Drift 很多时候直到 Screenshot 或 Integrated Product 才发现。

今天的 Local Saving,几年后可能由多个 Team 再付一次。

UI Autonomy 在本地很便宜;一旦用户可见,在全局就很贵。

这不代表 Multiple UI Stack 一定经济上错误。如果不同 Domain、Lifecycle 或 Team Structure 真正受益,复杂度完全可以合理。

但它应该被视为真实 Product Investment。每增加一个 Stack,都需要可靠 Theme Adapter、Test 与 Know-how;每一个合理例外都会扩大长期 Maintenance Surface。

技术多样性当然有成本,但 Visible Inconsistency 通常更贵,因为它不仅影响开发,还影响用户 Trust 与 Orientation。

共同 UI Contract 不意味着 Central Team 必须批准每一条 CSS Class。

那样会破坏 Autonomous Team 的收益,并让 Product Contract 本身变成 Bottleneck。好的 Governance 不是 Micromanagement,而是清晰、容易使用的 Boundary。

正确路径必须现实可用:

  • Product Token 技术上容易访问;
  • Theme Integration 成本合理;
  • Mini Host 提供 Product Context;
  • Component 与 Interaction Pattern 有文档;
  • Utility Class 覆盖常见 Layout;
  • Accessibility 基础准备好;
  • Local / Automated Test 可执行。

一个过于难遵守的 Contract,在实践中一定会被更多绕开。如果正确 Token 需要多个 Dependency 与长协调,而自由 CSS Value 一秒钟能写出来,那么 Platform 自己就在奖励偏离。

因此 Governance 必须双向负责。

Team 有责任不因为方便就绕过 Contract;Platform 有责任让正确路径成为最简单、合理的路径。

可以自动化的 Rule 应该自动化。Lint 可以发现某些 Free Value。Visual Test 可以验证 Theme Variant。Reference Implementation 可以展示典型 Form、Table 与 State。

不是每个 Deviation 都能技术上禁止,也没有必要。重要的是 Deviation 保持 Visible、Intentional、Justifiable。

Team 可以拥有 Remote Code,并在约定 Boundary 内高度自治。但它不拥有整个产品的 Visual Identity。

Technical UI Autonomy 在共同 Product Contract 内非常合理。

Remote 可以使用不同 Framework、Component Library 与技术 Theme Implementation。产品的一致性不来自相同 Technology,而来自共同 Semantic Role 与 Behavior Pattern。

Theme Adapter、Component API 与约定 Utility 是默认路径。Custom CSS 与 Own Component 仍然允许,但必须承担解释责任,并继续绑定 Product Contract。

多年后不一致 UI 很少因为“从来没有 Theme”而出现。更常见的是:小例外逐渐不再被当作例外。几个 Free Value 长成平行 Design Language,最终 Harmonization Cost 分散到所有 Remote 和 Team。

UI 自治结束于用户开始看见架构的地方。