微前端场景下如何做样式隔离?

问题示例

className 命名重复导致的样式冲突

在主应用和子应用上分别使用 div 元素插入一段标题,两个 div 元素使用相同的 class 名 title,分别在 class 中设置文字颜色,主应用 color 值为 yellow,子应用为 red。

由于子应用的样式晚于主应用加载,所以主应用的样式会被覆盖

以上问题在同时加载多个子应用时也会存在:各个应用之间也可能存在同名的 className 或者给相同条件的选择器添加了样式, 那么最终只有优先级最高的样式才会生效。要确保应用之间的样式不会互相影响,就需要对应用间的样式进行隔离。

html、body 标签的样式冲突

html 、 body 标签, 在各个应用中都是唯一的元素,其样式必然会对主应用的样式产生影响。

解决方案

为了以上样式冲突问题,通常有以下两种思路:

  • 通过样式命名 和 样式优先级解决
  • 通过宿主环境隔离来达到样式隔离

样式命名 和 样式优先级

假设各个应用之间的样式 className 都是全局唯一的, 那么不同 className 下的样式就一定不会发生冲突。

再加上样式优先级来配合解决,就能解决标签选择器的样式冲突:

  • 例如在原 className 、标签选择器前面再添加一个 selector
  • 标签选择器 + 属性选择器

子应用改造

这里需要处理的样式也分为以下两种:

UI 组件库等引入的全局样式

默认情况下,UI 组件库的 prefixCls 都是相同的,不过它们提供了 ConfigProvider 可以用来修改 UI 组件库全局样式的 prefixCls。

自定义样式

通过 BEM、CSS Modules、 CSS in JS 等手段来获得与其他应用不同的选择器名,来规避样式冲突。

或者直接使用 postcss 的插件,在编译阶段给所有样式添加 prefix selector。

add prefix selector

主应用在运行时统一转换样式

京东的微前端框架MicroApp的样式隔离是默认开启的,开启后会以<micro-app>标签作为样式作用域,利用标签的name属性为每个样式添加前缀,将子应用的样式影响禁锢在当前标签区域。

.test {
  color: red;
}

/* 转换为 */
micro-app[name=xxx] .test {
  color: red;
}

但主应用的样式依然会对子应用产生影响,如果发生样式污染,推荐通过约定前缀或CSS Modules方式解决。

宿主环境隔离

Shadow DOM

附加并隐藏在常规 DOM 下的节点叫做 Shadow DOM —— 它以 Shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,就和普通的 DOM 元素一样,它可以通过方法添加子节点、设置属性,以及为节点添加自己的样式,隐藏的 DOM 样式和其余 DOM 是完全隔离的,类似于 iframe 的样式隔离效果。

如何创建

可以使用 shadowHostElement.attachShadow() 方法来将一个 shadow root 附加到调用方法的元素上。它接受一个配置对象作为参数,该对象有一个 mode 属性,值可以是 open 或者 closed

let shadowRoot = shadowHostElement.attachShadow({mode: 'open'});
let shadowRoot = shadowHostElement.attachShadow({mode: 'closed'});

open 表示可以通过页面内的 JavaScript 来获取 Shadow DOM,例如使用 Element.shadowRoot 属性:

let shadowRoot = shadowHostElement.shadowRoot;

如果将 mode 设置为 closed,那么elementRef.shadowRoot 将会返回 null

浏览器中的某些内置元素就是如此,例如<video>,就包含了不可访问的 Shadow DOM。

为 shadow DOM 添加样式

我们可以通过创建<style> 元素为 Shadow DOM 添加样式,也可以通过创建<link> 元素引用外部样式表。

// 使用 style 元素为 shadow DOM 添加样式
const style = document.createElement('style');
style.textContent = `
    .title {
        color: blue;
    }
`;
shadow.appendChild(style);

// 使用 link 标签为 Shadow DOM 添加样式
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');
shadow.appendChild(linkElem);

优点

  1. 完全隔离 CSS 样式

缺点

  1. 在使用一些 antd Select 组件的时候(很多情况下都是将 open 后的元素默认添加到了 document.body 上 )这个时候它就跳过了阴影边界,逃逸到主应用里面,导致样式丢失,这时候就需要去子应用中手动修正该弹出元素的挂载节点(例如使用 antd select 的 getPopupContainer)。

  2. 会与 react v17 之前的事件代理机制产生冲突,React v16 会各种事件处理函数代理到 document ,但是根据 Shadow DOM 的事件模型,从 Shadow DOM 中冒泡出来的事件 target 都会被调整成 shadow host, 导致 react v16 无法通过 event.target 找到对应的元素并触发事件。

  3. 兼容性还行,需要考虑。

总结

样式隔离实现起来不复杂,各种方案都有其局限性。目前比较稳定的方案还是使用 css Modules 之类的工具配合团队之间协商好样式前缀,从样式命名和优先级上解决问题。

主应用的样式依然可以影响到子应用,优先级也可能会被 !important 等操作被破坏,不过大多数场景下足够了

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值