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 子应用更新,但是访问主应用时显示的还是旧的内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

崽崽的谷雨

漫漫前端路,摸爬滚打

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值