icestark icon indicating copy to clipboard operation
icestark copied to clipboard

[RFC]微前端样式隔离方案

Open maoxiaoke opened this issue 3 years ago • 6 comments

背景

微前端在样式隔离方案上没有一些比较完备的方案。考虑样式的种类,一个微前端应用通常包含下面几类:

  • 框架应用内部样式
  • 子应用内部样式
  • 三方组件库样式,比如 Fusion、AntD(版本冲突)
  • CSS resets(通常包含在三方组件库中)、Utility classes

通常我们认为最佳的使用方式是:所有共享的样式存在一份,其他内部样式 scoped。但实际情况下,业务开发需要精妙的编排,才能使共享样式独立保持一份。因此在之前的设计中,icestark 做如下推荐:

  • 内部样式 scoped
  • 框架应用的组件样式使用 prefix 能力,比如 Antd 的 prefixCls、Fusion 的 css-prefix

问题

  • 强依赖 css 预处理器,未来 ice.js 推荐直接使用 css 样式来降低项目打包时间

方案

PostCSS prefix 策略

通过如 postcss-prefix-selector 等 PostCSS 插件为所有样式添加 prefix。

在渲染的时候,提供相对应的容器 id 渲染微应用:

<div id="#icestark-microapp">
	<!-- 微应用渲染区域 -->
<div>

<!-- Modal 等组件会逃逸 -->
<div class="next-modal">
<div>

方案存在的问题:

  • 运行时需要感知 prefix
  • 部分组件存在逃逸的可能

方案的优化

为了解决第一个问题,可以放弃微应用编译时时添加 prefix,而是通过运行时添加。运行过程中,改写子应用的样式,并添加一个特定的选择器,达到添加 prefix 的目的。

// Before
.logo {
}	

// After
#icestark-microapp .logo {
}

类似的方案有:https://github.com/samthor/scoped

Shadow DOM 样式隔离

思路是,在 ShadowDOM 外层执行 js 代码,获取 mount 生命周期函数,并渲染在 Shadow DOM 结构中。

const container = document.getElementById("root");
const div = document.createElement("div");
const shadow = div.attachShadow({ mode: "open" });

shadow.innerHTML = '<div id="id">zheshige id</div>';

container.appendChild(div);

ReactDOM.render(
  <HelloMessage name="Jim Sproch" />,
  shadow.querySelector("#id")
);

示例 Demo

Shadow DOM 存在的问题:

  • Modal 这类全局弹窗的样式会丢失

和 PostCSS 存在同样的问题,Modal 组件会挂载到 document.body 下渲染,会导致针对 Modal 的样式不生效,这就是 ShadowDOM 下典型的 'dom 逃逸问题'。解决方案有:

  • 强制组件库暴露的能力规避

比如 AntD Modal 暴露的 getContainer 来处理。

  • hajack ReactDOM.createPortal

基于 React 的场景,大多的 Modal 实现都是通过 createPortal 来实现的。所以可以在执行沙箱中对 createPortal 进行暴力拦截,大致思路如下:

if (p === "ReactDOM") {
  const _targetValue = { ...targetValue };
  if (_targetValue.createPortal) {
    targetValue.createPortal = (Gateway, container) => {
      if (container.parentNode === document.body) {
        const shadow = document.getElementById(getContainer(appName));
        container = shadow.shadowRoot;
      }
      return _targetValue.createPortal(Gateway, container);
    };
  }
}

这种暴力拦截的方式比较恶心,需要覆盖大量的用户场景。

  • getElementId 等 document 下的 api 需要重写

某些方法会忽略 shadow-dom 内的元素,需要列举。

  • React 事件 delegation 不生效

由于 ShadowDOM 的 retarget 会导致 React17 以下的 React 的版本会出现事件不触发的问题。目前也有一些 hack 的方式。

结论

目前来看,相比 ShadowDOM 方案,运行时 PostCSS 方案对微应用的影响更小,是一个更为可行的方案。不过,仍然有以下两点需要考虑:

  • 性能问题
  • DOM 逃逸问题

maoxiaoke avatar Oct 09 '21 07:10 maoxiaoke

运行时动态获取 css 规则并修改是否存在性能问题

ClarkXia avatar Oct 11 '21 12:10 ClarkXia

为了尽可能地减少运行时开销,最终方案会采用静态构建的 scoped 方案。为了避免「运行时需要感知 prefix」,需要子应用提供一个 <div> wrapper,覆盖子应用的渲染区域。

// 微应用
<div id="micro-app-prefix">
   ... 微应用的真实渲染区域
</div>

ice.js 项目可通过 layout 配置。

官方提供 postcss 插件并提供接入指导。

maoxiaoke avatar Nov 15 '21 12:11 maoxiaoke

image 这里描述的是不是有错误,应该是放弃 微应用编译时添加 prefix吧?

xiaobindebingo avatar Mar 18 '22 03:03 xiaobindebingo

@xiaobindebingo 已更正

maoxiaoke avatar Mar 18 '22 12:03 maoxiaoke

MicroModule在加载css时,能否指定挂载节点,我看appendCSS方法支持在指定位置插入css样式,但是这个参数似乎没有暴露出来 使用场景:使用ShadowDOM进行样式隔离的时候需要指定css样式的插入节点 https://github.com/ice-lab/icestark/blob/1afc4101fcf2bfb163f2307106f8dcc6900e3ff6/packages/icestark-module/src/modules.tsx#L250-L252 https://github.com/ice-lab/icestark/blob/1afc4101fcf2bfb163f2307106f8dcc6900e3ff6/packages/icestark-module/src/modules.tsx#L138-L142

HuColin avatar Apr 15 '22 10:04 HuColin

@HuColin 这个方法不是对外的方法。这里有个小优化在于,如果在 shadowDOM 下,挂载到 ShadowRoot 而非 document.root 下,欢迎 pr

maoxiaoke avatar Apr 17 '22 02:04 maoxiaoke