微前端架构下的样式管控模式:使用CSS Variable来实现“样式参数”
背景
目前,社区比较关注微前端应用样式的”隔离性“,网上已经有诸多文章介绍了微前端的样式隔离方案。然而,如果微应用的样式只有“隔离性”而没有“可操控性”,那么微应用的样式就是定死的:如同一个不接受外部参数的函数,永远只做预先定义的事情,不接受调用者的管控和配置。
在这种缺乏灵活性的微应用架构下,宿主应用很难管控整个站点的样式主题,因为微应用天生封闭而固执。应用场景会大打折扣,比如以下需求就很难实现:
- 主题品牌色从橙色升级成浅蓝色
- 在线切换亮/暗色主题
- 同一个微应用在多个宿主中被使用。在某些宿主中,表现为橙色;在另一些宿主中,则表现为蓝色。
过去的实现方式需要耗费极大的成本:比如,对于每个微应用,为每种主题构建出一份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的实现也非常简单(操控根元素类名即可)。宿主应用很容易控制、切换整体站点的样式。
这两种模式的实现可以内置到微前端框架中。微应用开发者只需要定义自己要用的主题变量、变量默认值、以及变量的管控模式。由微应用加载器来管理根元素的类名。
csRyan的学习专栏
So you're passionate? How passionate? What actions does your passion lead you to do? If the heart...
手写一个Parser - 代码简单而功能强大的Pratt Parsing
csRyan阅读 2.7k
还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
XboxYan赞 35阅读 2.7k评论 2
CSS transition 小技巧!如何保留 hover 的状态?
XboxYan赞 30阅读 3.9k评论 2
CSS 如何设置自动滚动定位的“安全”间距?
XboxYan赞 31阅读 2.5k评论 2
【已结束】SegmentFault 思否写作挑战赛!
SegmentFault思否赞 20阅读 5k评论 10
由小见大!不规则造型按钮解决方案
chokcoco赞 17阅读 1.3k
CSS 如何根据背景色自动切换黑白文字?
XboxYan赞 19阅读 1.7k
So you're passionate? How passionate? What actions does your passion lead you to do? If the heart...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。