汉人 发表于 2024-12-3 11:33:58

qiankun 的 CSS 沙箱隔离机制

为什么需要CSS沙箱

在 qiankun 微前端框架中,由于每个子应用的开发和部署都是独立的,将主/子应用的资源整合到一起时,容易出现样式冲突的问题
因此,需要 CSS 沙箱来解决样式冲突问题,实现主应用以及各子应用之间的样式隔离,确保各自的样式独立运行,互不干扰
工程化手段

既然 CSS 沙箱是用来解决样式冲突的问题,那如果我通过工程化手段确保每个样式选择器名称都是唯一的,这样是不是就不需要 CSS 沙箱了?
使用工程化手段来生成唯一的 CSS 类名,常见解决方案有:
<ol>BEM:不同项目用不同的前缀或命名规则来确保类名唯一性,避免样式冲突,详见 BEM命名规范
CSS Module:通过构建工具配置(详见 webpack 启用 css-loader)在构建过程中自动生成唯一的类名。对了,vue3 中                  `    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>虽然 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,来限制这些样式的作用范围
对于一个选择器,如果需要限制它的作用范围,可以使用组合选择器的方式。在当前选择器A前面加一个选择器B,使得选择器A只作用在选择器B内部的节点
改写后的代码会表达为如下结构
// 假设 registerMicroApps 方法注册的子应用 name 是 react16
.app-main {
font-size: 14px;
}

// 改写后
div .app-main {
font-size: 14px;
}实现原理

提取和解析样式:当一个子应用被加载时,qiankun 会提取子应用中的所有标签内嵌样式和标签引入的外部样式,并对其进行解析,获取所有的 CSS 规则
重写样式规则:qiankun 给每个子应用的包裹容器新增唯一标识符 data-qiankun 属性,值为通过 registerMicroApps API 注册子应用的 name;然后修改子应用的样式选择器,添加前缀选择器 div,重写选择器
由于作用域沙箱不能直接修改 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
    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 沙箱 的组合
参考文档

GitHub - careyke/frontend_knowledge_structure: qiankun中CSS沙箱的实现
究竟什么是Shadow DOM?
使用影子 DOM - Web API | MDN

来源:https://www.cnblogs.com/burc/p/18568341
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: qiankun 的 CSS 沙箱隔离机制