手写微前端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
// 获取临时模板中的sheet
console.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_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
}
// 处理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,html
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*/, '')}`
})
})
}
效果