一、微前端是什么
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 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-inde
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来实现跨域共享。
七、界面效果