preface

Since the micro front-end framework micro-App became open source, many friends are very interested and ask me how to achieve it, but it is not a few words can be understood. To illustrate how this works, I’m going to implement a simple micro front-end framework from scratch. Its core features include rendering, JS sandbox, style isolation, and data communication. This is the third in a series, Style Isolation, which is divided into four articles based on functionality.

Through these articles, you will learn how micro front end frameworks work and how they are implemented, which will be of great help if you use them later or write your own. If this post helped you, feel free to like it and leave a comment.

Related to recommend

  • Micro-app Warehouse address
  • Simple-micro-app Warehouse address
  • Write a micro front end frame – render from scratch
  • Write a micro front end frame – sandbox from scratch
  • Write a micro front end frame – style isolation section from scratch
  • Write a micro front end framework from scratch – data communication article
  • Micro – app is introduced

start

In the last two articles, we have completed the rendering and JS sandbox of the micro front end, and now we implement the style isolation of the micro front end.

Problem of the sample

Let’s first create a problem to verify the existence of style conflicts. Insert text using a div element in the base application and in the child application. The two div elements use the same class name, text-color, and set the text color in the class, red for the base application and blue for the child application.

Since the child application is executed later, its style overrides the base application, resulting in style conflicts.

How style isolation is implemented

To achieve style isolation, you have to make changes to the APPLICATION’s CSS, because the base application is out of control and you can only make changes to the child application.

Take a look at the rendered element structure applied all at once:

All elements of the child application are inserted into the micro-app tag, and the micro-app tag has a unique name value, so you can make the CSS style take effect in the specified micro-app by adding the attribute selector prefix micro-app[name= XXX].

For example:.test {height: 100px; }

[name= XXX]. Test {height: 100px; }

Thus, the. Test style will only affect the elements of the micro-app whose name is XXX.

In the render, we convert the remote CSS file introduced by the link tag to the style tag, so only the style tag will exist in the child application. The way to achieve style isolation is to prefix the micro-app[name= XXX] before each CSS rule in the style tag. Let all CSS rules only affect the inside of a specified element.

TextContent is the easiest way to get style content, but textContent is a string of all CSS content, so it can’t be processed for individual rules, so we’ll go the other way: CSSRules.

When a style element is inserted into a document, the browser automatically creates a CSSStyleSheet for the style element. A CSS stylesheet contains a set of CSSRule objects that represent rules. Each CSS rule can be manipulated by the object associated with it. These rules are contained in the CSSRuleList and can be obtained through the cssRules property of the stylesheet.

The form is as follows:

Therefore, cssRules is a list of single CSS rules. We can limit the effect of the current style style within the micro-app element by traversing the list of rules and prefixed the picker of each rule with micro-app[name= XXX].

Code implementation

Create a scopedcss.js file where the core code for style isolation will be placed.

As mentioned above, a CSS stylesheet is created when a style element is inserted into the document, but some style elements (such as dynamically created styles) are not inserted into the document at the time of style isolation and the stylesheet is not yet generated. So we need to create a template style element that handles this particular case. The template style is only used as a formatting tool and does not affect the page.

There is one other case that requires special treatment: adding style content after the style element has been inserted into the document. This is often the case in development environments, with style elements created through the style-Loader plug-in. In this case, you can use a MutationObserver to listen for changes to the style element and then isolate it when style inserts a new style.

The specific implementation is as follows:

// /src/scopedcss.js

let templateStyle / / template sytle

/** * Make style isolation *@param {HTMLStyleElement} StyleElement Style element@param {string} AppName Application name */
export default function scopedCSS (styleElement, appName) {
  / / prefix
  const prefix = `micro-app[name=${appName}] `

  // Create template tags when initialization
  if(! templateStyle) { templateStyle =document.createElement('style')
    document.body.appendChild(templateStyle)
    // Set the stylesheet to invalid to prevent the application from being affected
    templateStyle.sheet.disabled = true
  }

  if (styleElement.textContent) {
    // Assign the content of the element to the template element
    templateStyle.textContent = styleElement.textContent
    // Format the rule and assign the formatted rule to the style element
    styleElement.textContent = scopedRule(Array.from(templateStyle.sheet?.cssRules ?? []), prefix)
    // Clear the template style content
    templateStyle.textContent = ' '
  } else {
    // Listen for a style element that dynamically adds content
    const observer = new MutationObserver(function () {
      // Disconnect the listener
      observer.disconnect()
      // Format the rule and assign the formatted rule to the style element
      styleElement.textContent = scopedRule(Array.from(styleElement.sheet?.cssRules ?? []), prefix)
    })

    // Listen for changes in the content of the style element
    observer.observe(styleElement, { childList: true}}})Copy the code

The scopedRule method mainly determines and processes cssrule-type. There are dozens of cssrule-type types. We only process STYLE_RULE, MEDIA_RULE, and SUPPORTS_RULE types. 1, 4, and 12. Other types are not processed.

// /src/scopedcss.js

