CSS SandBox應用場景及常見問題

  發布時間:2022-06-22 16:29:05   作者:袋鼠云數棧前端   我要評論
本篇文章主要介紹的是關于CSS Sandbox的一些事情,主要包括應用場景分析及場景問題的解決方法,需要的朋友跟隨小編一起看看吧

引言

本篇文章主要介紹的是關于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 會動態生成一個選擇器

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 解析

沒錯我們拿到了template、execScriptsassetPublicPath,這里我們不對后兩個進行講解,聚焦到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 動態添加的style、link或者script標簽是不是也需要運行在相應的CSS或者JS沙箱中呢,添加這些標簽的常見方法無疑是createElement、appendChildinsertBefore,那其實我們只要對他們設置監聽就可以了

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
  • 重寫appendChild、insertBefore

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的重寫,不需要建立微應用和新建元素的一一對應

到此這篇關于CSS SandBox應用場景及常見問題的文章就介紹到這了,更多相關CSS SandBox內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關文章,希望大家以后多多支持腳本之家!

相關文章

  • CSS SandBox應用場景及常見問題

    本篇文章主要介紹的是關于CSS Sandbox的一些事情,主要包括應用場景分析及場景問題的解決方法,需要的朋友跟隨小編一起看看吧
    2022-06-22
  • 使用CSS自定義屬性實現骨架屏效果

    這篇文章主要介紹了使用CSS自定義屬性實現骨架屏效果,在網上可以看到骨架屏的使用已經非常廣泛,Facebook,Google,Slack等公司都在使用,本文通過示例代碼給大家介紹的非
    2022-06-20
  • css中有哪些方式可以隱藏頁面元素及區別

    在平常的樣式排版中,我們經常遇到將某個模塊隱藏的場景,本文主要介紹了css中有哪些方式可以隱藏頁面元素及區別,具有一定的參考價值,感興趣的可以了解一下
    2022-06-14
  • css如何把元素固定在容器底部的四種方式

    本文主要介紹了css如何把元素固定在容器底部的四種方式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起
    2022-06-14
  • CSS控制繼承中的height能變為可繼承嗎

    這篇文章主要介紹了CSS控制繼承中的height能變為可繼承嗎,我們知道,CSS 屬性有可繼承和不可繼承之分, height 和 width 就是CSS 中典型的不可繼承屬性,文章圍繞主題展開
    2022-06-06
  • 基于CSS制作創意端午節專屬加載特效

    本文給大家分享的是一個css創意特效端午加載動畫,想法是讓粽葉,糯米,紅棗繞圓旋轉,然后聚集起來融合后變成一個可愛的小粽子的效果,對css加載特效實現代碼感興趣的朋友
    2022-05-31
  • 聊聊CSS粘性定位sticky案例解析

    這篇文章主要介紹了關于CSS粘性定位sticky,粘性定位可以被認為是相對定位和固定定位的混合,它主要用在對 scroll 事件的監聽上,對CSS粘性定位sticky相關知識感興趣的朋友
    2022-05-31
  • 使用CSS實現黑白格背景效果

    在頁面上,有時會需要展示一些透明背景的圖片,為了展示其透明的背景,通常會像PS一樣,使用黑白相間的格子組成背景,從而告訴用戶,這是一張透明的圖片,本文給大家介紹使
    2022-05-30
  • CSS文本陰影 text-shadow 懸停效果詳解

    這篇文章給大家介紹CSS 文本陰影 text-shadow 懸停效果,本文將專注于使用 CSS text-shadow 屬性來實現有趣的鼠標懸停效果,但是實際上并不會為這些文本制作任何文本陰影效
    2022-05-23
  • 左邊固定寬右邊自適應的6種方法

    本文主要介紹了左邊固定寬右邊自適應的6種方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習
    2022-05-11

最新評論

美丽人妻被按摩中出中文字幕