引言
本篇文章次要介绍的是对于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 外部用了相似于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
的重写,不须要建设微利用和新建元素的一一对应