前言:
qiankun 官网
qiankun(乾坤)微应用框架。可以让很多应用集成到一个项目里来。但集成时样式隔离是个很大的问题(坑)。官网也给出了一些解决方案。
虽然无界 完美的解决了样式隔离的问题(它的底层使用 iframe实现的),同样它也有其他缺点:
无界官网
qiankun也给了为什么不用 iframe的回答:
Why Not Iframe
为什么不用 iframe,这几乎是所有微前端方案第一个会被 challenge 的问题。但是大部分微前端方案又不约而同放弃了 iframe 方案,自然是有原因的,并不是为了 "炫技" 或者刻意追求 "特立独行"。
如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。
iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。
我们最后选了qiankun。
比如:strictStyleIsolation: true
这个是实验性得api
qiankun start strictStyleIsolation
再比如 ui库样式影响 比如 antd(主应用和子应用都用了这个库):
提供的方案,并不能完全解决样式冲突问题,因为 有的ui库并美有提供统一的加前缀功能。
策略:
1.子应用之间不用担心样式影响(乾坤给解决了)。
2.主子应用尽量别用相同的类名。
可以用less、scss 或者 styles modules(样式模块化)
上面的两种可以尽量做到互相不影响样式。但如果 都用了同一个 ui库可能就不适用了。如果 ui库提供了 加前缀的api那还好说(比如:antd)。没提供那就没太好的方法(比如 ag-grid 从子应用切换到主应用,主的ag-grid样式被影响了)。
我审查元素发现,虽然 切换了应用但是,子应用挂载的容器并没有移除,所以想到了动态移除link标签的方法。
ag-grid我们是用了当切换应用时,动态的移除掉 对应的 style标签,并且把对应的链接存起来,sessionStorage实现。切换到其他应用就移除掉 style。切换回来时动态追加上。
我们的代码如下仅供参考,给个另类的解决方案:
路由切换时需要调用此方法。
我们子应用都是 sub-开头
//处理 特殊 css 样式 问题, 例如 ag-grid
export const handlingCss = () => {
//如果是基座 css样式管理
if (!window.location?.hash.includes('sub-')) {
// 获取所有 link 元素
let links = document.getElementsByTagName('link');
// 创建一个数组,用于存储 sessionStorage 的 值 (pd 系统的 )
var hrefValues = JSON.parse(sessionStorage.getItem('pdCss'))?.length > 0 ? JSON.parse(sessionStorage.getItem('pdCss')) : [];
// 循环遍历每个 link 元素
for (let i = 0; i < links.length; i++) {
// 检查 href 属性是否包含指定部分
if (links[i].getAttribute('href').includes(pdDomain[REACT_APP_ENV]) && links[i].getAttribute('type') === "text/css") {
hrefValues.push(links[i].getAttribute('href'));
// 删除符合条件的 link 元素
if (links[i]?.parentNode && links[i]?.parentNode?.contains(links[i])) {
links[i]?.parentNode?.removeChild(links[i]);
}
}
}
var uniqueCssLinks = [];
var cssLinkSet = new Set();
hrefValues?.forEach(function (link) {
if (!cssLinkSet.has(link)) {
cssLinkSet.add(link);
uniqueCssLinks.push(link);
}
});
// 将数组转换为字符串并存储在 sessionStorage 中
sessionStorage.setItem('pdCss', JSON.stringify(uniqueCssLinks));
// 获取所有 <style> 元素
const styleElements = document.querySelectorAll('style');
let styleCrm = null
// 遍历每个 <style> 元素
styleElements?.forEach((styleElement, index) => {
// 获取 <style> 元素的文本内容
const styleContent = styleElement.textContent;
// 使用正则表达式提取注释代码
const comments = styleContent.match(/\/\*.*?\*\//g);
// 打印提取到的注释代码
comments?.forEach((item) => {
// 检查 href 属性是否包含指定部分
if (item.includes(crmDomain[REACT_APP_ENV])) {
styleCrm = item.replace('/*', '').replace('*/', '');
// 删除符合条件的 style 元素
// 在包含指定注释的情况下移除对应的 <style> 元素
if (styleElement?.parentNode && styleElement?.parentNode?.contains(styleElement)) {
styleElement?.parentNode?.removeChild(styleElement);
}
}
})
});
if (styleCrm) {
sessionStorage.setItem('crmStyle', styleCrm.trim());
}
}else{
if(window.location?.hash.includes('sub-pdProduct')){
// 从 sessionStorage 中获取存储的 href 值数组
let storedHrefs = JSON.parse(sessionStorage.getItem('pdCss'));
// 获取现有的 link 元素
const existingLinks = [...document.head.getElementsByTagName('link')];
// 根据获取到的 href 值数组动态创建 link 元素并添加到 head 中
storedHrefs?.forEach(function (href) {
// 检查是否已经存在相同 href 的 link 元素
const alreadyExists = existingLinks.some(link => link.href === href);
// 如果不存在,则创建并添加新的 link 元素
if (!alreadyExists) {
let link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = href;
document.head.appendChild(link);
}
});
}
}
};
之后我又发现 子应用挂载的容器不一样也能解决这个问题(可以尝试一下):
之前是所有子应用挂载到一个容器里#qiankun-imp-wrap,后面 发现挂载不同容器 也能避免 样式冲突。需要注意的是 需要在入口文件里加上两个div id分别为qiankun-imp-wrap、qiankun-imp-wrap1
const childApp = [{
name: 'sub-pdProduct',
entry: xxxx + "?version=" + localStorage.getItem("subPdVersion"),
container: '#qiankun-imp-wrap1', //之前是#qiankun-imp-wrap
activeRule: '/#/sub-pdProduct',
},
{
name: 'sub-multi-tabs-admin',
entry:xxx + "?version=" + localStorage.getItem("subPbVersion"),
container: '#qiankun-imp-wrap',
activeRule: '/#/sub-multi-tabs-admin',
}]
registerMicroApps(childApp);
建议的解决方案(终极):
我们是这样的,一开始搭建项目,并没有说,要集成很多应用。我们主应用都快开发完了,后面说想集成其他应用,类似一个工作台,用同一套登录,相同的菜单栏。后面 调研 想用qiankun,这时主应用的代码逻辑已经在了。并且项目着急,所以就直接在主应用里写了接入了很多应用。主应用用了antd,子应用也用了,要命的是版本还不一致,并且我们用自己的样式也覆盖了 antd的一些样式。
我们后期是准备这样的 主应用就只是用来接入子应用的,把现在主应用里的代码和页面 拿出去当成子应用。也就是说 主应用就用来当容器,里面可以有少量的逻辑。
主应用就相当于一个纯净的容器只在入口文件 接入子应用,处理一些通用逻辑。(比如:登录逻辑,路由逻辑)。
这样就会避免主子应用有样式冲突。因为qinkun很好的解决了子应用之间的样式隔离。
子应用更新,但访问主应用并没有及时更新。可解看:
qainkun 子应用更新,但是访问主应用时显示的还是旧的内容