6

Preface

Since the micro-front-end framework micro-app open sourced, many small partners are very interested and ask me how to achieve it, but this is not a few words to explain. In order to clarify the principle, I will implement a simple micro front-end framework from scratch. Its core functions include: rendering, JS sandbox, style isolation, and data communication. Because there is too much content, it will be divided into four articles for explanation according to the function. This is the third article in the series: the style isolation article.

Through these articles, you can understand the specific principles and implementation methods of the micro front-end framework, which will be of great help when you use the micro front-end or write a set of the micro front-end framework yourself. If this article is helpful to you, please like and leave a comment.

related suggestion

Start

In the first two articles, we have completed the rendering of the micro front end and the JS sandbox function, and then we will implement the style isolation of the micro front end.

Examples of questions

Let's create a question first to verify the existence of style conflicts. Use the div element to insert a paragraph of text on the base application and the sub application respectively. The two div elements use the same class name text-color . Set the text color in the class respectively. The base application is red and the sub application is blue .

Since the sub-application is executed later, its style covers the base application, resulting in style conflicts.

The realization principle of style isolation

To achieve style isolation, the application's css must be modified, because the base application cannot be controlled, we can only modify the sub-application.

Let's take a look at the element structure after the sub-application is rendered:

All elements of the sub-application are inserted into the micro-app tag, and the micro-app tag has a unique name , so by adding the attribute selector prefix micro-app[name=xxx] css style can be made effective in the specified micro-app.

E.g:
.test { height: 100px; }

After adding the prefix, it becomes:
micro-app[name=xxx] .test { height: 100px; }

In this .test , the style of 06113919a6c8fc will only affect the element of the micro-app whose name is xxx.

In the rendering article, we convert the remote css file introduced by the link tag into a style tag, so there will only be a style tag in the sub-application. The way to achieve style isolation is to add a prefix of micro-app[name=xxx] The rules can only affect the inside of the specified element.

It is the easiest to get style content through style.textContent, but textContent gets all the strings of css content, which cannot be processed for individual rules, so we have to use another method: CSSRules .

When the style element is inserted into the document, the browser will automatically create a CSSStyleSheet style sheet for the style element. A CSS style sheet contains a set of CSSRule objects representing rules. Each CSS rule can be operated by the objects associated with it. These rules are contained in the CSSRuleList and can be obtained through the cssRules property of the style sheet.

The form is shown in the figure:

So cssRules is a list of single CSS rules. We only need to traverse the list of rules and add the prefix micro-app[name=xxx] front of the selector of each rule to limit the influence of the current style to the inside of the micro-app element.

Code

Create a scopedcss.js file, the core code of style isolation will be placed here.

As we mentioned above, the CSS style sheet will be created after inserting the style element into the document, but some style elements (such as dynamically created style) have not been inserted into the document when style isolation is performed, and the style sheet has not been generated yet. So we need to create a template style element, which is used to handle this special situation. The template style is only used as a formatting tool and will not affect the page.

There is another situation that requires special treatment: style content is added after the style element is inserted into the document. This situation is common in the development environment, style-loader plug-in. In this case, you can MutationObserver , and perform isolation processing when style inserts a new style.

The specific implementation is as follows:

// /src/scopedcss.js

let templateStyle // 模版sytle

/**
 * 进行样式隔离
 * @param {HTMLStyleElement} styleElement style元素
 * @param {string} appName 应用名称
 */
export default function scopedCSS (styleElement, appName) {
  // 前缀
  const prefix = `micro-app[name=${appName}]`

  // 初始化时创建模版标签
  if (!templateStyle) {
    templateStyle = document.createElement('style')
    document.body.appendChild(templateStyle)
    // 设置样式表无效,防止对应用造成影响
    templateStyle.sheet.disabled = true
  }

  if (styleElement.textContent) {
    // 将元素的内容赋值给模版元素
    templateStyle.textContent = styleElement.textContent
    // 格式化规则,并将格式化后的规则赋值给style元素
    styleElement.textContent = scopedRule(Array.from(templateStyle.sheet?.cssRules ?? []), prefix)
    // 清空模版style内容
    templateStyle.textContent = ''
  } else {
    // 监听动态添加内容的style元素
    const observer = new MutationObserver(function () {
      // 断开监听
      observer.disconnect()
      // 格式化规则,并将格式化后的规则赋值给style元素
      styleElement.textContent = scopedRule(Array.from(styleElement.sheet?.cssRules ?? []), prefix)
    })

    // 监听style元素的内容是否变化
    observer.observe(styleElement, { childList: true })
  }
}

