icestark
icestark copied to clipboard
[RFC]微前端样式隔离方案
背景
微前端在样式隔离方案上没有一些比较完备的方案。考虑样式的种类,一个微前端应用通常包含下面几类:
- 框架应用内部样式
- 子应用内部样式
- 三方组件库样式,比如 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 逃逸问题
运行时动态获取 css 规则并修改是否存在性能问题
为了尽可能地减少运行时开销,最终方案会采用静态构建的 scoped 方案。为了避免「运行时需要感知 prefix」,需要子应用提供一个 <div>
wrapper,覆盖子应用的渲染区域。
// 微应用
<div id="micro-app-prefix">
... 微应用的真实渲染区域
</div>
ice.js 项目可通过 layout 配置。
官方提供 postcss 插件并提供接入指导。
这里描述的是不是有错误,应该是放弃 微应用编译时添加 prefix吧?
@xiaobindebingo 已更正
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 这个方法不是对外的方法。这里有个小优化在于,如果在 shadowDOM 下,挂载到 ShadowRoot 而非 document.root 下,欢迎 pr