qiankun的CSS沙箱隔离机制详解
qiankun的CSS沙箱隔离机制详解
在微前端开发中,样式冲突是一个常见的问题。本文详细介绍了qiankun框架中的CSS沙箱隔离机制,包括动态样式隔离、影子DOM沙箱和作用域沙箱,以及它们的实现原理和应用场景。
为什么需要CSS沙箱
在qiankun微前端框架中,由于每个子应用的开发和部署都是独立的,将主/子应用的资源整合到一起时,容易出现样式冲突的问题。因此,需要CSS沙箱来解决样式冲突问题,实现主应用以及各子应用之间的样式隔离,确保各自的样式独立运行,互不干扰。
工程化手段
既然CSS沙箱是用来解决样式冲突的问题,那如果我通过工程化手段确保每个样式选择器名称都是唯一的,这样是不是就不需要CSS沙箱了?
使用工程化手段来生成唯一的CSS类名,常见解决方案有:
- BEM:不同项目用不同的前缀或命名规则来确保类名唯一性,避免样式冲突,详见BEM命名规范。
- CSS Module:通过构建工具配置(详见webpack启用css-loader)在构建过程中自动生成唯一的类名。对了,vue3中
<style module>
标签也会被编译为CSS Module,详见Vue.js - 单文件组件|CSS功能。 - CSS-in-JS:在JS中定义CSS样式块,注入到DOM中,详见CSS-in-JS指南。
但是这些方案都存在一些问题:
- 历史包袱:对于老旧项目,尤其是那些未采用现代工程化手段的项目,修改现有代码以支持新的样式管理方案(如BEM或CSS-in-JS)需要大量的重构工作。
- 第三方库:即使你确保了自己的样式选择器唯一,第三方库的样式仍可能会导致冲突。
显然,工程化手段只能解决一部分问题,在实际应用中,可能需要结合使用工程化手段和CSS沙箱,以应对不同的样式管理需求。
乾坤沙箱
乾坤目前存在三种CSS隔离机制,分别是动态样式隔离、影子DOM沙箱和作用域沙箱:
- 动态样式隔离:qiankun默认开启,可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。
- 影子DOM沙箱(Shadow DOM):手动开启,qiankun会为每个微应用的容器包裹上一个shadow dom节点,从而确保微应用的样式不会对全局造成影响。
- 作用域沙箱(Scope CSS):手动开启,qiankun会改写子应用所添加的样式,为所有样式规则增加一个特殊的选择器规则来限定其影响范围。
你可能想问,开关在呢?如何手动开启我想要的沙箱机制?
在这个乾坤API - start({ })中,有一个可选参数sandbox
,用于控制是否开启沙箱以及开启哪种沙箱:
- true:默认值,开启动态样式隔离。
- { strictStyleIsolation: true }:开启影子DOM沙箱。
- { experimentalStyleIsolation: true }:开启作用域沙箱。
动态样式隔离
乾坤会默认开启此沙箱。可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景子应用之间的样式隔离。
实现原理是当子应用被加载时,其对应的样式会被注入到页面中;当子应用被卸载时,qiankun会自动移除其样式,确保页面的样式环境保持干净。
动态样式隔离虽然可以提供很好的隔离效果,但往往存在一些限制条件,所以在现实的使用中基本无法单独满足用户的需求。
对于新的子应用,使用动态样式隔离+工程化手段两种方案结合的方式,基本能够解决样式冲突的问题。
Shadow DOM 沙箱
手动开启,开启代码如下:
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([...]) // 注册子应用
start({
sandbox: { strictStyleIsolation: true } // 开启 Shadow DOM 沙箱
})
这种模式下qiankun会为每个微应用的容器包裹上一个shadow dom节点,从而确保微应用的样式不会对全局造成影响。
Shadow DOM是什么?
Shadow DOM是Web Components技术的一部分,它允许开发者创建一个封闭的DOM树,这个DOM树的样式和脚本与页面的主DOM树是隔离的。通过Shadow DOM,可以确保子应用的样式和脚本不会影响到主应用或其他子应用,从而避免冲突和干扰。
Shadow DOM,可以理解为是存在于DOM中的DOM。
记住!影子DOM是独立存在的DOM,有自己的作用域集,外部的配置不会影响到内部,内部的配置也不会影响外部。
影子DOM允许将隐藏的DOM树附加到常规DOM树中的元素上——这个影子DOM始于一个影子根,在其之下你可以用与普通DOM相同的方式附加任何元素。
这里有一些影子DOM术语:
- 影子宿主(Shadow host):影子DOM附加到的常规DOM节点
- 影子树(Shadow tree):影子DOM内部的DOM树
- 影子边界(Shadow boundary):影子DOM终止,常规DOM开始的地方
- 影子根(Shadow root):影子树的根节点
说了这么多,那如何创建创建影子DOM?
我们可以调用宿主上的attachShadow()来创建影子DOM
我们结合乾坤小demo实际演示一下,影子DOM到底有什么作用?
ok!我们创建了一个qiankun项目,现在主应用和子应用根节点类名相同,都是.App
,主应用根节点背景色设置为黑色,子应用根节点背景色设置为红色
由于qiankun默认的动态样式隔离机制存在缺陷,无法确保主应用和子应用之间的样式隔离,我们发现,子应用污染了主应用的背景色样式
启用Shadow DOM沙箱隔离机制,Later~,一切正常
实现原理
这里我们实现一下Shadow DOM沙箱机制的核心逻辑,对应乾坤的源代码在createElement
方法,可以看这里 -Shadow DOM沙箱源代码
其原理也很简单,就是将子应用模板包裹在Shadow DOM中,使其形成一个独立的样式作用域,确保其样式隔离
<body>
<div id="root">qiankun是一个基于single-spa的微前端实现库</div>
<script>
// 子应用的模版字符串
const template = `<div id="qiankun-xxx">
<div id="app">Shadow DOM沙箱</div>
<style>div{color:red}</style>
</div>`
function createElement(appContent) {
const containerElement = document.createElement('div')
containerElement.innerHTML = appContent
const appElement = containerElement.firstChild // 影子宿主(template模版字符串转换成了真实的dom)
const shadow = appElement.attachShadow({ // 影子DOM(调用宿主上的attachShadow()来创建影子DOM)
mode: 'open',
})
shadow.innerHTML = appElement.innerHTML // 给Shadow DOM附加宿主节点下的内容
appElement.innerHTML = ''
return appElement
}
document.body.appendChild(createElement(template))
</script>
</body>
虽然Shadow DOM是一个强大的技术,但在某些场景下,它并不是一个完美的解决方案
比如,越界的DOM操作,在实际应用中,子应用可能会有操作主文档DOM的需求,比如动态地向主文档document
添加全局组件、弹窗等。这些操作会创建Shadow DOM之外的元素,Shadow DOM的内部样式也就无法对这些元素生效
基于ShadowDOM的严格样式隔离并不是一个可以无脑使用的方案,大部分情况下都需要接入应用做一些适配后才能正常在ShadowDOM中运行起来(比如react场景下需要解决这些问题,使用者需要清楚开启了strictStyleIsolation
意味着什么 - 摘抄自qiankun文档
Scope CSS (Scoped CSS)
手动开启,开启代码如下:
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([...]) // 注册子应用
start({
sandbox: { experimentalStyleIsolation: true } // 开启作用域沙箱
})
这是qiankun一个实验性的样式隔离特性,它的核心思想是通过给子应用中的所有样式选择器添加一个唯一的前缀选择div[data-qiankun="xxx"]
,来限制这些样式的作用范围
对于一个选择器,如果需要限制它的作用范围,可以使用组合选择器的方式。在当前选择器A前面加一个选择器B,使得选择器A只作用在选择器B内部的节点
改写后的代码会表达为如下结构:
// 假设registerMicroApps方法注册的子应用name是react16
.app-main {
font-size: 14px;
}
// 改写后
div[data-qiankun="react16"] .app-main {
font-size: 14px;
}
实现原理
提取和解析样式:当一个子应用被加载时,qiankun会提取子应用中的所有<style>
标签内嵌样式和<link>
标签引入的外部样式,并对其进行解析,获取所有的CSS规则
重写样式规则:qiankun给每个子应用的包裹容器新增唯一标识符data-qiankun
属性,值为通过registerMicroApps
API注册子应用的name
;然后修改子应用的样式选择器,添加前缀选择器div[data-qiankun="xxx"]
,重写选择器
由于作用域沙箱不能直接修改link
标签引入的外部样式,所以会把link
外部样式转化为style
内嵌样式,再给其添加前缀
对应乾坤源代码的入口是createElement
方法,可以看这里 -Scope CSS沙箱源代码
function createElement(
appContent: string,
strictStyleIsolation: boolean,
scopedCSS: boolean,
appName: string,
): HTMLElement {
const containerElement = document.createElement('div');
containerElement.innerHTML = appContent;
const appElement = containerElement.firstChild as HTMLElement;
/**
* CSS样式冲突的处理方式
* 1. shadowDOM
* 2. scoped CSS
*/
if (strictStyleIsolation) {
// ... shadowDOM沙箱逻辑
}
if (scopedCSS) {
// 常量 css.QiankunCSSRewriteAttr = 'data-qiankun'
const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr);
if (!attr) {
// 给子应用的包裹容器新增data-qiankun属性,值为通过registerMicroApps注册子应用的name
appElement.setAttribute(css.QiankunCSSRewriteAttr, appName);
}
// 遍历子应用的所有样式,修改其样式选择器,添加前缀选择器div[data-qiankun="xxx"]
const styleNodes = appElement.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
css.process(appElement!, stylesheetElement, appName);
});
}
return appElement;
}
不足的话,应该是解析子应用的style样式,并为每个选择器添加前缀。这一过程在子应用的加载和渲染时会增加额外的计算开销,尤其是在样式表很大或者包含大量选择器的情况下,可能会影响页面的初始加载性能
沙箱方案
实际的工作中,选择合适的沙箱方案需要根据具体的场景和需求来决定。以下是一些常见的场景及其对应的沙箱选择
单实例模式
单实例模式指的是一次仅加载一个子应用的场景,这种模式下子应用之间不会并发运行,避免了同时多个应用运行导致的冲突
在这种模式下,**动态样式隔离+工程化手段(如BEM命名规范、CSS Modules)**通常就能满足大部分需求。因为在单实例模式中,不需要担心子应用之间的样式和脚本冲突问题。
多实例模式
在多实例模式下,多个子应用可能同时加载和运行,子应用之间的样式和脚本容易产生冲突
在这种模式下,需要更强的隔离性。可以使用作用域沙箱(Scoped CSS Sandbox)+ Shadow DOM沙箱的组合