iframe实现企业级微前端方案

5,213 阅读6分钟

我正在参与掘金创作者训练营第5期,点击了解活动详情

前言

都2022年了,还在使用iframe实现微前端是不是已经out了。确实,目前的微前端已经不同以往的解决方案匮乏,社区已经有许多成熟的微前端框架,诸如大家耳熟能详的single-spa,站在巨人肩膀上的qiankunwebpack5带来的模块联邦,脱离了single-spa拥抱WebComponentmicro-app,有很多的选择,后续会出关于这些方案的调研与选择的文章,今天我们先聊聊iframe实现微前端。

回归到问题,明明知道iframe实现微前端有许多缺点,也有更好的选择,为什么在2022年还是依然选择它做为微前端的解决方案。对于框架的选型还是比较巧妙的,并不是框架技术先进,做的好,所以我们必须要选择它,而是对于当前,什么样的框架能够解决我的问题,满足我的需求,所以我选择它。在团队还未有成熟的微前端方案以及时间很赶的情况下,又需要集成不同团队的项目到同一个系统时,优先使用iframe快速实现一套方案,后续在经过一段时间的调研,实践,迭代,这是我的解决思路。

iframe实现微前端

不管是iframe,还是其他的框架,都有其优点和缺点,在使用过程中总会遇到一些难点,接下来就聊聊iframe的优缺点,以及怎么去解决iframe实现微前端存在的问题。

为什么iframe可以实现微前端

微前端实现的是一个系统集成了多个子应用,这时候就需要考虑到JScss的互相影响问题,所以微前端框架很重要的一步就是实现样式隔离、js 隔离。而对于iframe来讲,它具有先天优势,其最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决,这是iframe可以作为微前端方案的原因。

iframe实现微前端的缺点和解决方案

iframe的优点我们已经了解了,原生支持硬隔离,但有句话叫成也萧何,败也萧何。由于隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题,这也成为了它的缺点。接下来聊聊它实现微前端的缺点和对应的解决方案,对于其缺点,这篇文章Why Not Iframe总结的挺好的。

一、url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。

遇到这种问题,可能有些人的第一反应是监听事件addEventListener('hashchange')或者addEventListener('popstate'),这样子是不科学的,会导致问题变得很复杂。现在我们的项目基本上都是spa,都会跟路由相关联,我们只需要做一个路由的url和iframesrc之间的映射就可以了,路由数据结构如下:

const routes = {
    path: "/system/order",
    name: "order",
    meta: {
      src: "http://xxx.com",
      name: "xxx",
      type: "iframe",
      keepAlive: true,
    },
}

二、UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示。

对于这个问题,解决起来确实比较麻烦,需要实现子应用与基座之间的通讯,将其实现交由基座来实现,这样子基座的耦合性较为严重。对于iframe的通信等会在讲,现在分析一下这个弹窗问题,我们现在实现的大多数都是类后台管理系统,类似下面这样的页面结构,弹窗显示在子应用区域完全符合我们的需求,只有一些特殊情况,才需要做些处理,总体开发成本来讲,还是可以接受的。

image.png

三、全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。

cookie 共享

子应用需要和基座共享cookie的话,条件还是比较苛刻的,需要主域名一致,我们是采用nginx代理的。

iframe 通信

iframe实现通信是很重要的一环,子应用和主应用和子应用都需要,每个项目都需要使用到的,建议可以独立封装成一个npm包,当然,如果使用webpack5,也可以直接使用模块联邦,来实现共享,对于通信,还是需要规定好规则,这样子比较好维护,接下来是通讯部分代码的具体实现。

目录结构

image.png

/src/main-app.js

import packageJSON from "../package.json";
import MessageType from "./message-type";

const { version, name } = packageJSON;

/**
 * 主应用,
 */
class MainApp {
  constructor() {
    this.registerEvents();
  }

  // 注册事件
  registerEvents() {
    window.addEventListener("message", (e) => {
      try {
        const { type, data } = e.data;
        const arg = { data, originEvent: e };
        if (type === MessageType.CHECK_COOKIE) {
          app.checkCookie(arg);
        }
      } catch (err) {
        console.error("主应用接收到消息失败", { info: { version, name } }, err);
      }
    });
  }
}

let app = null;
const start = ({ onCheckCookie }) => {
  app = new MainApp();
  app.checkCookie = onCheckCookie;
};

export default {
  start,
};

/src/message-type.js

const MESSAGE_TYPE = {
  CHECK_COOKIE: "CHECK_COOKIE", // 验证 cookie
};
export default MESSAGE_TYPE;

/src/micro-app.js

import packageJSON from "../package.json";
import MessageType from "./message-type";

const { version, name } = packageJSON;

let _targetOrigin = "*";

const setup = ({ targetOrigin }) => {
  _targetOrigin = targetOrigin;
};

// 通知事件
const notify = (type, data) => {
  top.postMessage(
    {
      type,
      data,
      info: {
        version,
        name,
      },
    },
    _targetOrigin
  );
};

// 验证 cookie 是否过期
const checkCookie = (data) => {
  notify(MessageType.CHECK_COOKIE, data);
};

// 是否 iframe
const isIframe = () => {
  return window.top !== window;
};

export default {
  setup,
  notify,
  checkCookie,
  isIframe,
};

/index.js

export { default as MainApp } from './src/main-app';
export { default as MicroApp } from './src/micro-app';

对于发布npm包,可以查看我之前发布的这边文章发布团队脚手架,对于发布的具体细节,以及可能遇到的问题,都讲的蛮清楚的,这里假设我已经发布了一个名为@LBINGXINj/iframe的npm包。

在主引用使用MainApp做事件监听


// main.js

import { MainApp } from "@LBINGXINj/iframe";

MainApp.start({
  // 监听事件
  onCheckCookie(res) {
    // 打印输出子应用传递过来的数据
    console.log(res)
    // to do something
  },
});

在子应用MicroApp注册microApp

// main.js

import { MicroApp } from '@LBINGXINj/iframe'
// iframe源,为了安全
const targetOrigin = '*';
MicroApp.setup({ targetOrigin: targetOrigin })
Vue.prototype.microApp = MicroApp

这样子就可以直接在任何页面做事件发布,实现数据通信了。

// any.vue

this.microApp.checkCookie({
    name: 'xxx'
})

四、慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

每一次进入都是页面重新加载,用户体验比较差,有能力的话,需要对于子应用做一定的优化,比如实现静态资源压缩,异步加载,分包,gz压缩,提供更好的服务器带宽,让子应用的加载速度更快,用户体验会好一些,同时也可以对于iframe做页面缓存,你可能会想用直接使用路由的keep-alive,想的很美好,现实很骨感哈,这样子是不行的,但是我们可以使用v-show或者display:nonedisplay: block来实现页面的缓存。

小结

经过一步步的分析,从为什么选择iframe实现微前端,iframe实现微前端的优劣势,以及怎么解决其所带来的问题,相信你对于是否选择它作为微前端方案,有了很清晰的认识,有问题欢迎评论区讨论,共同成长。