preface

Recently, I used Qiankun to build the framework for the company’s new background project. When I looked at the document, I found that the parameters of the application startup method start included such parameters:

sandbox - `boolean` | `{ strictStyleIsolation? : boolean, experimentalStyleIsolation? : boolean }` // Optional, whether to enable sandbox. Default is' true '
Copy the code

Combination of website, strictStyleIsolation said the style of the strict isolation, is actually using shadowDom will each application package, and the style of the experimentalStyleIsolation is for all mounted in front of the selector is added on the container, Take a look at the sample on the official website to make it easy to understand:

// Assume the application name is react16.app-main {
  font-size: 14px;
}
div[data-qiankun-react16] .app-main {
  font-size: 14px;
}
Copy the code

My curiosity drove me to explore how qiankun realized it. Here is how qiankun realized it.

Qiankun implementation

Get all the style tags and process each tag

If style isolation is enabled, the Qiankun will fetch all the style tags in the sub-application and perform a wave of traversal. The key logic in the css.js file is CSS.

// loader.js
var styleNodes = appElement.querySelectorAll('style') | | []; _forEach(styleNodes,function (stylesheetElement) {
  css.process(appElement, stylesheetElement, appName);
});

// css.js
export var process = function process(appWrapper, stylesheetElement, appName) {
  // ...
	// If it is an external style sheet, give a hint, do not handle this
  if (stylesheetElement.tagName === 'LINK') {
    console.warn('Feature: sandbox.experimentalStyleIsolation is not support for link element yet.');
  }

  // ...

  var tag = (mountDOM.tagName || ' ').toLowerCase();
	// Align when the style is inline for further processing
  if (tag && stylesheetElement.tagName === 'STYLE') {
    var prefix = "".concat(tag, "[").concat(QiankunCSSRewriteAttr, "= \" ").concat(appName, "\]" "); processor.process(stylesheetElement, prefix); }};Copy the code

Handling the Style node

The preceding function evaluates the incoming style element. If it is a style tag, it will call processor.process to add the prefix.

function process(styleNode) {
  var _this = this;

  // Gets the second argument of the function as a prefix
  var prefix = arguments.length > 1 && arguments[1]! = =undefined ? arguments[1] : ' ';

  var _a;

  // Operate when the style tag contains content
  if(styleNode.textContent ! = =' ') {
    var textNode = document.createTextNode(styleNode.textContent || ' ');
    // swapNode is an empty style created earlier. It acts as a toolman and will empty the contents of the style when finished
    this.swapNode.appendChild(textNode);
    // Retrieve the stylesheet object CSSStyleSheet from the styleElement. sheet API
    var sheet = this.swapNode.sheet;
		// Get all the style rules from the rules or cssRules field of CSSStyleSheet, and convert this pseudo-array into an array
    var rules = arrayify((_a = sheet === null || sheet === void 0 ? void 0: sheet.cssRules) ! = =null&& _a ! = =void 0 ? _a : []);
    
    // Add a prefix to the selector of each CSS rule to return the modified CSS
    var css = this.rewrite(rules, prefix); // eslint-disable-next-line no-param-reassign
		// Place the processed CSS in the style element passed in
    styleNode.textContent = css; // cleanup
		// The toolman completed his mission and cleaned up after using it
    this.swapNode.removeChild(textNode);
    return; }}Copy the code

Use styleElement. sheet to get all the CSS rule code inside the tag and then process it, as you’ll see later. This part of the code is executed on the premise that the style tag is passed to the beginning of the content, when the content of the style tag is dynamically passed to the end of the code:

function process(styleNode) {
  var _this = this;

  // Gets the second argument of the function as a prefix
  var prefix = arguments.length > 1 && arguments[1]! = =undefined ? arguments[1] : ' ';

  var _a;

  var mutator = new MutationObserver(function (mutations) {
    var _a;

    for (var i = 0; i < mutations.length; i += 1) {
      var mutation = mutations[i];

      // scopedcss. ModifiedTag is the Symbol object set previously. The value 'Symbol(style-modified-Qiankun)' should be used to trigger the mutaionObserver repeatedly after internal node changes
      
      if (ScopedCSS.ModifiedTag in styleNode) {
        return;
      }

      if (mutation.type === 'childList') {
        // The following code is the same as the first half, but without the participation of the toolman, why 🤔?
        var _sheet = styleNode.sheet;

        var _rules = arrayify((_a = _sheet === null || _sheet === void 0 ? void 0: _sheet.cssRules) ! = =null&& _a ! = =void 0 ? _a : []);

        var _css = _this.rewrite(_rules, prefix); // eslint-disable-next-line no-param-reassign
        
        styleNode.textContent = _css; // eslint-disable-next-line no-param-reassign
			
        styleNode[ScopedCSS.ModifiedTag] = true; }}});// There is no cleanup function because the listener is removed after the node is deleted
  / / website address: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/disconnect
	// Listen for the current style tag
  mutator.observe(styleNode, {
    childList: true
  });
}
Copy the code

This part of the code is mainly used when the style is initially empty, to prevent the style code from being added later, to use MutaionObserver to listen for the empty style tag, and to perform the CSS selector prefix when the node changes.

Process each style in the style node

The rewrite function iterates over all styles and determines what type of selector is currently available based on each style’s CSSStyleRule. Type (normal, @media, @supports selector). The @media and @supports selectors are recursive uses of rewrite handling internal nodes, mainly ruleStyle functions handling normal styles:

function ruleStyle(rule, prefix) {
  var rootSelectorRE = / ((? :[^\w\-.#]|^)(body|html|:root))/gm;
  var rootCombinationRE = /(html[^\w{[]+)/gm;
  var selector = rule.selectorText.trim();
  var cssText = rule.cssText; 
  
  // The following two judgments are for root selectors (body, HTML, :root). The processing logic will replace the root selector with prefix, which is the selector of the current child application mount container
  if (selector === 'html' || selector === 'body' || selector === ':root') {
    return cssText.replace(rootSelectorRE, prefix);
  } 
  // If the selector starts with HTML and has descendant elements
  if (rootCombinationRE.test(rule.selectorText)) {
    // If it is not standard, such as HTML + body, the root selector will not be processed, otherwise the HTML will be removed
    var siblingSelectorRE = /(html[^\w{]+)(\+|~)/gm;
    if(! siblingSelectorRE.test(rule.selectorText)) { cssText = cssText.replace(rootCombinationRE,' ');
    }
  } 

  cssText = cssText.replace(/^[\s\S]+{/.function (selectors) {
    // Starts with a string or, and ends before the next comma. The first argument is the entire string matched
    // The second argument is empty or, the third argument is, after the next comma or the end of the content
    return selectors.replace(/(^|,\n?) ([^,]+)/g.function (item, p, s) {
      // Handle group selectors with root selectors such as div,body,span {... }
      if (rootSelectorRE.test(item)) {
        return item.replace(rootSelectorRE, function (m) {
          // Handle cases like body, HTML, and *:not(:root) by prefixing the root selector directly
          var whitePrevChars = [', '.'('];

          if (m && whitePrevChars.includes(m[0]) {return "".concat(m[0]).concat(prefix);
          }

          return prefix;
        });
      }
      // Prefixes the selector and removes all Spaces before the previous selector
      return "".concat(p).concat(prefix, "").concat(s.replace(/ ^ * /.' '));
    });
  });
  return cssText;
}
Copy the code

The main logic is to use regular expressions to replace root selectors such as HTML, body, and :root with prefixes passed in (essentially the CSS selectors of the child application mount point container), prefix the regular selectors with the unique prefix of their mount container, and then return the processed CSS. In this way, the style of the current child does not affect other children and parents.

summary

Qiankun processing experimentalStyleIsolation options within the main logic is not complicated: Walk through all the style tags, get the selectors and CSS text for all the styles in each tag (using styleElement. sheet to get the details of each style), and process it, replacing the root selectors with prefixes. Normal selectors are prefixed directly.

Use postCSS to process the CSS

The principle of Qiankun is to request related resources of sub-applications after the main application is loaded and mount the sub-applications to a mount point of its own. Based on this mechanism, dom or strings can only be processed at runtime. If you need to prefix all CSS selectors in your application during development (for example, adding a plug-in to someone else’s page to prevent your own plug-in from affecting the original style of the page), you can use postCSS to generate THE AST of CSS to implement related operations on CSS.

Overview of the CSS AST

Just as Babel generates an AST for JS, PostCSS generates an AST for CSS.

  • Root: the Root node of the AST, representing each CSS file
  • AtRule: The Rule in which each selector starts with @
  • RuleCSS selectors and declarations together make up a CSS rule, for exampleinput, button {font-size: 20px; }The curly braces are declarations
  • Declaration: The curly braces that follow the selector in each Rule
  • Comment: a Comment that can exist in the selector, the @selector parameter, or the value of the CSS style key-value pair. The Comment in the node is saved in the RAWS property of the node

Nodes have properties and apis, including parent, Type, first, text, RAws, Clone (), replaceWith(newNode), and walkRules(rule). For details, see the official documentation

Here are a few that will be used below:

  • Parent: indicates the parent node of the current node

  • Type: indicates the node type

  • Clone: Each of the above methods is used to copy the nodes

  • ReplaceWith: Also available for each node, replacing the current node with the incoming one

  • WalkRules: Traverses each Rule node in the AST

Write a PostCSS plugin that prefixes selectors

If you are using the PostCSS plugin only in the Node environment, you can write:

const postcss = require('postcss')

const plugin = () = > {
  return {
    postcssPlugin: 'to-red'./ / must have
    Rule (rule) { // Traverses the Rule node of each CSS file
      console.log(rule.toString())
    },
    Declaration (decl) { // Iterate over the Declaration node of each CSS file
      console.log(decl.toString())
      decl.prop === 'color' && (decl.value = 'red')
    }
  }
}
plugin.postcss = true / / must have

postcss([plugin]).process('a { color: black; font-size: 100px; } ').then(res= > {
  console.log(res.toString()) // Output the converted result
})

// Output: 'a {color: red; font-size: 100px; } '
Copy the code

To write postCSS plugins that can be added to webpack.config.js, you need to use the postcss.plugin method to prefix each CSS selector:

module.exports = postcss.plugin('postcss-add-css-prefix'.function(opts = {}) {
  const {
    prefix = ' '
  } = opts

  The first parameter is the AST of each CSS file, and the second parameter is to obtain information about the conversion result (including information about the current CSS file).
  function plugin(css, result) {
    if(! prefix)return; // If prefix is not passed, the following logic is not executed
    css.walkRules(rule= > { // Traverses all rule nodes in the current AST
      const {
        selector
      } = rule
      // Prefix is added only when the node is a direct child of the AST root node
      // The root selector does not add a prefix, and does not add a prefix itself
      // added a flag to prevent repeated execution of the logic after node update into an infinite loop
      if (rule.parent.type === 'root' && !(selector.includes(':root') || selector.includes('body') || selector.includes('html') || selector.includes(prefix)) && ! rule.flag) { rule.flag =true
        const clone = rule.clone()
        clone.selector = `${prefix} ${selector}`
        rule.replaceWith(clone)
      }
    })
  }

  return plugin
})
Copy the code

Plugin takes two arguments, the first is the plugin name and the second is the callback function. This callback function takes the arguments passed in the postCSS plugin configuration and returns a function. The returned function plugin takes two arguments: The first is the AST after each CSS file has been parsed, and the second is the result of the AST transformation. Plugin function uses walkRules to process all Rule nodes of ast (walkDecls handles all declaration nodes and Walk handles all types of nodes, refer to the official document). After the selector is modified from the Rule node, the modified node is replaced with the previous node. In addition, you can obtain the current CSS file information according to the second parameter of plugin (result.opts.file field), and add a different prefix to the selectors in each CSS file.

Finally, register the plugin you just wrote in the configuration of postcss.config.js or webpack.config.js (for example, postcss.config.js) :

// postcss.config.js
const addCssPrefix = require('./addCssPrefix')

module.exports = {
  plugins: [
    addCssPrefix({
      prefix: 'body'}})]Copy the code