微前端架构下的样式管控模式:使用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...

1.1k 声望
181 粉丝
0 条评论
推荐阅读
简单优雅的JavaScript代码片段(三):合并请求,成批发出
简单优雅的JavaScript代码片段文章系列:简单优雅的JavaScript代码片段(一):异步控制简单优雅的JavaScript代码片段(二):流控和重试简单优雅的JavaScript代码片段(三):合并请求,成批发出场景说明后端提...

csRyan1阅读 418

你可能不需要JS!CSS实现一个计时器
CSS现在可不仅仅只是改一个颜色这么简单,还可以做很多交互,比如做一个功能齐全的计时器?样式上并不复杂,主要是几个交互的地方数字时钟的变化开始、暂停操作重置操作如何仅使用 CSS 来实现这样的功能呢?一起...

XboxYan25阅读 1.8k评论 1

封面图
那些不用js也能实现的效果
本文首发于公众号:GitWeb,欢迎关注,接收首发推文本文列举几个不需要使用js也能实现的效果一、页面回到顶部回到顶部是页面开发中很常见的一个功能,一般的做法是对回到顶部组件做一个监听,当用户点击的时候,...

Winn11阅读 1.2k评论 6

封面图
Three.js 进阶之旅:全景漫游-初阶移动相机版
3D 全景技术可以实现日常生活中的很多功能需求,比如地图的街景全景模式、数字展厅、在线看房、社交媒体的全景图预览、短视频直播平台的全景直播等。Three.js 实现全景功能也是十分方便的,当然了目前已经有很多...

dragonir12阅读 1.5k

封面图
使用 CSS 渐变来实现波浪动画
之前看到coco的这样一篇文章: 纯 CSS 实现波浪效果!,非常巧妙,通过改变border-radius和不断旋转实现的波浪效果,有兴趣的可以去研究一下。这次尝试一下使用 CSS 渐变来实现这样一个效果,还可以用在文字背景...

XboxYan11阅读 714评论 4

封面图
与众不同的夜间开关交互效果
这个夜间模式切换开关效果是不是很炫酷,在短视频曾刷到过是一个国外的设计师看似是为了难为我们前端开发设计了一个元素超多且动画复杂的开关切换效果。

南城FE9阅读 1.3k评论 4

突发奇想!借助CSS自定义彩色字体来实现多行文本展开收起
之前写过这样一篇文章:CSS 实现多行文本“展开收起”,介绍了一些纯 CSS 实现多行文本展开收起的小技巧,特别是右下角的“展开收起”按钮,用到了浮动布局,非常巧妙,有兴趣的可以回顾一下。

XboxYan9阅读 1.3k

封面图

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart...

1.1k 声望
181 粉丝
宣传栏