手写微前端micro-app-CSS隔离
子应用的CSS可能会对基座应用或者其他子应用产生的影响
首先现在我们把react页面放入到vue2的页面大家也能看到一些问题了,在react中的index.css中对body的一些css样式,已经影响了基座应用的css。
为了看的更明显,我自己写一下,在基座应用(vue2)中声明一个css样式,比如:
.text-color{color:red
}
做实验的时候,如果是vue2项目。别把这个样式写到了带scoped的style样式标签中了,这种本身就是隔离的,我们这里所谓的隔离,主要是针对全局样式
由于我们前面的处理只是将link标签转换为了style标签,因此在react项目中做处理的话,最好将样式写在静态css文件中,比如之前讲index.css文件放到了index.html中
如果在子应用中,也有同名的全局样式
.text-color{color:yellow
}
那么,你会发现,子应用的这个样式,对基座应用中,同样使用这个样式的标签起了作用,我们就是要隔绝这种情况
比如上面的.text-color这个样式,在子应用中,我们就可能会加上
micro-app[name=app] .text-color
2、代码实现
首先创建新的文件scopedcss.js,创建scopedCSS函数,来进行css过滤替换处理。从上面的演示可以分析出,这个函数我们至少需要两个参数
1、style节点对象,通过这个对象获取textContent与sheet的值
2、子应用app的名字,因为我们需要这个名字来组装前缀
/*** 进行样式隔离* @param {HTMLStyleElement} styleElement style元素* @param {string} appName 应用名称*/
export default function scopedCSS (styleElement, appName) {// 前缀const prefix = `micro-app[name=${appName}]`console.log(styleElement.sheet);console.log(styleElement.textContent);
}
你会发现打印了textContent的内容,但是sheet内容却为空,原因是css没有挂载到页面之前,样式表还没生成。是获取不了sheet的。而且有时候style元素(比如动态创建的style)在执行样式隔离时还没插入到文档中,此时样式表还没生成。也会出现这种情况
不能获取sheet内容的话,我们仅仅凭借textContent字符串的内容,去做处理工作量太大,也不好区分css中的内容
所以我们做一个取巧的办法,声明一个临时的style模板,用来填充css,用完之后删除
let templateStyle // 模版sytle
/*** 进行样式隔离* @param {HTMLStyleElement} styleElement style元素* @param {string} appName 应用名称*/
export default function scopedCSS (styleElement, appName) {// 前缀const prefix = `micro-app[name=${appName}]`// console.log(styleElement.sheet);// console.log(styleElement.textContent);// 初始化时创建模版标签if (!templateStyle) {templateStyle = document.createElement('style')document.body.appendChild(templateStyle)// 设置样式表无效,防止对应用造成影响templateStyle.sheet.disabled = true}if (styleElement.textContent) {// 将元素的内容赋值给模版元素templateStyle.textContent = styleElement.textContent// 获取临时模板中的sheetconsole.log(templateStyle.sheet)}
}
我们需要的是将**@media内部的.text加上前缀,而这些,sheet中的cssRules**已经帮我们划分了类型了,类型有数十种,我们只处理STYLE_RULE
、MEDIA_RULE
、SUPPORTS_RULE
三种类型
-
type为1的,是普通的样式
STYLE_RULE
-
type为4的,是media类型,
MEDIA_RULE
-
type为12的,为supports类型
SUPPORTS_RULE
也就是说,我们需要根据类型不一样,分开进行处理。其实分开处理无非也就是media和supports类型,再递归执行一下
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// console.log(templateStyle.sheet)// 格式化规则,并将格式化后的规则赋值给style元素styleElement.textContent = scopedRule(Array.from(templateStyle.sheet.cssRules || []), prefix)// 清空模版style内容templateStyle.textContent = ''}
}/*** 依次处理每个cssRule* @param rules cssRule* @param prefix 前缀*/
function scopedRule (rules, prefix) {let result = ''// 遍历rules,处理每一条规则for (const rule of rules) {switch (rule.type) {case 1: // STYLE_RULEresult += scopedStyleRule(rule, prefix)breakcase 4: // MEDIA_RULEresult += scopedPackRule(rule, prefix, 'media')breakcase 12: // SUPPORTS_RULEresult += scopedPackRule(rule, prefix, 'supports')breakdefault:result += rule.cssTextbreak}}return result
}// 处理media 和 supports
function scopedPackRule (rule, prefix, packName) {// 递归执行scopedRule,处理media 和 supports内部规则const result = scopedRule(Array.from(rule.cssRules), prefix)return `@${packName} ${rule.conditionText} {${result}}`
}
递归之后,最终其实还是使用**scopedStyleRule()**函数进行处理。这个函数难度最大,因为要写难度的很大的正则表达式,太复杂了,我也不会,找了一下micro-app的源码
/*** 修改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} *`)}// 匹配顶层选择器,如 body,htmlconst 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*/, '')}`})})
}
效果