接着上面两篇文章,继续学习微前端,本章主要内容如下:
- 微应用的样式隔离
首先,为什么需要做样式隔离?
因为微应用的话,是使用html entry加web components来实现的,所以说,这种方式实现的微应用并不具有iframe那种天然的样式隔离特性,
基于前两篇的分析,我们可以看出,我们是将html entry中的 资源(style,link,script)抽离出来,然后再挂载到micro-app-head中的,由于微应用
的样式挂载时机是出于基座应用后面的,所以如果基座应用与微应用具有相同的选择器,则微应用会覆盖掉基座应用的选择器。
借用一张图片来说明:
如何解决这个问题?
既然我们可以拿到资源的代码,那么就在获取代码之后进行处理,思路是,将对应的样式代码,进行正则匹配替换,统一加上前缀,
const prefix = `micro-app[name=${appName}]`;
这样的话,每个微应用由于appName不同,也就不会相互影响了。
具体实现是采用cssRules+递归来处理不同的cssRule来实现,代码如下:
let templateStyle; /** * 进行样式隔离 * @param {HTMLStyleElement} * @param {string} */ 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; styleElement.textContent = scopedRule( Array.from(templateStyle.sheet?.cssRules ?? []), prefix ); // 清空模板style内容; templateStyle.textContent = ""; } else { const observe = new MutationObserver(function () { // 断开监听 observe.disconnect(); styleElement.textContent = scopedRule( Array.from(templateStyle.sheet?.cssRule ?? []), prefix ); }); observe.observe(styleElement, { childList: true }); } } function scopedRule(rules, prefix) { let result = ""; for (const rule of rules) { switch (rule.type) { case 1: result += scopedStyleRule(rule, prefix); break; case 4: result += scopedPackRule(rule, prefix, "media"); break; case 12: result += scopedPackRule(rule, prefix, "supports"); default: result += rule.cssText; break; } } return result; } function scopedPackRule(rule, prefix, packName) { const result = scopedRule(Array.from(rule.cssRules), prefix); return `@${packName} ${rule.conditionText}{ ${result} }`; } function scopedStyleRule(rule, prefix) { 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 === "*") { return cssText.replace("*", `${prefix} *`); } const builtInRootSelectorRE = /(^|\s+)((html[\s>~]+body)|(html|body|:root))(?=[\s>~]+|$)/; return cssText.replace(/^[\s\S]+{/, (selector) => { return selector.replace(/(^|,)([^,]+)/g, (all, $1, $2) => { if (builtInRootSelectorRE.test($2)) { return all.replace(builtInRootSelectorRE, prefix); } return `${$1} ${prefix} ${$2.replace(/^\s*/, "")}`; }); }); }
然后在获取到资源的时候进行调用就可以了,举个例子:
function fetchLinksFromHtml(app, microAppHead, htmlDom) { const linkEntries = Array.from(app.source.links.entries()); // 通过fetch请求所有css资源 const fetchLinkPromise = []; for (const [url] of linkEntries) { fetchLinkPromise.push(fetchSource(url)); } 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; console.log("link2style", link2Style.textContent); scopedCSS(link2Style, app.name); microAppHead.appendChild(link2Style); // 将代码放入缓存,再次渲染时可以从缓存中获取 linkEntries[i][1].code = code; } // 处理完成后执行onLoad方法 app.onLoad(htmlDom); }) .catch((e) => { console.error("加载css出错", e); }); }