关于css:CSS-SandBox

8次阅读

共计 19246 个字符,预计需要花费 49 分钟才能阅读完成。

引言

本篇文章次要介绍的是对于 CSS Sandbox 的一些事件,为什么要介绍这个呢?在咱们日常的开发中,款式问题其实始终是一个比拟耗时的事件,一方面咱们依据 UI 稿一直的去调整,另一方面随着我的项目越来越大可能哪一次开发就发现——诶,我的款式怎么不起作用了,亦或是怎么被另一个款式所笼罩了。起因可能有很多:

  • 不标准的命名导致反复
  • 为了简略,间接增加全局款式的批改
  • 款式的不合理复用
  • 多个我的项目合并时,每个子项目都有本人的独立款式和配置,可能在本人我的项目中不存在这样的问题,然而合并当前相互影响造成了款式净化
  • 第三方框架引入
  • ……

CSS Sandbox 正式为了隔离款式,从而解决款式净化的问题

利用场景

通过上述咱们理解了款式净化产生的起因,从中咱们也能够总结一下哪些场景时咱们须要应用 CSS Sandbox 进行款式隔离呢

  • 微前端场景下的父子以及子子利用
  • 大型项目以及简单我的项目的款式抵触
  • 第三方框架以及自定义主题款式的笼罩
  • ……

常见的解决方案

既然说了这么多样式净化产生的起因和利用场景,那咱们该如何解决他们呢,目前有以下几种解决方案,其实解决的外围还是不变的——使 CSS 选择器作用的 Dom 元素惟一

Tips:当咱们在理论的开发中能够依据我的项目的理论状况进行抉择

CSS in JS

看名字是不是感觉很高级,直译下就是用 JS 去写 CSS 款式,而不是写在独自的款式文件里。例如:

<p style='color:red'>css in js</p>

这和咱们传统的开发思维很不一样,传统的开发准则是 关注点拆散 ,就比方咱们常说的不写 行内款式 行内脚本,即 HTML、JS、CSS 都写在对应的文件里。

对于 CSS in JS 不是一个新兴的技术,他的热度次要呈现于一些 Web 框架的倒退,比如说:React,它所反对的 jsx 语法,能够让咱们在一个文件中同时写 js、html 和 css,并且 组件 外部治理本人的款式、逻辑,组件化开发的思维深入人心。

const style = {color: 'red'}

ReactDOM.render(<p style={style}>
     css in js
  </h1>,
  document.getElementById('main')
);

每个组件的款式由本身的 style 决定,不依赖也不影响内部,从这一点来看的确实现了款式隔离的成果。

对于 Css in js 的库也有很多,比如说:

  • styled-components
  • polished
  • ······

其中 styled-components 会动静生成一个选择器

import styled from 'styled-components'

function App() {
  const Title = styled.h1`
    font-size: 1.5em;
    text-align: center;
    color: palevioletred;
  `;

  return (
    <div>
      <Title>Hello World, this is my first styled component!</Title>
    </div>
  );
}

优缺点

| 长处 | • 没有作用域的款式净化问题(次要指的是通过写外行款式以及生成惟一的 CSS 选择器)

• 缩小了无用款式的沉积,删除组件即删除对应的款式

• 通过导出定义的款式变量不便进行复用和重构
毛病 • 内联款式不反对伪类和选择器等写法

• 代码的可读性比拟差,违反了关注点拆散的准则

• 运行时会耗费性能,动静生成 CSS(咱们在写 CSS 时其实还是 js)

• 不能联合一些 CSS 预处理器,无奈进行预编译 |

款式约定

通过约利用的命名前缀实现对立的开发和保护,比如说 BEM 的命名形式,通过对块、元素以及修饰符三者的命名来标准的形容一个组件

.dropdown-menu__item-button--disabled

优缺点

| 长处 | • 款式隔离

• 语义化强,组件可读性高
毛病 • 命名太长
• 依赖于开发者的命名

预处理器

通过 CSS 预处理器能够解决很多独特的语法格局,比方:

  • 可嵌套性
