qiankun源码分析-7.css沙箱

css 沙箱

css沙箱隔离的是css样式,主要是为了防止样式污染,关于样式污染又有一下四种情况:

  1. 单实例场景下的子应用与子应用的样式隔离
  2. 单实例场景下的子应用与主应用的样式隔离
  3. 多实例场景下的子应用与子应用的样式隔离
  4. 多实例场景下的子应用与主应用的样式隔离

qiankun的css沙箱对于每种不同的场景都有不同的处理方式,下面我们来看一下qiankun是如何处理的。

默认的css样式隔离策略

我们以最简单的单例模式为突破口:qiankun默认是启用css隔离的,但是我们要理解的是,这里说的是子应用之间的应用隔离,qiankun子应用css隔离的实现思路是:当我们加载子应用的时候,会给子应用做一层处理,外边加了一层容器,容器中有两个东西:

  • 添加一个<qiankun-head>标签,这个head标签中有子应用的css样式。
  • 原来的内容部分,也就是子应用的内容。

源码如下:

js 复制代码
function loadApp () {
  //...
  // 给子应用添加一个容器<qiankun-head>
  const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);
  //...
}
export const qiankunHeadTagName = 'qiankun-head';
export function getDefaultTplWrapper(name: string, sandboxOpts: FrameworkConfiguration['sandbox']) {
  return (tpl: string) => {
    let tplWithSimulatedHead: string;

    if (tpl.indexOf('<head>') !== -1) {
      // We need to mock a head placeholder as native head element will be erased by browser in micro app
      tplWithSimulatedHead = tpl
        .replace('<head>', `<${qiankunHeadTagName}>`)
        .replace('</head>', `</${qiankunHeadTagName}>`);
    } else {
      // Some template might not be a standard html document, thus we need to add a simulated head tag for them
      tplWithSimulatedHead = `<${qiankunHeadTagName}></${qiankunHeadTagName}>${tpl}`;
    }

    return `<div id="${getWrapperId(
      name,
    )}" data-name="${name}" data-version="${version}" data-sandbox-cfg=${JSON.stringify(
      sandboxOpts,
    )}>${tplWithSimulatedHead}</div>`;
  };
}

上面的源码部分是比较简单的,就是给子应用的html添加一个容器,当我们切换到另一个子应用的时候,会将上一个子应用的容器移除,然后再添加一个新的子应用容器,那么head中的样式自然也就随着子应用的卸载而卸载,这样就可以实现子应用之间的样式隔离了。伪代码如下:

html 复制代码
<html>
<head></head>
<body>
 <div id="__qiankun_microapp_wrapper_for_vue__">
   <qiankun-head>
     <style>
       .app1 {
         color: red;
       }
     </style>
   </qiankun-head>
   <div id="app1">content...</div>
 </div>
</body>
<html>

但问题也是显而易见的,就是子应用的css仍然是作用于全局css上下文的,也就是父子应用之间的样式是没有隔离的,这样就会导致样式污染的问题。那么遇到这种问题,我们应该怎么办?第二种方案呼之欲出,那就是shadowDom。

shadowDom样式隔离策略

在qiankun中可以,通过配置strictStyleIsolationtrue来启用shadowDom

shadowDom是一种浏览器级别的样式隔离方案,它的原理是:shadowDom中的样式只会作用于shadowDom中的元素,不会作用于全局的css上下文中,这样就可以实现样式隔离了。那么我们可以将子应用的内容放在shadowDom中,这样就可以实现子应用与主应用的样式隔离了。 在qiankun中的实现如下:

ts 复制代码
function loadApp () {
  //...
  // 给子应用添加一个容器<qiankun-head>
  const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);

  // 创建元素,这里会判断是否启用shadowDom
  let initialAppWrapperElement: HTMLElement | null = createElement(
    appContent,
    strictStyleIsolation,
    scopedCSS,
    appInstanceId,
  );
  //...
}
// 创建元素
function createElement(
  appContent: string,
  strictStyleIsolation: boolean,
  scopedCSS: boolean,
  appInstanceId: string,
): HTMLElement {
  const containerElement = document.createElement('div');
  containerElement.innerHTML = appContent;
  // appContent always wrapped with a singular div
  const appElement = containerElement.firstChild as HTMLElement;
  // 是否启用严格隔离模式
  if (strictStyleIsolation) {
    const { innerHTML } = appElement;
    appElement.innerHTML = '';
    let shadow: ShadowRoot;

    shadow = appElement.attachShadow({ mode: 'open' });
    shadow.innerHTML = innerHTML;
  }

  return appElement;
}

上面的代码,在我们之前的基础上,有了一些改动,在创建元素的时候,会根据strictStyleIsolation的值判断是否启用shadowDom,如果启用了shadowDom,那么就会将子应用的内容放在shadowDom中, 伪代码如下:

html 复制代码
<html>
<head></head>
<body>
<div id="__qiankun_microapp_wrapper_for_vue__">
  #shadow-root (open)
    <qiankun-head>
      <style>
        .app1 {
          color: red;
        }
      </style>
    </qiankun-head>
    <div id="app1">content...</div>