scopedRule method mainly CSSRule.type determination and processing, CSSRule.type type dozens, we only deal STYLE_RULE , MEDIA_RULE , SUPPORTS_RULE three types, corresponding to their respective type values: 4, and 12 , Other types are not processed.

// /src/scopedcss.js

/**
 * 依次处理每个cssRule
 * @param rules cssRule
 * @param prefix 前缀
 */
 function scopedRule (rules, prefix) {
  let result = ''
  // 遍历rules,处理每一条规则
  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
}

In the scopedPackRule method, two types of media and supports are further processed. Because they contain sub-rules, we need to process their sub-rules recursively.
like:

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

Need to be converted to:

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

The processing method is also very simple: get the list of their sub-rules and execute the method scopedRule recursively.

// /src/scopedcss.js

// 处理media 和 supports
function scopedPackRule (rule, prefix, packName) {
  // 递归执行scopedRule,处理media 和 supports内部规则
  const result = scopedRule(Array.from(rule.cssRules), prefix)
  return `@${packName} ${rule.conditionText} {${result}}`
}

Finally, the scopedStyleRule method is implemented, and the specific CSS rules are modified here. The way to modify the rules is mainly through regular matching, query the selector of each rule, and add a prefix before the selection.

// /src/scopedcss.js

/**
 * 修改CSS规则,添加前缀
 * @param {CSSRule} rule css规则
 * @param {string} prefix 前缀
 */
function scopedStyleRule (rule, prefix) {
  // 获取CSS规则对象的选择和内容
  const { selectorText, cssText } = rule

  // 处理顶层选择器,如 body,html 都转换为 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 === '*') {
    // 选择器 * 替换为 micro-app[name=xxx] *
    return cssText.replace('*', `${prefix} *`)
  }

  const builtInRootSelectorRE = /(^|\s+)((html[\s>~]+body)|(html|body|:root))(?=[\s>~]+|$)/

  // 匹配查询选择器
  return cssText.replace(/^[\s\S]+{/, (selectors) => {
    return selectors.replace(/(^|,)([^,]+)/g, (all, $1, $2) => {
      // 如果含有顶层选择器,需要单独处理
      if (builtInRootSelectorRE.test($2)) {
        // body[name=xx]|body.xx|body#xx 等都不需要转换
        return all.replace(builtInRootSelectorRE, prefix)
      }
      // 在选择器前加上前缀
      return `${$1} ${prefix} ${$2.replace(/^\s*/, '')}`
    })
  })
}

use

At this point, the function of style isolation is basically completed, how to use it next?

In the rendering article , we have two aspects related to the processing of style elements, one is the recursive loop after html string is converted to DOM structure, and the other is to convert link element into style element. So we need to call the scopedCSS method in these two places, and pass in the style element as a parameter.

// /src/source.js

/**
 * 递归处理每一个子元素
 * @param parent 父元素
 * @param app 应用实例
 */
 function extractSourceDom(parent, app) {
  ...
  for (const dom of children) {
    if (dom instanceof HTMLLinkElement) {
      ...
    } else if (dom instanceof HTMLStyleElement) {
      // 执行样式隔离
+      scopedCSS(dom, app.name)
    } else if (dom instanceof HTMLScriptElement) {
      ...
    }
  }
}

/**
 * 获取link远程资源
 * @param app 应用实例
 * @param microAppHead micro-app-head
 * @param htmlDom html DOM结构
 */
export function fetchLinksFromHtml (app, microAppHead, htmlDom) {
  ...
  Promise.all(fetchLinkPromise).then((res) => {
    for (let i = 0; i < res.length; i++) {
      const code = res[i]
      // 拿到css资源后放入style元素并插入到micro-app-head中
      const link2Style = document.createElement('style')
      link2Style.textContent = code
+      scopedCSS(link2Style, app.name)
      ...
    }

    ...
  }).catch((e) => {
    console.error('加载css出错', e)
  })
}

verify

After completing the above steps, the style isolation function will take effect, but we need to verify it in detail.

Refresh the page and print the style sheet of the style element of the sub-application. You can see that all the rule selectors have been prefixed micro-app[name=app]

At this time, the text color in the base application changes to red, and the sub-application is blue. The style conflict problem is solved, and the style isolation takes effect🎉.

Concluding remarks

As you can see from the above, style isolation is not complicated to implement, but it also has limitations. The current solution can only isolate the style of the sub-application, and the style of the base application can still affect the sub-application. This is not as complete as iframe and shadowDom do, so the best solution is to use tools such as cssModule or between teams Negotiate the style prefix and solve the problem from the source.


cangdu
1.1k 声望281 粉丝

hurry up