前言
自从微前端框架micro-app开源后,很多小伙伴都非常感兴趣,问我是如何实现的,但这并不是几句话可以说明白的。为了讲清楚其中的原理,我会从零开始实现一个简易的微前端框架,它的核心功能包括:渲染、JS沙箱、样式隔离、数据通信。由于内容太多,会根据功能分成四篇文章进行讲解,这是系列文章的第三篇:样式隔离篇。
通过这些文章,你可以了解微前端框架的具体原理和实现方式,这在你以后使用微前端或者自己写一套微前端框架时会有很大的帮助。如果这篇文章对你有帮助,欢迎点赞留言。
相关推荐
- micro-app仓库地址
- simple-micro-app仓库地址
- 从零开始写一个微前端框架-渲染篇
- 从零开始写一个微前端框架-沙箱篇
- 从零开始写一个微前端框架-样式隔离篇
- 从零开始写一个微前端框架-数据通信篇
- micro-app介绍
开始
前两篇文章中,我们已经完成了微前端的渲染和JS沙箱功能,接下来实现微前端的样式隔离。
问题示例
我们先创建一个问题,验证样式冲突的存在。在基座应用和子应用上分别使用div元素插入一段文字,两个div元素使用相同的class名text-color
,分别在class中设置文字颜色,基座应用为red
,子应用为blue
。
由于子应用是后来执行的,它的样式覆盖了基座应用,产生了样式冲突。
样式隔离实现原理
要实现样式隔离必须对应用的css进行改造,因为基座应用无法控制,我们只能对子应用进行修改。
先看一下子应用被渲染后的元素构造:
子应用的所有元素都被插入到micro-app标签中,且micro-app标签具有唯一的name
值,所以通过添加属性选择器前缀micro-app[name=xxx]
可以让css样式在指定的micro-app内生效。
例如:
.test { height: 100px; }
添加前缀后变为:
micro-app[name=xxx] .test { height: 100px; }
这样.test
的样式只会影响到name为xxx的micro-app的元素。
渲染篇中我们将link标签引入的远程css文件转换为style标签,所以子应用只会存在style标签,实现样式隔离的方式就是在style标签的每一个CSS规则前面加上micro-app[name=xxx]
的前缀,让所有CSS规则都只能影响到指定元素内部。
通过style.textContent获取样式内容是最简单的,但textContent拿到的是所有css内容的字符串,这样无法针对单独规则进行处理,所以我们要通过另外一种方式:CSSRules
。
当style元素被插入到文档中时,浏览器会自动为style元素创建CSSStyleSheet样式表,一个 CSS 样式表包含了一组表示规则的 CSSRule 对象。每条 CSS 规则可以通过与之相关联的对象进行操作,这些规则被包含在 CSSRuleList 内,可以通过样式表的 cssRules 属性获取。
形式如图:
所以cssRules就是由单个CSS规则组成的列表,我们只需要遍历规则列表,并在每个规则的选择器前加上前缀micro-app[name=xxx]
,就可以将当前style样式的影响限制在micro-app元素内部。
代码实现
创建一个scopedcss.js
文件,样式隔离的核心代码都将放在这里。
我们上面提到过,style元素插入到文档后会创建css样式表,但有些style元素(比如动态创建的style)在执行样式隔离时还没插入到文档中,此时样式表还没生成。所以我们需要创建一个模版style元素,它用于处理这种特殊情况,模版style只作为格式化工具,不会对页面产生影响。
还有一种情况需要特殊处理:style元素被插入到文档中后再添加样式内容。这种情况常见于开发环境,通过style-loader
插件创建的style元素。对于这种情况可以通过MutationObserver监听style元素的变化,当style插入新的样式时再进行隔离处理。
具体实现如下:
// /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