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 @
Rule
CSS 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