body {
    with: 20px;
    p {color: red;}
}
  • 父选择器
body {
    with: 20px;
    &:hover {with: 30px;}
}
  • 属性继承
.dev {width: 200px;}

span {.dev}

通过这些非凡的语法让 CSS 更容易解读和保护

一些常见的市场上的预处理器

  • Sass
  • Less
  • Stylus
  • PostCss

优缺点

| 长处 | • 可读性较好,不便了解和保护 DOM 构造

• 利用嵌套等形式,也能够大幅度解决款式净化的问题
毛病 •须要减少额定的包,借助相干编译工具

Tips:通常与相似于 BEM 的命名形式联合,能够达到进步开发效率,加强可读性以及复用的成果

CSS Module

顾名思义就是将 CSS 进行模块化解决,编译好后能够防止款式被净化的问题,不过依赖于 Webpack 须要配置 css-loader 等打包工具,以下是我在 create-react-app 创立的我的项目中运行,因为其曾经在 webpack 配置了css-loader,因而在此篇文章中不展现具体配置

index.ts 文件

import style from './style.module.css'

function App() {

  return (
    <div>
      <p className={style.text}>Css Module</p>
    </div>
  );
}

style.module.css 文件

.text {color: red;}

// 等同于
:local(.text) {color: blue;}

// 还有一种全局模式,此时不会进行编译
:global(.text) {color: blue;}

打包工具会同时把 style.text 以及 text 编译成举世无双的值

优缺点

| 长处 | • 学习老本较低,不依赖于人工束缚

• 基本上能 100% 解决款式净化问题

• 不便实现模块的复用
毛病 • 只能在构建时应用,依赖于 css-loader 等

• 可读性差,在控制台调试时呈现 hash 值不不便调试 |

Shadow DOM

它能够将一个暗藏且独立的 DOM 附加到一个元素上。当咱们用 Shadow DOM 包裹一个元素后,其内款式不会对外部款式造成影响,内部款式也不会对其外部造成影响

// 创立一个 shadow dom,我这里是通过 ref 去拿附着的节点,个别能够用 document 去拿
import './App.css'; // 定义了 shadow-text 的款式

function App() {const divRef = useRef(null)

  useEffect(() => {if(divRef?.current) {const { current} = divRef
      const shadow = current.attachShadow({mode: 'open'}); // mode 用来管制是否用 js 获取 shaow dom 内的元素
      shadow.innerHTML = '<p className="shadow-text">Here is some new text</p>';
    }
  }, [])

  return (
    <div>
      <div ref={divRef} className='shadow-host'></div>
    </div>
  );
}

内部款式无奈影响 shadow dom 外部的款式

咱们再来看下 shadow dom 外部得款式会影响内部款式吗?

function App() {useEffect(() => {if(divRef?.current) {const { current} = divRef
      const shadow = current.attachShadow({mode: 'open'});
      shadow.innerHTML = '<style>.shadow-h1 {color: red} </style><p class="shadow-h1">Here is some new text</p>';
      
    }
  }, [])

  return (
    <div>
      <Title>Hello World, this is my first styled component!</Title>
      <h1 className='shadow-h1'>lalla1</h1>
      <div ref={divRef} className='shadow-host'></div>
    </div>
  );
}

