基于iframe优雅实现全新的微前端方案

微前端是什么

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。

简单来说,就是利用一系列工具和技术,将各个团队的UI页面 组装成用户可以连贯的应用程序。

微前端是最近几年火起来的概念,iframe是早期实现微前端的理想方案,而现在有了其它的方案,比如qianduan框架,single-spa,以及webpack5带来的联邦模块方案。但是每一个方案都有其优缺点,感兴趣的可以去实践一下。

iframe全新的微前端方案

iframe是一个天然的微前端方案,但受限于跨域的严格限制而无法很好的应用,本文介绍一种基于iframe优雅实现全新的微前端方案,继承iframe的优点,补足 iframe 的缺点,让 iframe 焕发新生。

前端开发中我们对 iframe 已经非常熟悉了,那么 iframe 的作用是什么?可以归纳如下:

在之前使用iframe的时候,是在页面的使用中引入另外一个页面进行渲染,所以它的基本功能是:在一个web应用中独立的运行另一个web应用

iframe的使用优点

  • 非常简单,使用没有任何心智负担
  • 隔离完美,无论是 js、css、dom 都完全隔离开来
  • 多应用激活,页面上可以摆放多个 iframe 来组合业务

iframe的使用缺点

  • 路由状态丢失,刷新一下,iframe 的 url 状态就丢失了
  • dom 割裂严重,弹窗只能在 iframe 内部展示,无法覆盖全局
  • 通信非常困难,只能通过 postmessage 传递序列化的消息

能否打造一个完美的 iframe ,保留所有的优点的同时,解决掉所有的缺点呢?

本文以vue2为例,搭建一个左侧导航与顶部导航的二级导航的iframe框架项目。

实现步骤

1、先创建一个基座项目,项目只有导航框架,没有页面,不需要路由。

所有功能都在App.vue实现

2、直接在App.vue里写结构
<div class="el-container">
    <div class="el-menu">左侧一级导航</div>
    <div class="el-main">
        <div class="el-header">右侧顶部二级导航</div>
        <div class="el-aside" id="iframeBox">iframe的容器</div>
    </div>
</div>
3、渲染导航数据

这里是data数据部分

