引言
本篇文章主要介紹的是關於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 內部用了類似於antd
的Modal
這些建立於document.body
下的彈窗或者其他元件時,無法應用於antd
的樣式,需要把antd
的樣式放到上一層中。
優缺點
| 優點 | • 不需要引入額外的包,瀏覽器原生支援
• 嚴格隔離 | |
---|---|
缺點 | • 在某些場景下可能出現樣式失效的問題,如上問題中的 shadow dom 內建立了全域性的 Modal |
淺析 QianKun 中的 CSS SandBox
上面我們講解了一些實現樣式隔離的基本方案,那作為一個比較成熟的微前端框架QianKun
中又是怎麼實現樣式隔離方案的呢,以下的原始碼解析是在v2.6.3
的版本上研究的,首先通過看文件可以發現
在 QianKun 中 CSS SandBox 有兩種模式:
strictStyleIsolation
——嚴格沙箱模式experimentalStyleIsolation
——實驗性沙箱模式
strictStyleIsolation
需要注意的是該方案不是一個無腦的解決方案,開啟後需要進行一定的適配
下面我們來詳細介紹下該模式:
我們設定strictStyleIsolation
為true
時,QianKun
採用的是Shadow DOM
方案,核心就是為每個微應用包裹上一個 Shadow DOM 節點。接下來我們看下是怎麼實現的
先來個流程圖我們有個大致的概念:
**registerMicroApps**
:註冊子應用,同時呼叫 single-spa 中的registerApplication
進行註冊**loadApp**
:載入子應用,初始化載入子應用的 Dom 結構,建立樣式沙箱和 JS 沙箱等,同時返回不同階段的生命週期**createElement**
:樣式沙箱的具體實現,主要分為兩種strictStyleIsolation
和experimentalStyleIsolation
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
、execScripts
和assetPublicPath
,這裡我們不對後兩個進行講解,聚焦到template
上:
對比下子應用原來的 HTML 結構
可以發現我們拿到的template
是link
標籤變成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
我們設定experimentalStyleIsolation
為true
時,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
、appendChild
和insertBefore
,那其實我們只要對他們設定監聽就可以了
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
的重寫,不需要建立微應用和新建元素的一一對應