然而也有例外,除了[:focus-within](https://developer.mozilla.org/zh-CN/docs/Web/CSS/:focus-within)

import {useEffect, useRef} from 'react'
import './App.css'; // .shadow-host:focus-within {background-color: yellow;}

function ShadowExample() {const divRef = useRef(null)

  useEffect(() => {if(divRef?.current) {const { current} = divRef
      const shadow = current.attachShadow({mode: 'open'});
      shadow.innerHTML = '<input class="shadow-h1"/>';
      
    }
  }, [])

  return (
    <div>
      <p>Css Module</p>
      <div ref={divRef} className='shadow-host'></div>
    </div>
  );
}

export default ShadowExample;

问题

正因为 shadow dom 内的款式只会利用于外部,如果咱们在 shadow dom 外部用了相似于 antdModal这些创立于 document.body 下的弹窗或者其余组件时,无奈利用于 antd 的款式,须要把 antd 的款式放到上一层中。

优缺点

| 长处 | • 不须要引入额定的包,浏览器原生反对

• 严格隔离
毛病 • 在某些场景下可能呈现款式生效的问题,如上问题中的 shadow dom 内创立了全局的 Modal

浅析 QianKun 中的 CSS SandBox

下面咱们解说了一些实现款式隔离的根本计划,那作为一个比拟成熟的微前端框架 QianKun 中又是怎么实现款式隔离计划的呢,以下的源码解析是在 v2.6.3 的版本上钻研的,首先通过看文档能够发现

在 QianKun 中 CSS SandBox 有两种模式:

  • strictStyleIsolation——严格沙箱模式
  • experimentalStyleIsolation——实验性沙箱模式

strictStyleIsolation

须要留神的是该计划不是一个无脑的解决方案,开启后须要进行肯定的适配

上面咱们来具体介绍下该模式:

咱们设置 strictStyleIsolationtrue时,QianKun采纳的是 Shadow DOM 计划,外围就是为每个微利用包裹上一个 Shadow DOM 节点。接下来咱们看下是怎么实现的

先来个流程图咱们有个大抵的概念:

  • **registerMicroApps**:注册子利用,同时调用 single-spa 中的 registerApplication 进行注册
  • **loadApp**:加载子利用,初始化加载子利用的 Dom 构造,创立款式沙箱和 JS 沙箱等,同时返回不同阶段的生命周期
  • **createElement**:款式沙箱的具体实现,次要分为两种 strictStyleIsolationexperimentalStyleIsolation

registerMicroApps:注册子利用

export function registerMicroApps<T extends ObjectType>(apps: Array<RegistrableApp<T>>,lifeCycles?: FrameworkLifeCycles<T>,) {
...
    registerApplication({
      name,
      app: async () => {
        ...
        // 加载微利用的具体方法,裸露 bootstrap、mount、unmount 等生命周期以及一些其余配置信息
        const {mount, ...otherMicroAppConfigs} = (await loadApp({ name, props, ...appConfig}, frameworkConfiguration, lifeCycles)
        )();
                ...
      },
      // 子利用的激活条件
      activeWhen: activeRule
            ...
    });
  });
}

调用 single-spa 的 registerApplication 对利用进行注册,并且在利用激活的时候调用 app 的回调,其中最次要的是 loadApp 加载微利用的具体方法

一些参数的阐明:

  • apps:微利用的注册信息

  • lifeCycles:微利用的一些生命周期钩子

loadApp:加载子利用

function loadApp (app: LoadableApp<T>, configuration: FrameworkConfiguration = {},lifeCycles?: FrameworkLifeCycles<T>) {
...
/**
 * 将操作权交给主利用管制,返回后果波及 CSS SandBox 和 JS SandBox
 * template --template 的为 link 替换为 style 正文 script 的 HTML 模版
 * execScripts -- 脚本执行器,让指定的脚本 (scripts) 在规定的上下文环境中执行,只做理解临时不讲
 * assetPublicPath -- 动态资源地址,只做理解临时不讲
 */
const {template, execScripts, assetPublicPath} = await importEntry(entry, importEntryOpts);

// 给子利用包裹一层 div 后的子利用 html 模版, 是模版字符串的模式
const appContent = getDefaultTplWrapper(appInstanceId)(template);

let initialAppWrapperElement: HTMLElement | null = createElement(
    appContent,
        // 是否开启了严格模式
    strictStyleIsolation,
        // 是否开启实验性的款式隔离
    scopedCSS,
        // 依据利用名生成的惟一值,惟一则为 appName,不惟一则为 appName_[count]为具体数量,反复会 count++
    appInstanceId,
  );
...
// 上面还有一些生命周期的解决办法
}

Q1:到当初不晓得还有没有人记得咱们开启严格款式模式是须要做啥?

!!!把子利用的 Dom 构造放到 Shadow dom 中与主利用隔离,避免款式净化

