1

背景

目前,社区比较关注微前端应用样式的”隔离性“,网上已经有诸多文章介绍了微前端的样式隔离方案。然而,如果微应用的样式只有“隔离性”而没有“可操控性”,那么微应用的样式就是定死的:如同一个不接受外部参数的函数,永远只做预先定义的事情,不接受调用者的管控和配置。

在这种缺乏灵活性的微应用架构下,宿主应用很难管控整个站点的样式主题,因为微应用天生封闭而固执。应用场景会大打折扣,比如以下需求就很难实现:

  • 主题品牌色从橙色升级成浅蓝色
  • 在线切换亮/暗色主题
  • 同一个微应用在多个宿主中被使用。在某些宿主中,表现为橙色;在另一些宿主中,则表现为蓝色。

过去的实现方式需要耗费极大的成本:比如,对于每个微应用,为每种主题构建出一份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
1.1k 声望198 粉丝

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart doesn't find a perfect rhyme with the head, then your passion means nothing.