data() {
    return {
      index1: 0, //一级导航当前下标
      index2: 0, //二级导航当前下标
      forWard: true, //是否记录路由
      isOpen: false, //是否打开弹窗
      menuTree: [
        {
          id: "1",
          name: "menu1",
          order: 10,
          subMenu: [
            {
              id: "11",
              name: "mneu1_sub1",
              order: 11,
              subMenu: null,
              text: "二级菜单11",
              url: "http://127.0.0.1:5500/js/alert.html",
            },
          ],
          text: "一级菜单1",
          url: "",
        },
      ],
    };
4、添加iframe标签

当点击导航时,触发该事件,动态添加iframe标签

//显示iframe
replaceUrlFun() {
  const bigNode = document.getElementById("iframeBox");
  const contentIframe = document.getElementById("contentIframe");
  if (contentIframe) {
    contentIframe.remove();
  }
  const { index1, index2, menuTree } = this;
  const url1 = menuTree[index1].url;
  const url2 = menuTree[index1].subMenu?.[index2]?.url || "";
  let url = url2 || url1;
  const iframeCon = document.createElement("iframe");
  bigNode.appendChild(iframeCon);
  iframeCon.setAttribute("class", "iframe");
  iframeCon.setAttribute("id", "contentIframe");
  iframeCon.setAttribute("frameborder", 0);
  iframeCon.setAttribute("allowfullscreen", true);
  iframeCon.src = url;
},

需要解决的一些问题

1、项目之间的通讯

使用postMessage方法来完成基座项目和子项目之间的通讯。

2、iframe的弹窗及遮罩层问题

将弹出层代码写到父页面中,子页面使用postMessage方法发送消息告诉父页面打开,关闭弹层,父页面监听打开,关闭弹层。

<!-- 父页面弹窗蒙层 -->
<div class="openDiv" v-if="isOpen"></div>

//监听子应用消息
window.addEventListener(
  "message",
  function (event) {
    if (event.data == "openDiv") {
      that.openDiv();
    }
    if (event.data == "closeDiv") {
      that.closeDiv();
    }
  },
  false
);

遇到问题:父页面弹出层会把整个的iframe遮住。

解决方案:

1.父页面弹出层设置position: fixed;z-index: 100;
2.给ifame设置position: relative;z-index: 200;
3.子页面也要设置遮罩层,遮住ifame区域。position: fixed;z-index: 300;
4.设置弹窗容器,position: fixed;z-index: 400;
5.子页面事件,打开弹窗,向父页面发消息
6.父页面事件,监听消息,打开弹窗

<div class="box">
    <button @click="openDiv">弹窗</button>
</div>
<!-- 子页面弹窗蒙层 -->
<div class="openDiv" v-if="isOpen">
    <div class="openBox" @click="closeDiv">点击关闭</div>
</div>
// 子页面事件
openDiv() {
    // 向父窗口发送消息
    window.top.postMessage('openDiv', '*');
    this.isOpen = true;
},
closeDiv() {
    // 向父窗口发送消息
    window.top.postMessage('closeDiv', '*');
    this.isOpen = false;
},
3、iframe里的全屏问题

全屏方案,原生方法使用的是 Element.requestFullscreen(),iframe 标签设置 allowfullscreen=“true” 属性

//全屏事件
fullscreen() {
    const bigNode = document.getElementById("app");
    bigNode.requestFullscreen()
}
4、组件复用问题

公共组件可以单独提出来放到一个单独的项目里,在项目中把公共组件全部暴露出来供其它项目安装使用,也就是说主项目和子项目可以选择性安装需要的组件。

5、浏览器的后退问题

iframe 和主页面共用一个浏览历史,iframe 会影响页面的前进后退。并且 iframe 页面刷新会重置,因为浏览器的地址栏没有变化,iframe 的 src 也没有变化。

iframe页面外部的跳转尽管不会让浏览器地址栏发生变化,然而却会产生一个看不见的“history记录”,也就是点击后退或后退按钮(history.forward()或history.back())能够让iframe页面也后退后退,然而地址栏无任何变动。

所以精确来说后退无需咱们做任何解决,咱们要做的就是让浏览器地址栏同步更新即可。

当点击导航时,触发该事件,更新URL
//记录路由方法
routerKeyArrFun() {
  if (!this.forWard) return;
  const { index1, index2, menuTree } = this;
  const name1 = menuTree[index1]?.name || "";
  const name2 = menuTree[index1]?.subMenu?.[index2]?.name || "";
  if (name1) {
    const url = `#/?${name1}${name2 ? `#${name2}` : ""}`;
    history.pushState(null, null, url);
  }
},
6、刷新的问题

保障URL同步更新须要满足这3种状况:

  • 页面刷新,iframe可以加载正确页面;
  • 页面跳转,浏览器地址栏可能正确更新;
  • 点击浏览器的后退,地址栏和iframe都可能同步变动;
上面我们已经把路由信息记录并更新了URL地址。所以每当刷新或后退的时候,只要解析URL就可以了
//解析浏览器信息
getUrlModuleInfo() {
  this.forWard = false; //记录地址开关
  const { menuTree } = this;
  const ohref = window.location.href;
  const len = ohref.indexOf("?");
  if (len < 0) {
    this.selectFirstMenu();
    this.forWard = true;
    return;
  }
  const params = ohref.substring(len + 1).split("#");
  const [first, second] = params;
  menuTree.forEach((item, index) => {
    if (item.name !== first) return;
    this.selectFirstMenu(index);
    if (!second) return;
    item.subMenu?.forEach((sub, i) => {
      if (sub.name !== second) return;
      this.selectSecondMenu(i);
    });
  });
  this.forWard = true;
},
7、实现子应用免登录
7.1、跨域共享cookie

在XMLHttpRequest v2标准下,提出了CORS(Cross Origin Resourse-Sharing)的模型,试图提供安全方便的跨域读写资源。目前主流浏览器均支持CORS。(IE10+)

7.2、代理跨域共享Cookie

当我们的请求需要经过代理服务器时,可以将请求转发到同一个域名下的不同端口,这样就可以共享Cookie了。例如,假设服务器在8080端口,而我们需要共享Cookie,可以使用Nginx配置一个反向代理来实现。

server {
    listen 80;
    server_name xxx.com;
    location / {
        proxy_pass http://127.0.0.1:8080/;
        proxy_set_header Host $host;
    }
}
7.3、Json Web Token

在前后端分离的项目中,可以使用JWT(Json Web Token)来进行身份验证,并用它来代替Cookie来实现跨域共享。

界面效果

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/shanghai597/article/details/131764952