Q2:那咱们咋拿到子利用的 Dom 构造呢?

没错就是通过 import-html-entry 库的 import-html-entry 办法,有趣味给看下对于 import-html-entry 解析

没错咱们拿到了 templateexecScriptsassetPublicPath,这里咱们不对后两个进行解说,聚焦到 template 上:

比照下子利用原来的 HTML 构造

能够发现咱们拿到的 templatelink标签变成 style 标签正文了 script 的 HTML 模版,其中就有咱们须要的子利用的 Dom 构造。

拿到当前 QianKun 里又在 template 上包裹了一层 Div 造成一个新的 HTML 构造的模版字符串,这是为什么呢?次要是为了在主利用中标识该节点下的内容为子利用,当然在前面咱们也须要它进行特地的解决,这个前面讲到的时候再说。因而咱们当初拿到的 appContent 长成这个样子:

这个 div 的 id 是惟一的哈!!!

那咱们当初是不是曾经做好了后期筹备,当初咱们须要进入最初一个步骤,把子利用的这个 Dom 构造挂载到一个 shadow dom 上,这就要用到 createElement 办法。

进入 createElement 办法前咱们先来看下目前的参数值:

  • appContent:包裹了一层 id 惟一的 div,具体如上所示
  • strictStyleIsolation:true
  • scopedCSS:false
  • appInstanceId:react16

createElement:增加 shadow dom

那咱们当初如何去创立一个 shadow dom,在后面对于 shadow dom 的解说中咱们晓得,创立一个 shadow dom 咱们须要两个货色:

一、挂载的 Dom 节点

二、须要增加到 shadow dom 的内容

那咱们从哪里去找呢,依据传进来的参数吧,咱们无疑是要对 appContent 进行解决了,回顾下 appContent 有什么,包裹了一层 div 的子利用的 HTML 模版是吧,自然而然的咱们就能够以里面的 div 为挂载的 dom 节点,拿子利用的 HTML 模版为须要增加到 shadow dom 的内容,即:

然而问题又来了,目前的 appContent 是模版字符串嘞,咱们咋办?这边 QianKun 的解决计划是:

这只是个大抵流程,上面让咱们跟着这样的思维看下代码里解决:

function createElement(appContent: string,strictStyleIsolation: boolean,scopedCSS: boolean,appInstanceId: string) {
...
const containerElement = document.createElement('div');
  containerElement.innerHTML = appContent;
  const appElement = containerElement.firstChild as HTMLElement;
    // 严格款式沙箱模式
  if (strictStyleIsolation) {if (!supportShadowDOM) {
      console.warn('[qiankun]: As current browser not support shadow dom, your strictStyleIsolation configuration will be ignored!',
      );
    } else {const { innerHTML} = appElement;
      appElement.innerHTML = '';
      let shadow: ShadowRoot;

            // 创立 shadow dom 节点
      if (appElement.attachShadow) {shadow = appElement.attachShadow({ mode: 'open'});
      } else {
        // 兼容低版本
        shadow = (appElement as any).createShadowRoot();}
      shadow.innerHTML = innerHTML;
    }
  }
...
// 此处省略了开启 experimentalStyleIsolation 的解决办法
...
return appElement;
}

这里有个很有意思的是:

appContent 以 innerHTML 变成 dom 构造后,HTML 模版中的 <html><head> 以及 <body> 会被去掉

最初咱们再来看下子利用挂载到主利用的 Dom 构造:

笔者在实际的过程中也遇到了一些问题:

1、微利用中应用相对路径引入图片呈现加载资源 404 的问题,这边笔者没有进行过多的尝试能够参考下官网的:https://qiankun.umijs.org/zh/faq# 为什么微利用加载的资源会 -404

2、还有一个问题就是 react 中动静关上 Modal 生效的问题,起因能够看下‣,大略看了下和 React 的事件机制无关,即便是设置弹窗默认开启,也会呈现之前下面提到的,款式失落的问题

experimentalStyleIsolation

咱们设置 experimentalStyleIsolationtrue时,QianKun采纳的是Runtime css transformer 动静加载 / 卸载样式表计划,为子利用的样式表减少一个非凡的选择器从而限定影响范畴,相似以下构造:

