微前端概念及方案对比

做了一年多的前端,今天突然听到了一个名词叫做微前端,很好奇,于是去查询了一些资料,在这里总结下。

微前端是什么

微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。微前端不是单纯的前端框架或者工具,而是一套架构体系,这个概念最早在2016年底被提出,可以参考在Google上搜索Micro-Frontends。

也就是说微前端更像是一种理念,像是微服务将一个大的系统拆分为多个系统一样,微前端也将一个大的前端项目拆分为多个。

从这个角度讲,微前端并不是一定要用前端技术来实现,而事实也确实如此,微前端有很多方案,我们下面就会逐一介绍并进行比较。

微前端是为了解决什么问题?

任何新技术的产生都是为了解决现有场景和需求下的技术痛点,微前端也不例外:

  1. 拆分和细化:当下前端领域,单页面应用(SPA)是非常流行的项目形态之一,而随着时间的推移以及应用功能的丰富,单页应用变得不再单一而是越来越庞大也越来越难以维护,往往是改一处而动全身,由此带来的发版成本也越来越高。微前端的意义就是将这些庞大应用进行拆分,并随之解耦,每个部分可以单独进行维护和部署,提升效率。
  2. 整合历史系统:在不少的业务中,或多或少会存在一些历史项目,这些项目大多以采用老框架类似(Backbone.js,Angular.js 1)的B端管理系统为主,介于日常运营,这些系统需要结合到新框架中来使用还不能抛弃,对此我们也没有理由浪费时间和精力重写旧的逻辑。而微前端可以将这些系统进行整合,在基本不修改来逻辑的同时来同时兼容新老两套系统并行运行。

也就是说微前端一方面可以将已有的大型项目进行拆分,便于以后的项目维护,另一方面,也可以将新的项目和旧的历史项目整合到一起,即使它们的语言,框架完全不同。

为了解决上述问题,微前端要做什么?

说白了,微前端就是为了把一个大的前端项目拆分为多个小的前端项目。而这几个前端项目之间可以使用完全不同的框架,但同时这几个项目之间还能通信。

为了实现这个目的,我们需要设计几个东西:

说实话,这个图一画出来,我就想起了electron,electron的主进程就像这里的主工程,electron的每个渲染进程就像这里的微应用。

不同的是electron只有主进程和渲染进程之间可以通信,渲染进程之间不可以。

简单描述下就是基座应用中有一些菜单项,点击每个菜单项可以展示对应的微应用,这些应用的切换是纯前端无感知的,所以,基于目前的方案来说,一个微前端的基座框架需要解决以下问题:

  1. 路由切换的分发问题。
  2. 主微应用的隔离问题。
  3. 通信问题。

下面针对这些问题来一一阐述。

几种方案的比较

单纯根据对概念的理解,很容易想到实现微前端的重要思想就是将应用进行拆解和整合,通常是一个父应用加上一些子应用,那么使用类似Nginx配置不同应用的转发,或是采用iframe来将多个应用整合到一起等等这些其实都属于微前端的实现方案,他们之间的对比如下图:

方案描述优点缺点
Nginx路由转发通过Nginx配置反向代理来实现不同路径映射到不同应用,例如www.abc.com/app1对应app1,www.abc.com/app2对应app2,这种方案本身并不属于前端层面的改造,更多的是运维的配置。简单,快速,易配置在切换应用时会触发浏览器刷新,影响体验
iframe嵌套父应用单独是一个页面,每个子应用嵌套一个iframe,父子通信可采用postMessage或者contentWindow方式实现简单,子应用之间自带沙箱,天然隔离,互不影响iframe的样式显示、兼容性等都具有局限性;太过简单而显得low
Web Components每个子应用需要采用纯Web Components技术编写组件,是一套全新的开发模式每个子应用拥有独立的script和css,也可单独部署对于历史系统改造成本高,子应用通信较为复杂易踩坑
组合式应用路由分发每个子应用独立构建和部署,运行时由父应用来进行路由管理,应用加载,启动,卸载,以及通信机制纯前端改造,体验良好,可无感知切换,子应用相互隔离需要设计和开发,由于父子应用处于同一页面运行,需要解决子应用的样式冲突,变量对象污染,通信机制等技术点

