1

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元素唯一

file

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;
}

file

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

file

 // 创建一个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>
  );
}

file

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>
  );
}

file

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;

file

question

shadow dom内的样式只会应用于内部,如果我们在shadow dom antdModal 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

file

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:

file

  • **registerMicroApps** : Register the sub-application and call registerApplication 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 types strictStyleIsolation and experimentalStyleIsolation

file

file

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

file

  • lifeCycles : some lifecycle hooks for microapps

file

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

file

没错templateexecScripts 96fde33d0289b2790c80e460f3c72896 assetPublicPath ,这里我们不对后两个进行讲解, template上:

Compare the original HTML structure of the application

file

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:

file

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:

file

But the problem comes again, the current appContent is the template string, what should we do? Here QianKun's solution is:

file

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

file

Finally, let's look at the Dom structure of the sub-application mounted to the main application:

file

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

file

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

file

styleNodes

file

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:

file

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

file

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

file

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

 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 动态stylelink 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

file

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

file

 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


袋鼠云数栈UED
277 声望34 粉丝

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。