qiankun(乾坤)解决父子应用样式的影响和策略
前言:
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(主应用和子应用都用了这个库):
qiankun 如何确保主应用跟微应用之间的样式隔离
提供的方案,并不能完全解决样式冲突问题,因为 有的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 子应用更新,但是访问主应用时显示的还是旧的内容