上述方案中,每种都有自己的优劣,最原始的Nginx配置反向代理是从接入层的角度来将系统进行分离,但是需要运维配置,而iframe嵌套是最简单和最快速的方案,但是iframe的弊端也是无法避免的,而Web Components的方案则需要大量的改造成本,最后的组合式应用路由分发方案改造成本中等并且能满足大部分需求,也不影响各前端应用的体验,是当下各个业务普遍采用的一种方案,本文后面的内容也是主要基于这种方案进行阐述。

路由分发问题

作为微前端的基座应用,是整个应用的入口,负责承载当前微应用的展示和对其他路由微应用的转发,对于当前微应用的展示,一般是由以下几步构成:

  1. 作为一个SPA的基座应用,本身是一套纯前端项目,要想展示微应用的页面除了采用iframe之外,要能先拉取到微应用的页面内容, 这就需要远程拉取机制

  2. 远程拉取机制通常会采用fetch API来首先获取到微应用的HTML内容,然后通过解析将微应用的JavaScript和CSS进行抽离,采用eval方法来运行JavaScript,并将CSS和HTML内容append到基座应用中留给微应用的展示区域,当微应用切换走时,同步卸载这些内容,这就构成的当前应用的展示流程。

    这里其实也可以用new Function的方法来执行JavaScript

    而微应用切换的时候我们应该也可以模仿单核CPU的进程切换,先保存当前应用的镜像,然后再切换,等切换回来的时候再恢复镜像

  3. 当然这个流程里会涉及到CSS样式的污染以及JavaScript对全局对象的污染

对于路由分发而言,以采用vue-router开发的基座SPA应用来举例,主要是下面这个流程:

  1. 当浏览器的路径变化后,vue-router会监听hashchange或者popstate事件,从而获取到路由切换的时机。
  2. 最先接收到这个变化的是基座的router,通过查询注册信息可以获取到转发到那个微应用,经过一些逻辑处理后,采用修改hash方法或者pushState方法来路由信息推送给微应用的路由,微应用可以是手动监听hashchange或者popstate事件接收,或者采用React-router,vue-router接管路由,后面的逻辑就由微应用自己控制。

可以采取命名空间或者basePath的方式去做路由的分发判断

应用隔离

应用隔离问题主要分为主应用和微应用,微应用和微应用之间的JavaScript执行环境隔离,CSS样式隔离,我们先来说下CSS的隔离。

CSS隔离:当主应用和微应用同屏渲染时,就可能会有一些样式会相互污染,如果要彻底隔离CSS污染,可以采用CSS Module 或者命名空间的方式,给每个微应用模块以特定前缀,即可保证不会互相干扰,可以采用webpack的postcss插件,在打包时添加特定的前缀。

而对于微应用与微应用之间的CSS隔离就非常简单,在每次应用加载时,将该应用所有的link和style 内容进行标记。在应用卸载后,同步卸载页面上对应的link和style即可。

JavaScript隔离:每当微应用的JavaScript被加载并运行时,它的核心实际上是对全局对象Window的修改以及一些全局事件的改变,例如jQuery这个js运行后,会在Window上挂载一个window.$对象,对于其他库React,Vue也不例外。为此,需要在加载和卸载每个微应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。

沙箱机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象。通常在Node.js端可以采用vm模块,而对于浏览器,则需要结合with关键字和window.Proxy对象来实现浏览器端的沙箱。

消息通信

应用间通信有很多种方式,当然,要让多个分离的微应用之间要做到通信,本质上仍离不开中间媒介或者说全局对象。所以对于消息订阅(pub/sub)模式的通信机制是非常适用的,在基座应用中会定义事件中心Event,每个微应用分别来注册事件,当被触发事件时再有事件中心统一分发,这就构成了基本的通信机制。

如果采用的是React或者是Vue,是可以结合Redux和Vuex来一起使用,实现应用之间的通信。

待检索问题

  • with
  • sandbox
  • iframe的优缺点