// 假如利用名是 react16
<style>
    .app-main {font-size: 14px;}
</style>

<style>
    div[data-qiankun="react16"] .app-main {font-size: 14px;}
<style>

先来通过流程图理解下大抵流程

createElement:给最外层减少 data-qiankun 属性,并且获取所有 style 标签

function createElement(appContent: string, strictStyleIsolation: boolean, scopedCSS: boolean,appInstanceId: string) {
...
if (scopedCSS) {
        // 给最外层设置 data-qiankun 的属性
    const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr);
    if (!attr) {appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId);
    }
        // 获取所有的 style 标签, 进行遍历
    const styleNodes = appElement.querySelectorAll('style') || [];
    forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {css.process(appElement!, stylesheetElement, appInstanceId);
    });
  }
...
}

export const QiankunCSSRewriteAttr = 'data-qiankun';

咱们来看下设置完属性后的属性后的 appElement

styleNodes

css.process 具体解决

/**
* 实例化 ScopedCSS
* 生成根元素属性选择器 [data-qiankun="利用名"] 前缀
*/
export const process = (
  appWrapper: HTMLElement,
  stylesheetElement: HTMLStyleElement | HTMLLinkElement,
  appName: string,
): void => {
  // 实例化 ScopedCSS
  if (!processor) {processor = new ScopedCSS();
  }
    ...
    // 一些空值的解决
  const mountDOM = appWrapper;
  if (!mountDOM) {return;}

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

  if (tag && stylesheetElement.tagName === 'STYLE') {// 生成前缀,根元素标签名[data-qiankun="利用名"]
    const prefix = `${tag}[${QiankunCSSRewriteAttr}="${appName}"]`;
    processor.process(stylesheetElement, prefix);
  }
};
  • prefix:
    div[data-qiankun="react16"]
  • stylesheetElement:

进入 processor.process 看看对它进行了什么操作

// 重写款式选择器以及对于空的 style 节点设置 MutationObserver 监听,起因可能存在动静减少款式的状况
process(styleNode: HTMLStyleElement, prefix: string = '') {
        // 当 style 标签有内容时进行操作
    if (styleNode.textContent !== '') {
            // styleNode.textContent 为 style 标签内的内容
      const textNode = document.createTextNode(styleNode.textContent || '');
            // swapNode 为创立的空的 style 标签
      this.swapNode.appendChild(textNode);
            // 获取样式表
      const sheet = this.swapNode.sheet as any;
            // 从样式表获取 cssRules 该值是规范的,把款式规定从伪数组转化成数组
      const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
            // 通过遍历和正则重写每个选择器的前缀
      const css = this.rewrite(rules, prefix);
            // 将解决后的重写后的 css 放入原来的 styleNode 中
      styleNode.textContent = css;
      // 清理工具人 swapNode
      this.swapNode.removeChild(textNode);
      return;
    }

        // 对空的款式节点进行监听,可能存在动静插入的问题
    const mutator = new MutationObserver((mutations) => {for (let i = 0; i < mutations.length; i += 1) {
                // mutation 为变更的每个记录 MutationRecord
        const mutation = mutations[i];

                // 判断该节点是否应解决过
        if (ScopedCSS.ModifiedTag in styleNode) {return;}

        if (mutation.type === 'childList') {
          const sheet = styleNode.sheet as any;
          const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
          const css = this.rewrite(rules, prefix);

          styleNode.textContent = css;
          // 减少解决节点的标识
          (styleNode as any)[ScopedCSS.ModifiedTag] = true;
        }
      }
    });

    // 监听以后的 style 标签,当 styleNode 为空的时候,以及变更的时候,比方引入的 antd 款式文件
    mutator.observe(styleNode, { childList: true});
  }

Q1:为什么在 style 标签有内容的时候应用 this.swapNode 这个工具人,而在监听的时候不应用?

还记得咱们是须要干什么吗?

改写 style 标签内的款式规定

