|
作者简介谢宇航:Self-introductionisnotdefined!什么是微前端?微前端是一种多个团队通过独立发布功能的方式来共同构建现代化web应用的技术手段及方法策略。将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品。从下图可以看出,微前端的页面布局主要分为三部分header头部(他也是主应用的基座),菜单(切换不同子应用),以及子应用展示区域。我们通过切换切换不同菜单来渲染不同的应用。页面布局微前端具备以下几个特点技术栈无关(主框架不限制接入应用的技术栈,微应用具备完全自主权)独立开发,独立部署(微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新)增量升级(面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略)独立运行时状态隔离(每个微应用之间状态隔离,运行时状态不共享)whynotiframe?提到微前端的解决方案,为什么不用iframe?这几乎是所有微前端方案第一个会被提到的问题。但是大部分微前端方案又不约而同放弃了iframe方案,自然是有原因的...虽然iframe无疑是最简单的方式,还天然支持样式隔离以及全局变量隔离,但是iframe也有让人诟病的几个问题:url不同步。浏览器刷新后iframeurl状态会丢失、后退前进按钮无法在iframe里使用。但是产品又要求你子应用之间跳转返回到上一个子应用里某个页面的url的时候,你是不是心里会凉了一截。UI不同步,DOM结构不共享。想象一下屏幕右下角1/4的iframe里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器resize时自动居中,这时候你是不是已经感到无比的崩溃。全局上下文完全隔离,内存变量不共享。iframe内外系统的通信、数据同步等需求,主应用的cookie,token要透传到根域名的不同的子应用中实现免登效果。在之前需求中,子应用接口数据还需要到主应用中解密才可以,导致需要利用postmessage+监听做父子之间。虽然可以解决,但是你是不是感到十分的恶心?由于iframe还存在同源策略,极大地增加了子应用之间通信的难度。慢。每次子应用切换都是一次浏览器上下文重建、资源重新加载的过程。qiankun微前端优势我们开始切入主题,既然说了iframe这么多缺点,那么一定是有很好的方案去解决iframe的痛点。下面我们用一张表格来对比一下qiankun和iframe的区别:qiankunIframe数据共享windowhash事件机制私有通信机制PostMessage访问历史应用之间统一应用之间独立全局作用域共享完全隔离CSS作用域可共享可独立完全独立资源加载可预加载快慢数据共享方面,qiankun是通过**共享window**来实现,而iframe则很局限,只能在通过hash、query等方式在url上添加数据。事件机制方面,qiankun封装了一套私有通信机制,主应用和子应用可以通过提供封装好的API来进行双向的数据交互,而iframe子应用到主应用通信则需要postMessage方式进行。访问历史方面,qiankun应用之间前进后退的访问栈是统一的,iframe应用之间独立。qiankun可预加载子应用资源,iframe很慢,每次仍然需要加载全部资源。qiankun实现原理qiankun是基于single-spa,具备js沙箱、样式隔离、HTMLLoader、预加载等微前端系统所需的能力。qiankun可以用于任意js框架,微应用接入像嵌入一个iframe系统一样简单。qiankun主要作用在主应用层(基座)。应用层基座作为微前端主框架的核心成员,充当调度者的角色,由它来决定在不同的条件下激活不同的子应用。因此主框架的定位则仅仅是:导航路由+资源加载框架。它保证应用具备独立开发权的同时,又有将它们整合到一起保证产品完整的流程体验的能力。(下图为应用架构示意图)应用架构他的实现原理主要分为以下几个方面,这里通过梳理原理顺便讲一下对应需要的配置项。这样在应用期间可以很清晰的明白为什么这样配置,并且也可以帮助快速定位问题。路由系统例如下面的链路图,当点击导航中的某个子应用链接,这时候主应用通过劫持urlchange事件,匹配子路由并加载命中的子应用资源,待加载完之后,子路由接管urlchange事件。子应用的路由表需要设置baseurl来保证访问某一子应用期间是一直可以命中当前子应用的。链路图APPEntry主应用与子应用的集成方式为运行时组合。子应用自己构建打包,主应用运行时动态加载子应用资源。这样的方式也更加灵活,并没有采用构建时的组合方式。这样子应用每次发布更新不依赖父应用重新打包发布。达到父子应用解耦的效果。HTMLEntry刚才讲到路由系统时说道匹配完子路由并加载子应用,那么qiankun加载过程实际上是主框架通过fetchhtml的方式获取子应用的静态资源,同时将HTMLdocument作为子节点塞到主框架的容器中(如下图所示)。由于子节点是实实在在插入到主应用的,这样实际上实现了共享window的效果。qiankun一大特点就是将html做为入口文件,规避了JavaScript为了支持缓存而根据文件内容动态生成文件名,造成入口文件无法锁定的问题。qiankun将html做为入口,所依赖的import-html-entry库。原理:import-html-entry模块传入需要解析的html模板路径(和封装的fetch方法)->解析html文档->返回被处理后的HTML模板字符串,script,link,style标签及内容提取->外联样式被替换为内联样式->通过修改脚本字符串,改变脚本执行时候的window/self/this的指向->执行所有的script代码HTMLEntry示意图需要注意的几点:使用fetch涉及跨域,需要在子应用中设置CORS跨域。需要配置publicpath解决微应用动态载入的脚本、样式、图片等地址不正确的问题。JS隔离qiankun的JS沙箱是基于Proxy实现代理了window上常用的常量和方法以及不支持Proxy时降级通过快照实现备份还原。原理:模拟ES6的ProxyAPI,通过代理劫持window->当子应用修改或使用window上的属性或方法时,把对应的操作记录下来->每次子应用挂载/卸载时生成快照,当再次从外部切换到当前子应用时,再从记录的快照中恢复通过qiankun提供的生命周期钩子,在bootstrap及mount钩子下记录快照;unmount回滚,remount恢复。CSS隔离由于微前端场景下,不同技术栈的子应用会被集成到同一个运行时中,所以我们必须在框架层确保各个子应用之间不会出现样式互相干扰的问题。解决方案有以下几种:1.ShadowDOM基于WebComponents的ShadowDOM能力,子应用的样式作用域仅在shadow元素下,我们可以将每个子应用包裹到一个ShadowDOM中,保证其运行时的样式的绝对隔离。但是shadowDOM的缺点也很明显。比如sub-app里调用了antdmodal组件,由于modal是动态挂载到document.body的,而由于ShadowDOM的特性antd的样式只会在shadow这个作用域下生效,结果就是弹出框无法应用到antd的样式。解决的办法是把antd样式上浮一层,丢到主文档里,但这么做意味着子应用的样式直接泄露到主文档了。2.CSSModule通过约定css前缀的方式来避免样式冲突,即各个子应用使用特定的前缀来命名class,或者直接基于cssmodule方案写样式。通常可以给主应用样式都加前缀,因为主应用内容相对较少,加起来很方便。这也适用于全新的项目。但是对于子应用中使用了三方的组件库,三方库在写入了大量的全局样式的同时又不支持定制化前缀的时候又很难去解决。3.DynamicStylesheet配合HTMLEntry,只需要在应用切出/卸载后,同时卸载掉其样式表即可,原理是浏览器会对所有的样式表的插入、移除做整个CSSOM的重构,从而达到插入、卸载样式的目的。这样就可以保证,在一个时间点里,只有一个应用的样式表是生效的。这也是qiankun默认选择的css隔离方式。qiankun使用教程1.安装qiankun#或者npmiqiankun-S$yarnaddqiankun2.在主应用中注册微应用import{registerMicroApps,loadMicroApp,start}from'qiankun';registerMicroApps([{name:'reactapp',//appnameregisteredentry:'//localhost:7100',container:'#yourContainer',activeRule:'/yourActiveRule',},{name:'vueapp',entry:{scripts:['//localhost:7100/main.js']},container:'#yourContainer2',activeRule:'/yourActiveRule2',},]);//如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:loadMicroApp({name:'app',entry:'//localhost:7100',container:'#yourContainer',});start();当微应用信息注册完之后,一旦浏览器的url发生变化,便会自动触发qiankun的匹配逻辑,所有activeRule规则匹配上的微应用就会被插入到指定的container中,同时依次调用微应用暴露出的生命周期钩子。3.微应用导出相应的生命周期钩子微应用需要在自己的入口js(通常就是你配置的webpack的entryjs)导出bootstrap、mount、unmount三个生命周期钩子,以供主应用在适当的时机调用。/**bootstrap只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用mount钩子,不会再重复触发bootstrap。通常我们可以在这里做一些全局变量的初始化,比如不会在unmount阶段被销毁的应用级别的缓存等。*/exportasyncfunctionbootstrap(){console.log('reactappbootstraped');}/**应用每次进入都会调用mount方法,通常我们在这里触发应用的渲染方法*/exportasyncfunctionmount(props){ReactDOM.render(
|
|