</div>
</body>
<html>

如上使用shadow-dom的方式,就可以实现dom间的样式隔离,那么子应用与主应用的样式隔离自然也就实现了,除此之外,在多实例场景下,shadowDom仍然可以实现子应用之间的样式隔离,但是shadow DOM也不是绝对的银弹, 网上说的兼容性问题倒也并不是最大的问题,最大的问题还是因为shadow DOM的隔离,在我们日常开发中,会遇到以下三个问题:

  1. 兼容性问题
  2. 可能会用到第三方UI库,比如element-ui,第三方库的dialog组件、drawer组件,当这些组件挂载到全局document.body时,但由于我们的子应用的样式只能作用于shadow DOM中,所以这些组件的样式是无法作用于shadow DOM中的,这样就会导致样式丢失的问题。
  3. 当我们按照常规思维去访问元素,是访问不到的,比如我们通过document.getElementById('app')是访问不到的,因为shadow DOM是一个封闭的DOM树,我们无法访问到shadow DOM中的元素(当然可以通过改造获取,详见shadowRoot使用),这个问题很头疼。

以上三点,我认为是shadowDom的核心问题所在,并导致在v3版本中弃用

接下来我们看qiankun提供的第三中样式隔离方案experimentalStyleIsolation,

experimentalStyleIsolation样式隔离策略

experimentalStyleIsolation 被设置为 true 时,qiankun 会改写子应用所添加的样式为所有样式规则增加一个特殊的选择器规则来限定其影响范围,官方文档中的例子如下:

css 复制代码
// 假设应用名是 react16
.app-main {
  font-size: 14px;
}

div[data-qiankun-react16] .app-main {
  font-size: 14px;
}

这种方案类似 Vue 的scoped,会给应用添加一个 data-qiankun=${appInstanceId} 的属性标识,通过对 style的textContent 进行重写(rewrite) CSSRule 的方式解析添加前缀。核心实现代码如下:

ts 复制代码
export const process = (
    appWrapper: HTMLElement,
    stylesheetElement: HTMLStyleElement | HTMLLinkElement,
    appName: string,
): void => {
  // lazy singleton pattern
    if (!processor) {
        processor = new ScopedCSS();
    }

    const mountDOM = appWrapper;
    if (!mountDOM) {
        return;
    }

    const tag = (mountDOM.tagName || '').toLowerCase();

    if (tag && stylesheetElement.tagName === 'STYLE') {
        const prefix = `${tag}[${QiankunCSSRewriteAttr}="${appName}"]`;
        processor.process(stylesheetElement, prefix);
    }
};
// rewrite逻辑
private rewrite(rules: CSSRule[], prefix: string = '') {
    let css = '';

    rules.forEach((rule) => {
        // 区分不同的rule类型,然后分别处理
        switch (rule.type) {
            case RuleType.STYLE:
                css += this.ruleStyle(rule as CSSStyleRule, prefix);
                break;
            case RuleType.MEDIA:
                css += this.ruleMedia(rule as CSSMediaRule, prefix);
                break;
            case RuleType.SUPPORTS:
                css += this.ruleSupport(rule as CSSSupportsRule, prefix);
                break;
            default:
                if (typeof rule.cssText === 'string') {
                    css += `${rule.cssText}`;
                }

                break;
        }
    });

    return css;
}
// 重写style
styleNode.textContent = css;

上面的代码是experimentalStyleIsolation的核心实现,核心思路是:通过重写CSSRule的方式,给样式添加一个前缀,这样就可以实现样式隔离了,但是这种方式也是有缺陷的,比如我们刚刚提到的

  • 第三方UI库的往body插入modal的样式丢失问题

总结:

qiankun的三种样式隔离,都有各自的优点和缺陷,没有哪种方案是绝对的银弹,在实际开发中,我们需要根据使用场景选择改动成本最小的方案。

相关推荐
hvinsion几秒前
HTML 迷宫游戏
前端·游戏·html
m0_672449604 分钟前
springmvc前端传参,后端接收
java·前端·spring
万物得其道者成14 分钟前
在高德地图上加载3DTilesLayer图层模型/天地瓦片
前端·javascript·3d
码农君莫笑32 分钟前
Blazor用户身份验证状态详解
服务器·前端·microsoft·c#·asp.net
万亿少女的梦16833 分钟前
基于php的web系统漏洞攻击靶场设计与实践
前端·安全·web安全·信息安全·毕业设计·php
LBJ辉39 分钟前
1. npm 常用命令详解
前端·npm·node.js
闲人陈二狗1 小时前
Vue 3前端与Python(Django)后端接口简单示例
前端·vue.js·python
你挚爱的强哥1 小时前
基于element UI el-dropdown打造表格操作列的“更多⌵”上下文关联菜单
javascript·vue.js·elementui
陈随易2 小时前
开源巨变:Anthony Fu引领前端版本控制新时代
前端·后端·程序员
嶂蘅2 小时前
【调研】Android app动态更新launcher_icon
android·前端·程序员