因而这里就通过 style.sheet.cssRules 形式去获取 style 标签里的每一条规定进行重写,咱们来看下 sheet 样式表的数据结构

通过这个构造咱们其实下一步想要做的事件很分明了

就是重写每一条 cssRules 并且通过字符串拼接赋值给 style 标签

然而咱们得留神两点:

  • 选择器不同咱们的解决形式也不同
  • 对选择器的匹配规定的解决

让咱们看看 rewrite 具体进行了什么操作,这里次要分为两块

  • 一对选择器的类型进行判断
    CSSRule.type
private rewrite(rules: CSSRule[], prefix: string = '') {
    let css = '';

    rules.forEach((rule) => {switch (rule.type) {
                // 一般选择器类型
        case RuleType.STYLE:
          css += this.ruleStyle(rule as CSSStyleRule, prefix);
          break;
                // @media 选择器类型
        case RuleType.MEDIA:
          css += this.ruleMedia(rule as CSSMediaRule, prefix);
          break;
                // @supports 选择器类型
        case RuleType.SUPPORTS:
          css += this.ruleSupport(rule as CSSSupportsRule, prefix);
          break;
        default:
          css += `${rule.cssText}`;
          break;
      }
    });

    return css;
  }
  • 二是进行正则替换

非凡的

// 解决相似于 @media screen and (min-width: 900px) {}
private ruleMedia(rule: CSSMediaRule, prefix: string) {const css = this.rewrite(arrayify(rule.cssRules), prefix);
  return `@media ${rule.conditionText} {${css}}`;
}

// 解决相似于 @supports (display: grid) {}
private ruleSupport(rule: CSSSupportsRule, prefix: string) {const css = this.rewrite(arrayify(rule.cssRules), prefix);
  return `@supports ${rule.conditionText} {${css}}`;
}

一般的