/** * process each cssRule * in turn@param rules cssRule
 * @param The prefix prefix * /
 function scopedRule (rules, prefix) {
  let result = ' '
  // Loop through rules to process each rule
  for (const rule of rules) {
    switch (rule.type) {
      case 1: // STYLE_RULE
        result += scopedStyleRule(rule, prefix)
        break
      case 4: // MEDIA_RULE
        result += scopedPackRule(rule, prefix, 'media')
        break
      case 12: // SUPPORTS_RULE
        result += scopedPackRule(rule, prefix, 'supports')
        break
      default:
        result += rule.cssText
        break}}return result
}
Copy the code

The media and Supports types are further processed in the scopedPackRule method because they contain subroutine, and we need to recursively process their subroutine. Such as:

@media screen and (max-width: 300px) {
  .test {
    background-color:lightblue; }}Copy the code

Needs to be converted to:

@media screen and (max-width: 300px) {
  micro-app[name=xxx] .test {
    background-color:lightblue; }}Copy the code

The handling is simple: get a list of their sublines and recursively execute the method scopedRule.

// /src/scopedcss.js

// Handle media and supports
function scopedPackRule (rule, prefix, packName) {
  // recursively execute scopedRule, handling media and supports internal rules
  const result = scopedRule(Array.from(rule.cssRules), prefix)
  return ` @${packName} ${rule.conditionText} {${result}} `
}
Copy the code

Finally, implement the scopedStyleRule method, where specific CSS rules are modified. You can modify a rule by querying the selector of each rule through regular matches and adding a prefix to the selection.

// /src/scopedcss.js

/** * Modify the CSS rule by adding the prefix *@param {CSSRule} Rule CSS rule *@param {string} The prefix prefix * /
function scopedStyleRule (rule, prefix) {
  // Get the selection and content of the CSS rule object
  const { selectorText, cssText } = rule

  // Handle the top level selectors, such as body, HTML are converted to micro-app[name= XXX]
  if (/^((html[\s>~,]+body)|(html|body|:root))$/.test(selectorText)) {
    return cssText.replace(/^((html[\s>~,]+body)|(html|body|:root))/, prefix)
  } else if (selectorText === The '*') {
    // Replace selector * with micro-app[name= XXX] *
    return cssText.replace(The '*'.`${prefix}* `)}const builtInRootSelectorRE = /(^|\s+)((html[\s>~]+body)|(html|body|:root))(? =[\s>~]+|$)/

  // Match the query selector
  return cssText.replace(/^[\s\S]+{/.(selectors) = > {
    return selectors.replace(/(^|,)([^,]+)/g.(all, $1, $2) = > {
      // If there is a top-level selector, it needs to be handled separately
      if (builtInRootSelectorRE.test($2)) {
        / / body [xx] name = | body. Xx | body# xx, etc all don't need to transform
        return all.replace(builtInRootSelectorRE, prefix)
      }
      // Prefixes the selector
      return `The ${$1} ${prefix} The ${$2.replace(/^\s*/.' ')}`})})}Copy the code

use

Now that style isolation is almost complete, how do you use it?

In the render, we have two things to do with style elements, one is to recurse the HTML string into a DOM structure, and the other is to convert the link element into a style element. So we need to call the scopedCSS method in both places, passing in the style element as an argument.

// /src/source.js

/** * recursively process each child element *@param Parent Parent element *@param App Application instance */
 function extractSourceDom(parent, app) {...for (const dom of children) {
    if (dom instanceof HTMLLinkElement) {
      ...
    } else if (dom instanceof HTMLStyleElement) {
      // Perform style isolation
+      scopedCSS(dom, app.name)
    } else if (dom instanceofHTMLScriptElement) { ... }}}/** * Get link remote resource *@param App Application instance *@param microAppHead micro-app-head
 * @param HtmlDom HTML DOM structure */
export function fetchLinksFromHtml (app, microAppHead, htmlDom) {...Promise.all(fetchLinkPromise).then((res) = > {
    for (let i = 0; i < res.length; i++) {
      const code = res[i]
      // Get the CSS resource and insert the style element into the micro-app-head
      const link2Style = document.createElement('style') link2Style.textContent = code + scopedCSS(link2Style, app.name) ... }... }).catch((e) = > {
    console.error('Error loading CSS', e)
  })
}
Copy the code

validation

After the above steps, the style isolation feature is in effect, but we need to verify it.

Refresh the page, print the style sheet for the applied style element, and see that all the rule selectors have been added before themmicro-app[name=app]The prefix.

At this point, the text color in the base application becomes red, and the child application is blue. The style conflict problem is resolved, and the style isolation takes effect 🎉.

conclusion

As you can see above, style isolation is not complicated to implement, but it does have limitations. The current solution can only isolate the styles of the child application. The style of the base application can still affect the child application, which is not as complete as iframe and shadowDom, so the best solution is to use a tool like cssModule or negotiate the style prefixes between the teams to deal with the problem at the source.