introduction
This article mainly introduces some things about CSS Sandbox
, why introduce this? In our daily development, the style problem has always been a relatively time-consuming thing. On the one hand, we constantly adjust it according to the UI draft. On the other hand, as the project becomes larger and larger, it may be discovered in any development—eh, Why is my style not working, or how is it being overridden by another style. There could be many reasons:
- Irregular naming leads to duplication
- For simplicity, directly add the modification of the global style
- Unreasonable reuse of styles
- When multiple projects are merged, each sub-project has its own independent style and configuration, which may not exist in its own project, but after the merger, it affects each other and causes style pollution
- Introduction of third-party frameworks
- ...
And CSS Sandbox
is officially in order to isolate styles, thereby solving the problem of style pollution
Application scenarios
Through the above we understand the causes of style pollution, from which we can also summarize which scenarios we need to use CSS Sandbox
for style isolation?
- Parent-child and child-child applications in micro-frontend scenarios
- Style conflicts for large and complex projects
- Third-party frameworks and overrides for custom theme styles
- ...
Common Solutions
Now that we have talked about the causes and application scenarios of so many style pollution, how can we solve them? There are the following solutions, but the core of the solution is still the 使CSS选择器作用的Dom元素唯一
Tips: When we are in actual development, we can choose according to the actual situation of the project
CSS in JS
See if the name feels very advanced, the literal translation is to use JS to write CSS styles, rather than write them in a separate style file. E.g:
<p style='color:red'>css in js</p>
This is very different from our traditional development thinking. The traditional development principle is 关注点分离
, just like we often say not to write 行内样式
, 行内脚本
, , JS, CSS are written in the corresponding files.
About CSS in JS is not a new technology, its popularity mainly appears in the development of some web frameworks, such as: React, the jsx syntax it supports, allows us to write js, html and css in one file at the same time, and 组件
The idea of managing its own style, logic and component development is deeply rooted in the hearts of the people.
const style = {
color: 'red'
}
ReactDOM.render(
<p style={style}>
css in js
</h1>,
document.getElementById('main')
);
The style of each component is determined by its own style, and it does not depend on or affect the outside. From this point of view, the effect of style isolation is indeed achieved.
There are also many libraries about Css in js
, for example:
where styled-components will dynamically generate a selector
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>
);
}
Advantages and disadvantages
| Advantages | • No scoped style pollution issues (mainly by writing inline styles and generating unique CSS selectors)
• Reduce the accumulation of useless styles, delete the component to delete the corresponding style
• Easy reuse and refactoring by exporting defined style variables | |
---|---|
shortcoming | • Inline styles do not support pseudo-classes and selectors |
• The code is less readable and violates the principle of separation of concerns
• Will consume performance at runtime, dynamically generate CSS (we are still js when we write CSS)
• Can't combine some CSS preprocessors, can't precompile|
style conventions
Achieve unified development and maintenance through naming prefixes about applications, such as the naming method of BEM, which can standardize a component by naming blocks, elements, and modifiers
.dropdown-menu__item-button--disabled
Advantages and disadvantages
| Advantages | • Style isolation
• Strong semantics and high readability of components | |
---|---|
shortcoming | • The name is too long |
• Developer-dependent naming |
preprocessor
Many unique syntax formats can be handled by the CSS preprocessor, such as:
- Nestability
body {
with: 20px;
p {
color: red;
}
}
- parent selector
body {
with: 20px;
&:hover {
with: 30px;
}
}
- property inheritance
.dev {
width: 200px;
}
span {
.dev
}
Make CSS easier to read and maintain with these special syntaxes
Some common preprocessors on the market
- Sass
- Less
- Stylus
- PostCss
Advantages and disadvantages
| Advantages | • Better readability, easy to understand and maintain DOM structure
• Using nesting and other methods can also greatly solve the problem of style pollution | |
---|---|
shortcoming | • Need to add additional packages, with the help of related compilation tools |
Tips: Usually combined with a naming method similar to BEM, it can achieve the effect of improving development efficiency, enhancing readability and reuse
CSS Module
As the name implies, it is to modularize CSS. After compiling, it can avoid the problem of style pollution. However, it depends on Webpack to configure css-loader
and other packaging tools. The following is what I created in create-react-app
Running in the project, because it has been configured in webpack css-loader
, so the specific configuration is not shown in this article
index.ts file
import style from './style.module.css'
function App() {
return (
<div>
<p className={style.text}>Css Module</p>
</div>
);
}
style.module.css file
.text {
color: red;
}
// 等同于
:local(.text) {
color: blue;
}
// 还有一种全局模式,此时不会进行编译
:global(.text) {
color: blue;
}
The bundler will compile both style.text and text into unique values
Advantages and disadvantages
| Advantages | • Low learning cost, not dependent on manual constraints
• Basically 100% solution to style pollution problems
• Facilitate the reuse of modules | |
---|---|
shortcoming | • Can only be used at build time, depends on css-loader etc. |
• Poor readability, the hash value appears inconvenient to debug when debugging in the console|
Shadow DOM
It can attach a hidden and independent DOM to an element. When we wrap an element with Shadow DOM, its inner style will not affect the outer style, and the outer style will not affect its interior
// 创建一个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>
);
}
External styles cannot affect styles inside shadow dom
Let's take a look at the shadow dom internal style will affect the external style?
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>
);
}
But there are exceptions, except [: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;
question
正shadow dom
内的样式只会应用于内部,如果我们在shadow dom antd
的Modal
document.body
When the pop-up window or other components under document.body
cannot be applied to the style of antd
, the style of antd
needs to be placed in the upper layer.
Advantages and disadvantages
| Advantages | • No need to introduce additional packages, native browser support
• strict isolation | |
---|---|
shortcoming | • There may be a problem of style failure in some scenarios, such as the creation of a global Modal in the shadow dom in the above question |
Analysis of CSS SandBox in QianKun
Above we explained some basic schemes for implementing style isolation. As a relatively mature micro-front-end framework QianKun
, how to implement style isolation scheme, the following source code analysis is in v2.6.3
Researched on the version of the -- first, you can find it by looking at the documentation
There are two modes of CSS SandBox in QianKun:
-
strictStyleIsolation
sandbox mode -
experimentalStyleIsolation
Experimental sandbox mode
strictStyleIsolation
It should be noted that this solution is not a brainless solution, and certain adaptations are required after opening.
Let's introduce this mode in detail:
strictStyleIsolation
36c3c63fdbab912a2932cbd2d7a79c32---为true
时, QianKun
的是---6575a527b85b0add05c88101a8ebfbd0---方案,核心就是为每个微应用Shadow DOM
Shadow DOM node. Next, let's see how
Let's first have a flow chart and we have a general concept:
-
**registerMicroApps**
: Register the sub-application and callregisterApplication
in single-spa to register -
**loadApp**
: Load sub-applications, initialize the Dom structure for loading sub-applications, create style sandboxes and JS sandboxes, etc., and return the life cycle of different stages -
**createElement**
: The specific implementation of the style sandbox is mainly divided into two typesstrictStyleIsolation
andexperimentalStyleIsolation
registerMicroApps: register sub-apps
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
...
});
});
}
Call the registerApplication of single-spa to register the application, and call the callback of the app when the application is activated, the most important of which is loadApp
the specific method of loading the micro application
Description of some parameters:
-
apps
: Registration information of the micro application
-
lifeCycles
: some lifecycle hooks for microapps
loadApp: load sub-application
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: Until now, I don’t know if anyone remembers what we need to do to enable strict style mode?
! ! ! Put the Dom structure of the sub-application into the Shadow dom to isolate it from the main application to prevent style pollution
Q2: Then how do we get the Dom structure of the sub-application?
That's right, through the import-html-entry
library's import-html-entry
method, if you are interested in the analysis of import-html-entry
没错template
、 execScripts
96fde33d0289b2790c80e460f3c72896 assetPublicPath
,这里我们不对后两个进行讲解, template
上:
Compare the original HTML structure of the application
It can be found that the template
that we got is link
the label becomes style
the label is annotated script
the HTML version we need The Dom structure of the child application.
After getting it, QianKun wraps a layer of Div on the template
to form a template string with a new HTML structure. Why? The main purpose is to identify the content under the node as a sub-application in the main application. Of course, we also need it for special processing later, which will be discussed later. So what we get now appContent
looks like this:
The id of this div is unique! ! !
So are we ready now? Now we need to enter the last step to mount the Dom structure of the sub-application to a shadow dom, which requires the createElement
method.
Before entering the createElement
method, let's take a look at the current parameter values:
- appContent: wraps a div with a unique id, as shown above
- strictStyleIsolation:
true
- scopedCSS:
false
- appInstanceId:
react16
createElement: add shadow dom
So how do we create a shadow dom now? In the previous explanation about shadow dom, we know that to create a shadow dom we need two things:
1. Mounted Dom node
Second, the content that needs to be added to the shadow dom
Then where do we find it? According to the parameters passed in, we undoubtedly have to deal with appContent
, and review appContent
what is it, wrapping a layer of div The HTML template of the application is right? Naturally, we can use the outer div as the mounted dom node, and take the HTML template of the sub-application as the content that needs to be added to the shadow dom, namely:
But the problem comes again, the current appContent
is the template string, what should we do? Here QianKun's solution is:
This is just a general process. Let's follow this idea and look at the processing in the code:
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;
}
Here's an interesting one:
After appContent is transformed into a dom structure with innerHTML, the <html>
, <head>
and <body>
in the HTML template will be removed
Finally, let's look at the Dom structure of the sub-application mounted to the main application:
The author also encountered some problems in the process of practice:
1. There is a problem of loading resource 404 when using relative paths to import pictures in the micro-application. The author has not tried too much here. You can refer to the official one: https://qiankun.umijs.org/zh/faq#Why micro-applications are loaded The resource will -404
2. Another problem is that the dynamic opening of Modal in react fails. The reason can be seen ‣, and it is probably related to React's event mechanism. Even if the pop-up window is set to be enabled by default, the above mentioned will appear. style loss problem
experimentalStyleIsolation
experimentalStyleIsolation
c9948ee9a4815ad2b35e047e000e0ceb---为true
时, QianKun
Runtime css transformer
动态加载/卸载样式表方案,为子应用的样式表Add a special selector to limit the scope of influence, similar to the following structure:
// 假设应用名是 react16
<style>
.app-main {
font-size: 14px;
}
</style>
<style>
div[data-qiankun="react16"] .app-main {
font-size: 14px;
}
<style>
First, let's understand the general process through the flow chart
createElement: Add the data-qiankun attribute to the outermost layer and get all style tags
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';
Let's take a look at the appElement after the properties are set
styleNodes
css.process detailed processing
/**
* 实例化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:
Enter processor.process to see what's going on with it
// 重写样式选择器以及对于空的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: Why use this.swapNode
this tool when the style
tag has content, but not when listening?
Remember what we needed to do?
Rewrite style
style rules inside tags
So here we use style.sheet.cssRules
to get each rule in the style tag and rewrite it. Let's take a look at the data structure of sheet
style sheet
Through this structure, what we actually want to do next is very clear
It is to rewrite each cssRules
and assign it to the style
tag through string concatenation
But we have to pay attention to two things:
- Different selectors, we handle it differently
- Processing of matching rules for selectors
Let's see what exactly rewrite does, which is mainly divided into two parts
- Judging the type of a pair of selectors
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;
}
- The second is to perform regular replacement
special
// 处理类似于@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}}`;
}
normal
// 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;
}
Thinking of adding styles dynamically 🤔
那么通过JS 动态style
、 link
script
标签是不是也需要运行CSS
JS
In the sandbox, the common ways to add these tags are undoubtedly createElement
, appendChild
and insertBefore
, then we only need to monitor them span
dynamicAppend
is used to solve the above problem, it exposes two methods
- patchStrictSandbox: Multi-instance mode for QianKun JS sandbox mode
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)!,
);
...
}
- Rewrite
Document.prototype.createElement
- Rewrite
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
);
}
- Rewrite
document.createElement
- Establish the corresponding relationship between the new element element and the sub-application configuration
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;
}}
- Override when appendChild, appendChild and insertBefore are not overridden
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: Singleton mode and snapshot mode of QianKun JS sandbox mode
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,
}),
);
}
Because it is a singleton mode modification or a global window, the rewriting of document.createElement
is removed, and there is no need to establish a one-to-one correspondence between micro-applications and new elements
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。