// prefix 为 "div[data-qiankun="react16"]"
private ruleStyle(rule: CSSStyleRule, prefix: string) {
        // 根选择器,比方 body、html 以及:root
    const rootSelectorRE = /((?:[^\w\-.#]|^)(body|html|:root))/gm;
        // 根组合选择器,相似于 html body{...}
    const rootCombinationRE = /(html[^\w{[]+)/gm;

        // 获取选择器
    const selector = rule.selectorText.trim();

        // 获取款式文本,比方 "html {font-family: sans-serif; line-height: 1.15; text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }"
    let {cssText} = rule;
       // 对根选择器 (body、html、:root) 进行判断,替换成 prefix
    if (selector === 'html' || selector === 'body' || selector === ':root') {return cssText.replace(rootSelectorRE, prefix);
    }

    // 对于根组合选择器进行匹配
    if (rootCombinationRE.test(rule.selectorText)) {const siblingSelectorRE = /(html[^\w{]+)(\+|~)/gm;
            // 对于非标准的兄弟选择器转换时进行疏忽,置空解决
      if (!siblingSelectorRE.test(rule.selectorText)) {cssText = cssText.replace(rootCombinationRE, '');
      }
    }

    // 一般选择器匹配
    cssText = cssText.replace(/^[\s\S]+{/, (selectors) =>
            // selectors 为相似于.link{selectors.replace(/(^|,\n?)([^,]+)/g, (item, p, s) => {// 解决相似于 div,body,span { ...},含有根元素的
        if (rootSelectorRE.test(item)) {return item.replace(rootSelectorRE, (m) => {const whitePrevChars = [',', '('];
                        // 将其中的根元素替换为前缀保留, 或者(if (m && whitePrevChars.includes(m[0])) {return `${m[0]}${prefix}`;
            }
                        // 间接把根元素替换成前缀
            return prefix;
          });
        }

        return `${p}${prefix} ${s.replace(/^ */, '')}`;
      }),
    );

    return cssText;
  }

动静增加款式的思考🤔

那么通过 JS 动静增加的 stylelink 或者 script 标签是不是也须要运行在相应的 CSS 或者 JS 沙箱中呢,增加这些标签的常见办法无疑是 createElementappendChildinsertBefore,那其实咱们只有对他们设置监听就能够了

dynamicAppend就是用来解决下面的问题的,它裸露了两个办法

  • patchStrictSandbox:QianKun JS 沙箱模式的多例模式

patchStrictSandbox

export function patchStrictSandbox(
  appName: string,
    // 返回包裹子利用的那一块 Dom 构造
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  proxy: Window,
  mounting = true,
  scopedCSS = false,
  excludeAssetFilter?: CallableFunction,
){
  ...
let containerConfig = proxyAttachContainerConfigMap.get(proxy);
  if (!containerConfig) {
    containerConfig = {
      appName,
      proxy,
      appWrapperGetter,
      dynamicStyleSheetElements: [],
      strictGlobal: true,
      excludeAssetFilter,
      scopedCSS,
    };
        // 建设了代理对象和子利用配置信息 Map 关系
    proxyAttachContainerConfigMap.set(proxy, containerConfig);
  }

    // 重写 Document.prototype.createElement
  const unpatchDocumentCreate = patchDocumentCreateElement();

    // 重写 appendChild、insertBefore
  const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions((element) => elementAttachContainerConfigMap.has(element),
    (element) => elementAttachContainerConfigMap.get(element)!,
  );
  ...
}
  • 重写Document.prototype.createElement
  • 重写appendChildinsertBefore

patchDocumentCreateElement

function patchDocumentCreateElement() {
    // 记录 createElement 是否被重写
  const docCreateElementFnBeforeOverwrite = docCreatePatchedMap.get(document.createElement);

  if (!docCreateElementFnBeforeOverwrite) {
    const rawDocumentCreateElement = document.createElement;
        // 重写 Document.prototype.createElement
    Document.prototype.createElement = function createElement<K extends keyof HTMLElementTagNameMap>(
      this: Document,
      tagName: K,
      options?: ElementCreationOptions,
    ): HTMLElement {const element = rawDocumentCreateElement.call(this, tagName, options);
            // 判断创立的是否为 style、link 和 script 标签
      if (isHijackingTag(tagName)) {const { window: currentRunningSandboxProxy} = getCurrentRunningApp() || {};
        if (currentRunningSandboxProxy) {
                    // 获取子利用的配置信息
          const proxyContainerConfig = proxyAttachContainerConfigMap.get(currentRunningSandboxProxy);
          if (proxyContainerConfig) {
            // 建设新元素 element 和子利用配置的对应关系
            elementAttachContainerConfigMap.set(element, proxyContainerConfig);
          }
        }
      }

      return element;
    };

    if (document.hasOwnProperty('createElement')) {
            // 重写
      document.createElement = Document.prototype.createElement;
    }

    docCreatePatchedMap.set(Document.prototype.createElement, rawDocumentCreateElement);
  }
}

function isHijackingTag(tagName?: string) {
  return (tagName?.toUpperCase() === LINK_TAG_NAME ||
    tagName?.toUpperCase() === STYLE_TAG_NAME ||
    tagName?.toUpperCase() === SCRIPT_TAG_NAME);
}
  • 重写document.createElement
  • 建设新元素 element 和子利用配置的对应关系elementAttachContainerConfigMap

patchHTMLDynamicAppendPrototypeFunctions

export function patchHTMLDynamicAppendPrototypeFunctions(isInvokedByMicroApp: (element: HTMLElement) => boolean,
  containerConfigGetter: (element: HTMLElement) => ContainerConfig,
) {
  // 当 appendChild 和 insertBefore 没有被重写的时候
  if (
    HTMLHeadElement.prototype.appendChild === rawHeadAppendChild &&
    HTMLBodyElement.prototype.appendChild === rawBodyAppendChild &&
    HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore
  ) {
    HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadAppendChild,
      containerConfigGetter,
      isInvokedByMicroApp,
    }) as typeof rawHeadAppendChild;
    HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawBodyAppendChild,
      containerConfigGetter,
      isInvokedByMicroApp,
    }) as typeof rawBodyAppendChild;

    HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadInsertBefore as any,
      containerConfigGetter,
      isInvokedByMicroApp,
    }) as typeof rawHeadInsertBefore;
  }}
  • 当 appendChild、appendChild 和 insertBefore 没有被重写的时候进行重写

