【方案】网页由微应用拼图,微前端
大家好,欢迎来到停止重构的频道。
本期我们讨论:微前端。
网站的前端网页是否可以像后端应用服务一样,可以拆分为独立开发的子应用,从而提升大型前端项目的协作效率与维护性呢。
我们按这样的顺序展开讨论
- 微前端及其好处
- iframe与单页应用的弊端
- 我们的解决方案
- 具体实现
微前端及其好处
微前端指的是,一个大型前端应用拆分成多个小型前端应用,小型前端应用可独立开发、独立测试和独立部署。
以我们的官网为例,头部header和底部footer都是固定的,只有中间的部分是变化的。
我们对应的微前端解决方案为:打开的网页是一个基座网页,基座网页上有三个插槽,分别放入“头部Header”和“尾部Footer”。
当网页URL产生变化时,不会发生真正的网页跳转,而是由基座响应这个变化,只替换中间内容部分。
虽然这么说不太不准确,但是也可以这么理解:
微前端,就是希望一个网页可以引入别的独立网页。
被引入的独立网页可以独立开发、独立部署。
这么一说很多人会想到iframe,iframe确实是其中一个不太好的解决方案,后面会展开讨论。
这里需要补充说明的是,微前端与React/Vue等框架的组件概念是不一样的。
微前端的网页碎片是夸工程且独立开发的,甚至一些微前端框架允许引入由不同技术栈开发的网页。
微前端的好处不言而喻,就是它能对需求变更、多团队合作等场景非常友好,所有的网页碎片都可以独立开发、独立替换。
在网站特别复杂的时候,这种优势尤为明显,网站可划分更多的插槽,灵活嵌入更多的网页碎片 网页碎片中也可以挖多个插槽,嵌套插入网页碎片。
比如说,网站需要新增一个排行榜的小栏目,这个小栏目需要在多个网页中显示。
那么,微前端就可以单独开发一个排行榜网页,然后多个网页直接引入这个排行榜网页,并对其大小进行限制就可以了。
对于我们而言,我们希望一个大型网站是由多个子系统拼接而成的。
其中,一个子系统的前端网页往往是一些碎片零件。
比如博客系统的文章编写、文章展示这些网页零件,都不可能直接作为单独网页展示,而是需要嵌入到别的网页中作为其中一部分。
微前端的概念正好能解决这个问题。
也就是说,对于前端部分,我们可以直接接入已经开发好的子系统。
而不是每次都从零开始,对着每次都大同小异的UI设计图,写着重复的代码。
这样对持续升级也是十分友好,不需要每次升级都对已有代码进行大范围修改。
iframe与单页应用的弊端
一听到,微前端就是,允许一个网页引入另一个网页。
相信大家都会想到iframe,iframe允许在一个网页引入另一个网页,使用方便的同时隔离性也非常好。
虽然,网上有很多人说iframe性能差,基本的证据都是大同小异的。
就是网页是单线程,iframe与宿主网页共用线程会导致性能差。另外,就是iframe可能会加载重复的文件,导致性能浪费等等。
但是,iframe方案我坚持了很多个项目,它在使用上非常简单,不需要过多的学习成本。
另外,性能问题也是几乎是不存在的,毕竟用户并不是在用单片机上打开网页,iframe方案甚至比很多花里胡哨框架的响应速度还要高。
让我放弃iframe的理由是,iframe存在样式割裂的问题。
比如弹出模块框,黑色背景不能全屏网站显示,若是想固定某个html元素,只能通过Js代码计算固定,而Js代码计算固定,会有抖动等不可解决的问题。
对于微前端,另一个比较常见的方案是单页应用。
通过一个网页js入口,可以将网页碎片封装成各种组件,按需加载这些组件。
但是,这样是单个工程的解决方案,不能夸工程引入。
除非网站内容非常少,不然单论一个工程而言,团队合作中的代码冲突、目录混乱 都会让人头皮发麻。
当然,除了传统的单页应用工程,还有服务端渲染的框架,是可以允许夸工程引入的。
但是,这些解决方案都有一个致命问题。
就是在切换网页碎片时,浏览器虽然可以清理掉不再需要的HTML/CSS部分,但是却没办法清理不再需要的JS代码,也就是内存占用会越来越高。
所以单页应用在大型网站的表现并不佳,把整个大型网站的前端做成一个单页应用也是不可行的,尝试过的团队,都是一做一个不吱声。
我们的解决方案
iframe和单页应用的弊端其实也凸显出了,微前端的两个核心问题:
1、网页样式部分不希望有割裂感
2、JS部分需要可以清理
针对以上的核心问题,我们借鉴了wujie框架的设计思想。
将一个网页分成了两个部分:UI部分和Js部分。
UI部分放在宿主网页的ShadowDom中隔离,Js部分放在iframe沙箱中运行。
其中,ShadowDom是dom树下的隔离节点,与dom树在同一个文档流的同时,样式可以进行隔离。
这样就可以解决,纯Iframe方案,UI样式割裂的问题,也能解决,单页应用方案,JS代码累积的问题。
Trick2也因此扩展了微前端相关功能,在开发调试网页时,按正常网页开发调试即可,当工程需要发布时,从原来的普通打包方式改为SPA方式即可。
网页需要引入SPA方式打包的网页碎片时,需要采用_BoxPage组件引入。
引入的方式也很简单,像iframe一样,就是设置目标网页的URL即可。
具体实现
接下来是具体的实现方式,由于需要将网页分离为UI部分和Js部分,UI部分放在ShadowDom中隔离,Js部分放在iframe沙箱中运行。
所以具体实现也分成了4个步骤,具体的代码实现可以翻看_BoxPage组件的源码。
- 分离网页的UI部分和JS部分
- 将UI部分插入宿主网页的ShadowDom中
- 将JS部分放入iframe沙箱中运行
- 重新关联UI部分和JS部分
首先是分离UI部分和JS部分,这部分其实很简单,就是将目标URL的HTML文本获取下来,再通过现成JS函数转换为HTML对象。
<link>标签和body为UI部分,<script>标签为JS部分。
分离完毕后,直接将UI部分写入当前网页的ShadowDom中,ShadowDom其实就是在HTML节点下开启一个ShadowDom节点,ShadowDom节点的窗体大小受外层节点的影响。
剩下的JS部分,通过iframe的srcdoc属性写入即可,也就是iframe中只包含目标网页的JS部分。
值得一提的是,IOS的微信浏览器是不支持上述iframe属性的,需要通过比较老旧的方式写入。
最后一步是关联UI部分和JS部分,更具体的说,是劫持JS代码中的document、window等基础对象,使其指向Shadowdom中的UI部分。
这需要分两部分完成,先是宿主网页需要创建proxy对象,用于替代JS代码中的document、window等基础对象。
创建proxy对象的原因,是可以精准控制哪些变量、函数使用宿主网页,哪些指向ShadowDom中的UI部分。
然后,在网页打包时,需要将document、window对象改为宿主网页传入的proxy对象。
实际上就是在webpack打包时,将所有js文件的外层都套一层自动执行函数,就可以将document、window对象劫持为上面的proxy对象。
至于为什么我们源码中除了document、window对象以外,还劫持了其他对象。
这些都是在调试时发现的,这些特殊对象的使用都存在于某些第三方库中,若不一并劫持,网页将运行不成功。
以上就是大概的实现步骤,具体可以翻看_BoxPage组件的代码,很多琐碎的细节没办法在这里展开。
总结
以上是我们的微前端解决方案。
我们希望微前端网页的引入方式跟iframe是一样的,只需要目标网页的URL。
微前端网页开发上也不希望有特殊适配,只需要更换打包方式即可。
我们的微前端方案,确实只是一个网页可以引入另一个独立网页。
与其他比较重的微前端解决方案不同,它们都包含更加复杂的网页跳转/切换方式,也就是网页路由。
关于网页路由,我们会在后续的网站基座解决方案提及,因为网页路由我们保持了最原始直接的a标签跳转。我们不希望徒增不必要的学习成本和多余的适配工作。