背景
目前,社区比较关注微前端应用样式的”隔离性“,网上已经有诸多文章介绍了微前端的样式隔离方案。然而,如果微应用的样式只有“隔离性”而没有“可操控性”,那么微应用的样式就是定死的:如同一个不接受外部参数的函数,永远只做预先定义的事情,不接受调用者的管控和配置。
在这种缺乏灵活性的微应用架构下,宿主应用很难管控整个站点的样式主题,因为微应用天生封闭而固执。应用场景会大打折扣,比如以下需求就很难实现:
- 主题品牌色从橙色升级成浅蓝色
- 在线切换亮/暗色主题
- 同一个微应用在多个宿主中被使用。在某些宿主中,表现为橙色;在另一些宿主中,则表现为蓝色。
过去的实现方式需要耗费极大的成本:比如,对于每个微应用,为每种主题构建出一份css,然后根据宿主当前的主题状态,用js来切换每个微应用的css。并且,任何一个涉及到主题的修改,都要求开发者修改、重新构建、发布多个微应用,极其繁琐。
根本原因在于,其中的微应用已经将样式硬编码(并且实现了样式隔离),而无法通过宿主的配置来改变,缺乏主子应用之间的信息传递。
因此,就诞生了样式管控的需要:来自不同项目(不同团队,或不同时间开发的应用)的UI集成到一起的时候,就需要样式管控,兼得样式的封装性和可操控性。
样式管控理论同样适用于业务组件、区块。
样式管控方案简介
本文分享一个方案,通过CSS Variable来实现微应用的样式管控:微应用的样式不再是定死的,宿主应用可以通过“传参”的方式来控制它。在这套方案中,微应用的样式就如同一个有可选参数的函数:
function renderStyle(cssVar1 = defaultVar1, cssVar2 = defaultVar2) {
// 使用 cssVar1, cssVar2, cssVar3 来渲染样式
// 其中 cssVar3 是从环境闭包捕获的,无需调用者显式传入
}
在封装了内部样式的同时,又对外提供了可以配置的API。更灵活地满足各种场景的需求。
根据样式变量的默认来源,可以将样式管控模式分为两种:
隔离优先:
- 微应用有自己的默认主题。默认使用自己的主题,不受宿主环境影响。开箱即用,无需任何配置。
- 宿主有掌控权。当宿主想要进行样式管控的时候,可以精确、按需、显式地覆盖每个微应用的主题变量,保证整个应用是和谐一致的。
- 继承优先:微应用的样式变量默认继承宿主环境的值。无需宿主显式覆盖。
下面用几个简单的例子,来介绍这套方案的实现思路。
隔离优先模式
微应用默认使用自己的主题,不受宿主环境影响。宿主可以显式覆盖其中的样式变量。
开箱即用的默认主题
这是微应用挂载的样式:
/* 微应用css */
.widget-k7na5-default-theme {
--button-bg: orange;
}
.widget-k7na5-btn {
background-color: var(--button-bg);
}
其中,widget-k7na5
是微应用的类名前缀,实现样式隔离。你可以用网上的各种方式来实现样式隔离(比如css module、css-in-js),它们都可以与本方案组合使用。
它的DOM结构如下:
<!--微应用根元素-->
<div classname="widget-k7na5-default-theme">
<button classname="widget-k7na5-btn">button</button>
</div>
因此,这个微应用会展示默认的橙色主题。开箱即用,无需配置。
它的关键点在于,在微应用根元素上,定义一份默认的样式变量;然后在微应用内部,引用样式变量来实现样式,而不是将具体值硬编码在样式中。
即使宿主意外地使用了重名的变量名(比如宿主css有html { --button-bg: red; }
),微应用也不会受其影响。因为微应用根元素将这个变量重置为了默认值,这保证了良好的封装性和隔离性。
宿主显式覆盖微应用的样式变量
如果宿主想要将整个站点的主题升级为蓝色,那么微应用就不应该继续表现为橙色。因此宿主需要有覆盖微应用默认主题的能力。
如何做到呢?
首先,微应用要提供一个API,允许宿主配置微应用根元素的类名。
/* 宿主js */
// 宿主加载微应用的时候,定制微应用根元素的类名
function App() {
return <LoadWidget id="widget-instance-list" className="theme-blue" />
}
于是微应用有如下DOM结构:
<!--微应用根元素-->
<div classname="widget-k7na5-default-theme theme-blue">
<button classname="widget-k7na5-btn">button</button>
</div>
微应用加载的样式与前面一样,无需改变:
/* 微应用css */
.widget-k7na5-default-theme {
--button-bg: orange;
}
.widget-k7na5-btn {
background-color: var(--button-bg);
}
因此,微应用的样式始终只用准备一份,无需用js来做动态切换,实现与维护都很简单。
宿主的样式包含如下主题变量定义:
/* 宿主css */
/* 选择器权重高于微应用自己的变量定义 */
.theme-blue.theme-blue {
--button-bg: blue;
}
完成!现在微应用中的button会展示为蓝色主题!
这里的关键点在于,宿主覆盖了微应用根元素上的cssVar样式变量定义。
宿主只需要用这种方式,给每个微应用都加上.theme-blue
的类名,就可以让整个站点都统一变成蓝色主题。在这个主题升级过程中,各个微应用不需要做任何改动、发布。
通过样式API来维持封装性
注意到,宿主始终没有侵入微应用内部的实现,维持了微应用的封装性。宿主使用的仅仅是以下API:
- 通过className来定制根元素类名。
- 微应用支持配置的cssVar变量名。它们就如同函数的具名参数,也是一种API。
如果微应用有一些内部cssVar变量名不希望被外部使用,可以使用特殊的命名规则来避免。
只要保证这两个API能够维持稳定,宿主与微应用就能各自独立迭代。封装性与灵活性兼得。
高级例子:精确控制能力
这种方案简单灵活,宿主可以精确地控制每一个微应用,给不同的微应用传入不同的样式变量。
宿主js如下,给每个微应用分别传入主题类名:
/* 宿主js */
// 宿主加载微应用的时候,可以定制微应用根元素的类名
function App() {
return (
<>
<LoadWidget id="widget-instance-list" className="theme-blue" />
<LoadWidget id="widget-instance-list" className="theme-green" />
</>
);
}
宿主的样式包含每个主题类名的变量定义:
/* 宿主css */
/* 选择器权重高于widget自己的变量定义 */
.theme-blue.theme-blue {
--button-bg: blue;
}
.theme-green.theme-green {
--button-bg: green;
}
这样的话,前者会展示为蓝色主题,而后者会展示为绿色主题。两者相互不干扰,并且完全受宿主控制。
继承优先模式
在前面的隔离优先模式中,微应用默认展示自己的主题。宿主可以覆盖微应用的主题,但是需要给每个微应用显式定制类名,有一些麻烦。
cssVar也可以实现继承优先的方案:微应用默认继承宿主的样式变量,宿主无需给微应用传入额外配置。
损失了一些控制的明确性(在隔离优先模式中,微应用主题明确受控于根元素类名),换取了便捷性。
宿主js:
/* 宿主js */
function App() {
return <LoadWidget id="widget-instance-list" disableDefaultTheme />
}
其中,disableDefaultTheme
使得使得微应用根元素不具有默认主题的类名。因此,微应用DOM结构如下:
<!--微应用根元素-->
<div classname="">
<button classname="widget-k7na5-btn">button</button>
</div>
宿主css:
/* 宿主css */
html {
--button-bg: blue;
}
注意到,宿主直接在全局范围定义主题变量。不需要给每个微应用的根元素定义变量。
微应用加载的css与前面一样,无需改变:
/* 微应用css */
/* 注意,现在根元素没有.default-theme类名,因此这条规则不生效 */
.widget-k7na5-default-theme {
--button-bg: orange;
}
.widget-k7na5-btn {
background-color: var(--button-bg);
}
这样,微应用会默认从宿主环境读取到--button-bg
的值,表现为蓝色。
当然,宿主仍然可以显式覆盖微应用的样式变量。
总结
本文讨论了两种微应用与宿主之间的样式管控模式:
- 隔离优先:默认使用微应用自己的样式变量,宿主可以显式覆盖
- 继承优先:默认继承宿主环境的样式变量
这两种模式可以组合使用,比如,微应用的某一些样式变量使用【隔离优先模式】,另一些样式变量则使用【继承优先模式】。
注意到,在前面的所有例子中,微应用加载的css都没有改变。我们仅仅通过操控根元素类名,就可以实现上述微应用样式管控模式。 因此,微应用不需要为每种模式打包一份css。js的实现也非常简单(操控根元素类名即可)。宿主应用很容易控制、切换整体站点的样式。
这两种模式的实现可以内置到微前端框架中。微应用开发者只需要定义自己要用的主题变量、变量默认值、以及变量的管控模式。由微应用加载器来管理根元素的类名。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。