getOverwrittenAppendChildOrInsertBefore

function getOverwrittenAppendChildOrInsertBefore(opts: {rawDOMAppendOrInsertBefore: <T extends Node>(newChild: T, refChild?: Node | null) => T;
  isInvokedByMicroApp: (element: HTMLElement) => boolean;
  containerConfigGetter: (element: HTMLElement) => ContainerConfig;
}) {
  return function appendChildOrInsertBefore<T extends Node>(
    this: HTMLHeadElement | HTMLBodyElement,
    newChild: T,
    refChild: Node | null = null,
  ) {
    let element = newChild as any;
    const {rawDOMAppendOrInsertBefore, isInvokedByMicroApp, containerConfigGetter} = opts;
    // 当不是 style、link 或者是 script 标签的时候或者在元素的创立找不到对应的子利用配置信息时,走原生的办法
    if (!isHijackingTag(element.tagName) || !isInvokedByMicroApp(element)) {return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
    }

    if (element.tagName) {
      // 获取以后子利用的配置信息
      const containerConfig = containerConfigGetter(element);
      const {
        appName,
        appWrapperGetter,
        proxy,
        strictGlobal,
        dynamicStyleSheetElements,
        scopedCSS,
        excludeAssetFilter,
      } = containerConfig;

      switch (element.tagName) {
        case LINK_TAG_NAME:
        case STYLE_TAG_NAME: {
          let stylesheetElement: HTMLLinkElement | HTMLStyleElement = newChild as any;
          const {href} = stylesheetElement as HTMLLinkElement;
          // 配置项不须要被劫持的资源
          if (excludeAssetFilter && href && excludeAssetFilter(href)) {return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
          }

          // 挂载的 dom 构造,即子利用的 dom 构造
          const mountDOM = appWrapperGetter();

          // 如果开启了实验性的款式沙箱模式
          if (scopedCSS) {
            // exclude link elements like <link rel="icon" href="favicon.ico">
            const linkElementUsingStylesheet =
              element.tagName?.toUpperCase() === LINK_TAG_NAME &&
              (element as HTMLLinkElement).rel === 'stylesheet' &&
              (element as HTMLLinkElement).href;
            // 对于 link 标签进行款式资源下载,并进行款式的重写
            if (linkElementUsingStylesheet) {
              const fetch =
                typeof frameworkConfiguration.fetch === 'function'
                  ? frameworkConfiguration.fetch
                  : frameworkConfiguration.fetch?.fn;
              stylesheetElement = convertLinkAsStyle(
                element,
                (styleElement) => css.process(mountDOM, styleElement, appName),
                fetch,
              );
              dynamicLinkAttachedInlineStyleMap.set(element, stylesheetElement);
            } else {css.process(mountDOM, stylesheetElement, appName);
            }
          }

          // 重写当前的 style 标签
          dynamicStyleSheetElements.push(stylesheetElement);
          const referenceNode = mountDOM.contains(refChild) ? refChild : null;
          return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode);
        }
    ...
}
  • patchLooseSandbox:QianKun JS 沙箱模式的单例模式和快照模式下

export function patchLooseSandbox(
  appName: string,
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  proxy: Window,
  mounting = true,
  scopedCSS = false,
  excludeAssetFilter?: CallableFunction,
): Freer {let dynamicStyleSheetElements: Array<HTMLLinkElement | HTMLStyleElement> = [];

  const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
    // 判断以后微利用是否运行
        () => checkActivityFunctions(window.location).some((name) => name === appName),
    // 返回微利用的配置信息
        () => ({
      appName,
      appWrapperGetter,
      proxy,
      strictGlobal: false,
      scopedCSS,
      dynamicStyleSheetElements,
      excludeAssetFilter,
    }),
  );
}

因为是单例模式批改的还是全局的 window 去掉了对 document.createElement 的重写,不须要建设微利用和新建元素的一一对应

正文完
 0