因为目前有时间了,所以在整理一下自己这几年写过的一些东西的相关文档,准备把一些东西改一下发出来,有的内容可能并不复杂,甚至有点浅显,但是也是对自己这几年的一些复盘和总结了
如果有需要,转载前请向我确认
另:刚入职的公司把总监和经理都干掉了,有可能因为发展路线原因不再需要前端,现在求一份广州 、 深圳的前端开发工作
本科5年经验,19年毕业,18年开始从事前端工作,基础良好、有千万级日活产品开发维护经验,有大型产品开发经验,有良好的代码风格与文档习惯
微前端这个概念相信对于前端来讲,其实并不陌生,大家也或多或少看到过相关的文章、或者有过相关的实践,如果我们去搜索:什么是微前端
那么我们其实很容易找到以下一些类似的定义:
等等,诸如此类的定义
「这时候可能会有同学会说:哎嘿?那我搞几个项目里面套几个iframe那不也是微前端?」
没错,如果按照上面的微前端的定义来说,iframe或许就是最初的微前端方案了,甚至连通过nginx路由转发来组合不同项目的功能组成一个系统都能可以是微前端。
那这时候可能不太清楚的同学就会想:既然基于 iframe 我们就可以搭建一套微前端的系统了,那为什么现在业界的微前端方案还层出不穷,各自都给出了自己的答卷?
关于这个问题,莫急,关于这个我们稍后简单讨论一下
「前面说了这么多,那微前端到底是什么呢?」
就像我们前面说过的一样,我们将不同的功能模块或业务通过例如
1、按照业务
2、按照权限
3、按照变更的频率
4、按照组织结构
5、跟随后端微服务设计
6、从代码出发的ddd实践
(可能很多人了解 ddd 这个概念都是在微前端或者微服务入坑的,手动滑稽.jpg)
等等的各种适用于不同实际业务场景的原则来划分出不同的子应用,通过「组合」的方式来组成一个完整应用的思想和技术方案 (所以微前端的拆分通常没办法简单抄作业)
所以,微前端本质上是一种通过 「模块化、拼图式」的开发,以「组合的思想」来降低前端集成的复杂度和成本的思想(以上概念仅限于个人理解,本人不为该观点正确性负责)
说到组合的思想,这时候可能很多同学都会不约而同地想到:
「嘿,终于到我熟悉的领域了」
在目前组合代替继承的思想流行下,相信每位前端同学对组合的思想都有自己的经验和见解,那么在这种思想下进行的前端开发中碰到的一些组合的思想所带来的问题,其实在微前端这种应用层面的组合上也不能逃脱
具体的内容我们会在第二部分中再进行简单的讨论
在前面我们抛出了一大堆的概念,大家其实对微前端的思想也有了一定的了解,很多人可能这时候会在想:
前面巴巴巴说了一大堆,那我一定就要用到这玩意吗?
它能给我们的业务带来什么价值,能超过它所带来的项目管理的问题吗?
(虽然我们可以通过 Lerna+Monorepo之类的工程结构来缓解管理的问题)
我们面临的问题就非它不可,必须要从架构的层面上去改变吗?
其实在采用微前端架构的时候,不管是用 qiankun,还是用 iframe,抑或是其他的什么解决方案,能用不同框架只是添头,实际上并没有触及到问题的本质,微前端各个部分之间相互独立,独立部署的能力本质上是在允许构建孤立或「松散耦合」的服务。
而松散耦合的系统,对于开发、维护、还是后期渐进式重构的好处都是毋庸置疑的。
松散耦合是各种相互联合事件的反映但是,每一个事件也都在保持自身的独特性,也存在着某些物质或逻辑上的分离,各种结构性要素松散联系,但是结构对结果基本没有什么影响的状态,有兴趣的同学可以了解一下相关概念,挺有意思的
所以在下面我们可以简单讨论一下,它更具体一点的应用场景
这个无疑是提起微前端时最容易被联想到的场景,在实际的开发生涯中其实我们经常能碰到以下的经典场景:
「(1) 在进入一个新团队的时候,经常有可能接手到一个 5 年陈的项⽬,或者我们需要重启一个多年未维护的项目」
这个项目可能会有强⾏混⽤多种技术栈的情况,例如我们现在这个六年陈的pc端就会有react 和 jq 混合开发的情况,或者是使⽤的技术栈落后、较为⼩众或学习成本过⾼。
例如Foundation、angular1.x、Easy Framework之类的玩意,又或者是重构不彻底的代码,经历了了重构-烂尾-又重构-又烂尾的项目(没错,说的上面那项目)
那这时候我们把原有功能抽离为单独的子应用,而新的功能模块作为新的子应用嵌入的办法来保证在逐渐重构的同时既要保证中间版本能够平滑过渡,同时持续交付新的功能?(只是一个思路,不一定就是用这种方式)
「(2) 保证当前技术⽅案在 3-5 年的业务迭代后还保有⽣命⼒,不会变成⼀个遗产项⽬」
「(3) 避免代码库不断膨胀⽽带来的各种问题」
「例如」
因为单体应用的不断膨大导致的理解和修改成本的不断上升
从代码的提交到实际部署的周期越来越⻓,并且很容易出问题,例如我们的H5项目有接近一百五十个打包入口,在每一个版本的迭代都需要重新全量打包部署,之前在有集团内项目打包时间统计的时候也名列打包最长时间的前三
难以交付可靠的单体应⽤, 系统庞⼤复杂 -> ⽆法进⾏全⾯⽽彻底的测试 -> 代码中的错误会进⼊⽣产环境 -> 因为程序中的代码都在同⼀进程中运⾏,应⽤缺乏故障隔离 -> 可能出现⼀些例如:内存泄漏 之类的问题导致⽤户运⾏过久后崩溃之类的问题
例如node写的中间件的实例则有可能崩溃导致⼤半夜因为⽣产环境的问题爬起来查bug
需要⻓期依赖某个可能已经过时的技术栈,并且框架难以升级新的版
等等。。。。。。。。
其实我们碰到
「(1)产品想要去上线一个试验性的活动或者功能的场景非常多」
这类功能在用户反馈不好的时候或许在很短的时间内就会被下线,而这个活动或功能的上线和下线每次都要经历一个成本较高的过程。
「(2)同时验证同一个功能的不同实现」
而在这方面其实就像酷狗曾经有一个组件即服务的微前端实现,每个直播间的组件灵活控制上下线,对于用户反馈的时机把握贼灵活
例如我们的目前的业务、实际上经常有可能会面临对不同学校有不同的特定的定制化需求或者功能组合,那实际上我们可以对每个单独的功能作为一个单独的子系统开发、⼦系统间的耦合只需要规定好相应的通讯⽅式和内容,不需要关注对⽅的实现,在需要的时候自由组合即可。
因为松散耦合的系统结构可应用的场景太多了,所以就不一一列举了
通过第一部分的简单描述,我们大概了解了什么是微前端,那么,在我们假设对目前流行的微前端的技术方案或者架构都不清楚的情况下,倘若我们需要去实践这么一个微前端的架构,我们需要关注哪些切实的问题?
跟我们在第一部分所提到的一样,微前端的实现本质上也是一种组合的思想,子应用间的组合其实和功能中组件的组合有一定程度上的异曲同工之妙,那么我们组件之间的组合最重要的是什么?
没错!就是 「通讯和状态管理」
就好像组件一样,子应用总会有各样的组合方式,那么父子应用间的通讯、兄弟应用之间的通讯、甚至爷孙应用之间的通讯就成了一个需要关注的问题。
既然是由不同的子应用之间组合而成,无法避免状态管理的问题,无论是全局下的状态管理、几个子应用间的局部状态管理,还是单个子应用间的状态管理,以及状态的上传和下发,都将是我们需要去考虑的问题
我们简单举一个例子,对于数据和状态的管理、在你想采用微前端架构的时候,不管是用 qiankun,还是用 iframe,用不同框架只是添头,没什么作用(因为编程语言或者目标语言没有改变),很多时候你遇到的问题可以转化成这样
就容易变成了redux 动机文档中的曼妥思糖 如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么
单独看这个问题,你会很想当然地说出状态提升的解法
这种解法没有问题,但是,当你这么解决问题,一旦应用范围内广泛存在数据耦合,你怎么办?
这时候必然将所有数据放置于全局单例,虽然牺牲了多例和初始化控制以及析构控制能力,但是确实能大大减少开发负担,这种解法就是 redux 或者说状态管理的本质
但是!
这种解法在微前端中是完全无法使用的
因为微前端架构,在应用这一层级之上,目标是「分开开发,分开构建,分开部署」
「你能将单一数据原则应用于此么?」
答案是不能的,每个应用有自己的 store,最终还是会回到第一个例子的问题上去
所以,我们要正视怎么解决这样的问题,这才是微前端之所以微,松散耦合之所以松散的本质
「其实这个本质上是两个问题」
1、子会响应父的状态变化,父会在子初始化之后初始化,子会在父变更后变更,导致子状态必须在子组件内部
2、react 以及状态管理,只有子向父 dispatch 事件的能力,父无法向子 dispatch 事件
即使是换到父子应用层面上的理解也依旧如此
而且我们需要知道一件事,在第一部分中其实我们有提过微前端的应用间的关系其实也是一种组合关系
在组合关系中,「被组合对象是绝对会耦合于源对象的」
所以我们在状态管理的层面上将组合的方式转为更松散的逻辑关系,例如:聚合?
而这种转换其实一般来说我们都是通过「依赖注入」的方式去实现的
但是这种情况下,子组件依然无法拥有自己的状态,因为这依然是单一数据处理方式,只不过没有隔一层 props
并且这时候还是没能解决 只有子向父 dispatch 事件的能力,父无法向子 dispatch 事件的问题
「那我们需要怎么解决这个问题?」
其实这个问题很好解决,实现一个「发布订阅模型」就可以了,所以这也是为什么qiankun之类的微前端方案的应用间通讯方式是使用发布订阅的方式进行
这样处理,parent 和 Child 就可以彼此通过事件传递消息,且独立变化,让整个结构松散耦合起来
js沙箱相关:https://zhuanlan.zhihu.com/p/527437146
「js 沙箱」
因为我们每一个子应用都是一个单独的项目和应用,在同一个页面中出现多个子应用是很常见的场景
那么出于安全的考虑,例如全局变量污染、多版本库、以及上面提到的故障隔离,还有各种复杂的场景下的执行问题
我们理所当然是需要对每个子应用间的 js 的工作空间进行隔离,使每个子应用内的执行自洽,只关注输出和输入
那么js沙箱的实现也就无疑成为了微前端方案中急需关注的一点。
「css 沙箱」
既然关注了js的隔离,那css的隔离自然也逃不掉
虽然是微前端的结构,但是本质上同一个页面中每个子应用的挂载依旧在同一个dom树上
那么我们自然不希望子应用间的样式会出现互相干扰的情况,并且在子应用切换时可以自行装载和卸载。
因为在微前端的方案中,每个子应用都是独立的,所以如果不做任何处理的话在一个页面存在多个子应用的情况下,每个子应用的加载都是独立的,在这种情况下我们就很容易让用户体验到不同功能之间陆续从白屏到加载完成的割裂过程。这个无疑是致命的缺陷。
并且在实际用户使用中,浏览器大部分时间是处在空闲的,我们要怎么利用这样的空闲时间,去加载其他子应用的JS,用来优化用户体验?
这个问题属于我们软件开发中项目管理的部分,子项目多了之后,公共依赖如果处理不好,不但造成我们开发工时的浪费,有各种重复工作,同时BUG的风险也会随着复制的代码过多,成指数增长,更不要说日后的长期维护
这个为啥要关注不用我多说了吧,这位靓仔,你也不想你的路由不知道咋跳对吧。目前我知道的有两个方案
方案原理大致是监听了popstats或者hashchange事件,并劫持了浏览器history下的pushState和replaceState后
实现思路是主应用使用现有的路由库,例如vue router或者react router,子应用使用webpack federation,将现有的页面发布成独立的服务,在主应用中重新配置路由,使用webpack提供的import()函数,动态加载子用的模块
但是上面其实没有考虑到子应用自身的路由跳转、子应用a跳转到子应用b,或者子应用a要打开子应用b的指定路由的跳转之类的场景是html entry还是js entry。别问,问就是快,一个加载的是按原来方式打包出来的一个html文件,一个是加载子应用打出来的整个js文件。
而且将整个微应用打包成一个 JS 文件常见的打包优化基本上都没了,按需加载、首屏资源加载优化、css 独立打包等想都别想
事关keep-alive之类的需求在微前端的架构中怎么做,这位靓仔你也不想切个子应用,之前的状态就全没了吧
支不支持子应用之间的互相嵌套,子应用件互相嵌套情况下的通讯和状态管理
当然,以上这些实际上只是具体在落地微前端方案的时候需要关注的具体问题 但是从更高一点的层面上来看
其实我个人觉得主要其实要关注的有「三个纬度和两个问题」
三个维度:代码的管理、工程的独立性、优雅地集成
两个问题:什么场景适用、怎么更好地拓展
包括了请求安全、数据安全、配置安全、界面完整、交互完整、元素完整等,但是这里我们就不细细讨论了
毕竟这只是一个四十分钟不到的分享
「所以我们最后的小结是」:
在我们需要一个微前端方案时,我们需要考虑的问题有如下几点:
通过第二部分的思考,我们大概了解了,假设我们在对现有的微前端解决方案都不清楚的情况下
我们需要去落地一个微前端的架构,我们将会需要关注哪方面,以及需要应对哪些可能会出现的问题
那么在这一部分,我们将会去对比一下当前除了 wujie 以外的较为流行的微前端方案
简单探讨一下他们的实现方案的优劣,以及未来可能会存在的发展方向
(因为时间有限,并且不是此次分享重点,所以不会过于详细)
根据我们第一部分对微前端的定义来说,nginx转发来分割和组合应用其实也能算是一种微前端的实现方案 但是根据我们第二部分的思考来说,显然大部分问题的处理都是做不到的,所以这里就贴个图出来凑个数算了
根据我们第二部分的思考,其实iframe的方案在一些地方有着天然的优势,例如 iframe 的隔离完美,无论是 js、css、dom 都完全隔离开来,子应用间的嵌套毫无压力,随便套,并且使用简单,没有任何心智负担,天然支持html entry
但是「缺点」也非常明显:
所以这很难说得上是一个比较好的实现方案,但是后续我们会讲到wujie是怎么解决iframe的缺点,打造一个接近完美的iframe方案的。
qiankun是一个很经典的基座模式下的微服务方案,但是在使用成本上来说是有点大的,它对代码的侵入型很强,如果要改造的话,从 webpack、代码、路由等等都要做一系列的适配
能力 | 现状 |
---|---|
通讯与状态管理 | 1、初始化状态通过props传入子组件 2、qiankun通过initGlobalState, onGlobalStateChange, setGlobalState实现主应⽤的全局状态管理,然后默认会通过props将通信⽅法传递给⼦应⽤,但是本质上还是通过发布 - 订阅的方式来进行通讯 3、主子应用localStrage、cookie可共享 所以在通讯上并不是很完美 |
js和css隔离 | 提供js和css隔离,但是在js的沙箱方面依然有不少坑有问题,近一年的很多changelog都是在修js沙箱的问题 |
预加载 | 做了静态资源的预加载能力 |
公共依赖的处理 | 文档内写明不推荐,但是硬要的话可以在微应用中将公共依赖配置成 external,然后在主应用中导入这些公共依赖的 |
路由管理 | 1、每个子应用中注册,然后由主应用进行管理,主应用的路由实例通过 props 传给微应用,微应用这个路由实例跳转 2、注册微应用的基础配置信息。当浏览器 url 发生变化时,会自动检查每一个微应用注册的 activeRule 规则,符合规则的应用将会被自动激活 3、页面上不能同时显示多个依赖于路由的微应用,因为浏览器只有一个 url,如果有多个依赖路由的微应用同时被激活,那么必定会导致其中一个 404。 「(因为基于路由匹配,所以无法同时激活多个子应用)」 |
html entry | 支持 |
应用保活 | 无法支持子应用保活 |
应用嵌套 | 支持 |
生命周期 | 支持 |
能力 | 现状 |
---|---|
通讯与状态管理 | 基于发布订阅+CustomEvent |
js和css隔离 | js: 使用Proxy拦截了用户全局操作的行为。 css: 利用标签的name属性为每个样式添加前缀,将子应用的样式影响禁锢在当前标签区域,但是依旧没办法必定隔绝 |
预加载 | 在浏览器空闲时间,依照开发者传入的顺序,依次加载每个应用的静态资源,以确保不会影响基座应用的性能 |
公共依赖的处理 | 没有解决基座应用和子应用共用依赖的特性,issues中回答了在计划中,但是还没想好怎么做 |
路由管理 | 每个应用的路由实例都是不同的,应用的路由实例只能控制自身,无法影响其它应用,包括基座应用无法通过控制自身路由影响到子应,路由跳转只有三种方式:window.history,通过history.pushState或history.replaceState进行跳转、通过数据通信控制跳转(基座控制子应用跳转,子应用监听基座数据变化而跳转)、传递路由实例方法,把实例传到子应用里去跳转(子应用控制基座跳转) url属性和子应用路由没有关系,只是用来加载html资源 |
html entry | 支持, 实际上是以类WebComponent + HTML Entry实现微前端的组件化,但是webcomponents没有做降级处理 |
应用保活 | 支持,< micro-app name='xx' url='xx' keep-alive> |
应用嵌套 | 支持 |
生命周期 | 支持 |
能力 | 现状 |
---|---|
通讯与状态管理 | 别想了,互相之间都是独立模块,并且去中心了,你还想咋共享? |
js和css隔离 | js: 没有沙箱,全凭开发者自觉不瞎搞 css: 别想了,只能通过 postcss-selector-namespace 添加前缀或者别名之类的方式来处理 简单来说,没有有用的 css 沙箱和 js 沙箱,一切需求靠用户自觉 |
预加载 | 没有利用浏览器空闲时间去做子应用加载的处理 |
公共依赖的处理 | 天然支持 |
路由管理 | 微应用的路由是有发生冲突的可能性的,为了实现独立部署能切换页面,各微应用都有自己的路由,要解决就只能微应用单独部署时,使用 web 路由,集成到 container 时,使用内存路由,web 路由由 container 接管,浏览器地址栏变化时,告 诉集成进来的微应用,然后微应用再跳转到相应的页面。 |
html entry | 没有 |
应用保活 | 没有,别想 |
应用嵌套 | 想怎么套就怎么套 |
生命周期 | 别想 |
通过第二和第三部分的思考,我们大概了解了落地一个微前端的架构可能会需要面临的挑战,以及当前流行的技术方案对于面临的问题提出了哪些思想或解决方案 所以第四部分我们就简单讲讲第二部分我们所考虑到的问题在 wujie 的方案中是怎么处理的
其实我们可以直接看 wujie 的github仓库 可以发现它的实现实际上非常简单,加起来不过3000行代码,所以打消了我想挑一块源码功能的实现来讲用于消磨时间的打算(滑稽.jpg)
wujie 是一个iframe + webComponent 的解决方案 既然提到了iframe,那么我们肯定要关注前面提到的iframe存在着通讯、路由、dom割裂严重、白屏时间长、难做预加载、应用无法保活、浏览器对相同域的连接有限制,会影响⻚⾯的并⾏加载等问题在wujie中是否被解决
在wujie中提供了三种通讯方式:
(1 「props 通信」,主应用可以通过props注入数据和方法
(2 「window 通信」,由于在设计上子应用运行的iframe的src和主应用是同域的,所以相互可以直接通信,这个我们在下一步中细述
(3 「eventBus 通信」 其中最有趣的就是这一点,wujie 提供了一套去中心化的通讯方式
假如我们在应用a中想要加载应用b,那么我们可以怎么做才能避免上述问题?
在应用 A 中构造一个shadowRoot 和iframe,然后将应用 B 的html写入shadowRoot中,js运行在iframe中,因为iframe的js隔离真的很完美。
这时候我们注意 iframe 的 url,iframe保持和主应用同域但是保留子应用的路径信息,这样子应用的js可以运行在iframe的location和history中保持路由正确。
「这样就可以解决了路由状态的问题,并且解决可以使用window来通讯」
Shadow DOM API 的 ShadowRoot 接口是一个 DOM 子树的根节点,它与文档的主 DOM 树分开渲染。
「源码」
那在iframe中dom割裂的问题要怎么处理?
在iframe中拦截document对象,统一将dom指向shadowRoot,此时比如新建元素、弹窗或者冒泡组件就可以正常约束在shadowRoot内部。
我们可以从源码中看出,在非降级的情况下, wujie对iframe的 document进行了代理,从而解决了解决了 dom 割裂严重的问题
那这时候实际就只剩下了白屏、预加载和应用保活的问题
白屏实际上可以分为两个场景,一是「首次加载白屏」,二是「切换应用时白屏」
「首次白屏的问题」,wujie实例可以提前实例化,包括shadowRoot、iframe的创建、js的执行,这样极大的加快子应用第一次打开的时间
「切换白屏的问题」,一旦wujie实例可以缓存下来,子应用的切换成本变的极低,如果采用保活模式,那么相当于shadowRoot的插拔
所以以上的整套机制在wujie中的实现对应的流程图如下图一样
对于预加载的处理也非常简单,在子应用使用fiber模式执行的情况下,使用 requestIdleCallback ,在浏览器空闲时间去加载资源
这个不用多言,懂的都懂
文档内写明不推荐,但是硬要的话可以在微应用中将公共依赖配置成 external,然后在主应用中导入这些公共依赖的
跟其他方案不同的是,wujie的路由管理有两个比较有意思的点
「1、路由同步」
会将子应用路径的path+query+hash通过window.encodeURIComponent编码后挂载在主应用url的查询参数上,其中key值为子应用的 name。
开启路由同步后,「刷新浏览器或者将url分享出去子应用的路由状态都不会丢失」,当一个页面存在多个子应用时无界支持所有子应用路由同步,浏览器刷新、前进、后退子应用路由状态也都不会丢失,并且「提供短路径的能力」,当子应用的url过长时,可以通过配置 prefix 来缩短子应用同步到主应用的路径,无界在选取短路径的时候,按照匹配最长路径原则选取短路径。
「2、路由跳转」
无界支持子应用间的路由的跳转,例如子应用a可以跳转子应用b,也可以从子应用a跳转到子应用b的指定路由中,这点在其他方案中暂时还没看到有。
并且如果子应用a要跳转到子应用b是保活应用,并且已经进行过初始化了,那也有对应的方案使子应用b的状态不会因为跳转而丢失
wujie 和 micro-app 的发布时间其实相距不远,并且两者都用到了webComponent的能力,但是wujie 对于webComponent 特性不支持的情况做了无感知的降级方案,但是micro-app没有
以上内容纯属个人观点,仅供讨论,不保证内容正确性
往期推荐
欢迎加我微信,拉你进技术群,长期交流学习...
欢迎关注「前端Q」,认真学前端,做个专业的技术人...