Micro-Frontends
需求
各个项目上云之后,每一个项目作为一个【微前端的应用】是独立开发的,通过权限确定允许使用的模块,各个应用中的模块可以同时运行在云平台中。
定义
微前端架构可以理解为使用不同 JavaScript
框架为多个团队构建现代 Web 应用程序的技术,策略和方法。
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。
微前端的价值
- 技术栈无关:主框架不限制接入应用的技术栈,子应用具备完全自主权。
- 独立开发、独立部署:子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级
- 独立运行时:每个子应用之间状态隔离,运行时状态不共享
微前端可以解决的问题
- 有利于技术迭代升级,新需求采用新的框架开发,老的页面仅仅做维护。
- 一个系统由多个子模块整合,一般会由一个大团队维护前端相关,使用微前端可以按照业务应用来拆分开发团队,避免效率的天花板的问题。
- 使用同一个git仓库的跨多团队合作开发造成的困难,如代码合并冲突、模块冲突。
- 已有的页面中加入另一个团队实现的业务,微前端也是一个比较好的方案。
- 无需同时升级整个项目。
- 项目分离,运营聚合。
架构调整的关注点
- 打包速度
- 页面加载速度
- 多人多地协作
- SaaS产品定制化
- 产品拆分
微前端的现状
微前端,并不是一个新的技术名词,在 2016 年就被提出来了,它是由后端微服务启发而提出来的。目前各个大厂都有相关的尝试和实践,但还是处于探索阶段,没有最佳的实践。
技术演进
MPA (MPA多页Web应用):方案的优点在于 部署简单、各应用之间硬隔离,天生具备技术栈无关、独立开发、独立部署的特性。缺点则也很明显,应用之间切换会造成浏览器重刷,由于产品域名之间相互跳转,流程体验上会存在断点。
SPA (SPA单页Web应用):则天生具备体验上的优势,应用直接无刷新切换,能极大的保证多产品之间流程操作串联时的流程性。缺点则在于各应用技术栈之间是强耦合的。
传统的前端 SPA 开发模式,一方面,随着系统迭代,发展到一定程度,规模已经非常庞大。通过项目内的模块化,已经无法解决业务膨胀的问题;另一方面,随着应用框架的升级、变迁,多框架多版本、同框架多版本共存的状态无法避免,必须要有一种方案,能对整个业务进行合理拆分、组合,所以微前端的思想应运而生。
微前端架构的优势,正是 MPA 与 SPA 架构优势的合集。即保证应用具备独立开发权的同时,又有将它们整合到一起保证产品完整的流程体验的能力。
Stitching layer
作为主框架的核心成员,充当调度者的角色,由它来决定在不同的条件下激活不同的子应用。因此主框架的定位则仅仅是:导航路由 + 资源加载框架。
技术方式
1 路由分发式
通过 HTTP 服务器的反向代理功能,来将请求路由到对应的应用上。
通过路由将不同的业务分发到不同的、独立前端应用上。其通常可以通过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。如下图所示:
路由分发式的架构应该是采用最多、最容易的 “微前端” 方案。
2 前端微服务化
在不同的框架之上设计通讯、加载机制,以在一个页面内加载对应的应用。
前端微服务化,是微服务架构在前端的实施,每个前端应用都是完全独立(技术栈、开发、部署、构建独立)、自主运行的,最后通过模块化的方式组合出完整的前端应用。
其架构如下图所示:
采用这种方式意味着,一个页面上同时存在二个及以上的前端应用在运行。目前,还没有看到比较完善的实践,一般都是Single-SPA实践方案。
3 微应用
通过软件工程的方式,在部署构建环境中,组合多个独立应用成一个单体应用。
微应用化,即在开发时,应用都是以单一、微小应用的形式存在,而在运行时,则通过构建系统合并这些应用,组合成一个新的应用。
微应用化更多的是以软件工程的方式,来完成前端应用的开发,因此又可以称之为组合式集成。对于一个大型的前端应用来说,采用的架构方式,往往会是通过业务作为主目录,而后在业务目录中放置相关的组件,同时拥有一些通用的共享模板。
4 微件化
开发一个新的构建系统,将部分业务功能构建成一个独立的 chunk 代码,使用时只需要远程加载即可。
微件(widget),指的是一段可以直接嵌入在应用上运行的代码,它由开发人员预先编译好,在加载时不需要再做任何修改或者编译。
而微前端下的微件化则指的是,每个业务团队编写自己的业务代码,并将编译好的代码部署(上传或者放置)到指定的服务器上,在运行时,我们只需要加载相应的业务模块即可。对应的,在更新代码的时候,我们只需要更新对应的模块即可。
下图便是微件化的架构示意图:
5 前端容器化
前端容器:iframe
通过将 iFrame 作为容器,来容纳其它前端应用。
WC 容器化
Web Components 是一套不同的技术,允许开发者创建可重用的定制元素(它们的功能封装在代码之外),并且在 Web 应用中使用它们。
目前困扰 Web Components 技术推广的主要因素,在于浏览器的支持程度。在 Chrome 和 Opera 浏览器上,对于 Web Components 支持良好。
工程
微前端工程可以划分为 portal 工程、业务工程、common 工程。
portal 工程
portal
,就是入口,即微前端加载器。当用户打开浏览器,首次进入我们的页面时,不管是什么 URL
,首先加载的就是portal
。portal
里会配置所有业务工程的地址、匹配哪些 URL、需要加载哪些资源。
业务工程
业务工程就是普通的微前端工程,一般一个模块一个工程。
公共依赖处理
大部分的业务工程可能都会有一些共同的依赖,比如 Vue
、moment
、lodash
等。如果将这些内容都打包到各自业务工程的 vendor.js
里,则势必会导致代码冗余太多,浪费带宽,还可能导致浏览器运行内存压力增大。我们可以把这些公共依赖、公共组件、CSS、Fonts 等都放到 portal 工程里,将依赖、组件 export,并以 UMD
的方式注入到全局。
主流的微应用架构
1 MPA + 路由分发
这种方式就是在多个独立的 SPA 应用之间跳转,通过把界面、导航、皮肤做成类似的样子,让用户感觉像是同一个应用。
优点:
- a. 框架无关;
- b. 独立开发、部署、运行;
- c. 应用之间 100% 隔离。
缺点:
- a. 应用之间的彻底割裂导致复用困难。(比如,每个应用左侧和顶部都带有导航,那么, 当我要把该应用在其他系统中复用时,需要对该子应用的导航做较为复杂的改动) ;
- b. 每个独立的 SPA 应用加载时间较长,容易出现白屏,影响用户体验;
- c. 后续如果要做同屏多应用,不便于扩展。
2 Single-SPA 通用中心路由基座式
子工程可以使用不同技术栈;子工程之间完全独立,无任何依赖;统一由基座工程进行管理,按照DOM节点的注册、挂载、卸载来完成。
主应用的代码,仅作为加载容器,管理子应用的生命周期。主应用捕获全局的路由事件,基于判断当前路由需要加载哪个子应用,然后 load
它。
即同一时刻,只有一个子应用被展示,子应用具备一个完整的应用生命周期。通常基于 url
的变化来做子应用的切换。
优点:
- a. 框架无关;
- b. 独立开发、部署、运行;
- c. 项目自由切割,应用可以自由组合,方便复用;
- d. 便于自由扩展功能。
缺点:
- a. 子应用需要实现
mount
、unmount
等钩子,侵入式的代码开发体验并不友好; - b. 全局污染和资源竞争。
3 Single-SPA,特定中心路由基座式-主从应用设计
子业务线之间使用相同技术栈;基座工程和子工程可以单独开发单独部署;子工程有能力复用基座工程的公共基建。
主应用会包含应用依赖的绝大多数环境,包括基础框架、基础组件与第三方依赖包,而子应用只会包含自己的一些业务代码。
主应用启动之后,基本就有了全套的运行时环境,子应用一般会把自己的路由注册到主应用中,并不接管系统路由,子应用更像是主应用的一个“路由模块”。
优点:
- 打包出来的子应用只包含了业务代码,体积小、加载快、用户体验好。
缺点:
- 基座就决定了它是框架强相关的,哪怕是基座的版本升级迭代,也会非常容易造成子应用
break change
;
4 传统 SPA + 组件化(比如 Web Components) + 私有 npm 源
把通用的一些业务功能发布成组件,通过私有 npm
的方式去维护和管理。其中,跟框架无关又比较有代表性的方案就属 Web Components
了。这种模式更像是业务组件,或者说业务模块,而不是应用。
同一时刻可展示多个子应用。通常使用 Web Components
方案来做子应用封装,子应用更像是一个业务组件而不是应用。
NPM式:子工程以NPM包的形式发布源码;打包构建发布还是由基座工程管理,打包时集成。
优点:
对现有项目渐进式增强,逐步改进
缺点:
- 着业务中组件数量的爆发式增加,组件粒度通信、组件的维护成本都急剧增加;
- 并不能做到真正的独立开发、测试、部署。
总结
类Single-SPA的方式的方案比较好
需要解决的问题
应用注册和发现
在微前端需要提供一个查找应用的机制,即服务的注册表模式。可以是一个固定值的配置文件,如 JSON
文件,又或者是一个可动态更新的配置,又或者是一种动态的服务。
- 应用发现:让主应用可以寻找到其它应用。
- 应用注册:即提供新的微前端应用,向应用注册表注册的功能。
- 第三方应用注册:即让第三方应用,可以接入到系统中。
- 访问权限等相关配置。
生命周期
微前端应用作为一个客户端应用,每个应用都拥有自己的生命周期:
- Load,决定加载哪个应用,并绑定生命周期
- bootstrap,获取静态资源
- Mount,安装应用,如创建 DOM 节点
- Unload,删除应用的生命周期
- Unmount,卸载应用,如删除 DOM 节点、取消事件绑定
路由
由于我们的子应用都是 lazy load
的,当浏览器重新刷新时,主框架的资源会被重新加载,同时异步 load
子应用的静态资源,由于此时主应用的路由系统已经激活,但子应用的资源可能还没有完全加载完毕,从而导致路由注册表里发现没有能匹配子应用 /subApp/123/detail
的规则,这时候就会导致跳 NotFound
页或者直接路由报错。
需要设计这样一套路由机制:
- 要确保子应用的路由系统注册进主框架之后后,由子应用的路由系统接管
url change
事件。 - 在子应用路由切出时,主框架需要触发相应的
destroy
事件,子应用在监听到该事件时,调用自己的卸载方法卸载应用。
社区比较完善的微前端路由相关实践 single-spa。
App Entry 主框架与子应用集成的方式
构建时组合 VS 运行时组合:子应用载入方式
构建时组合
子应用通过 Package Registry
(可以是 npm package
,也可以是 git tags
等其他方式) 的方式,与主应用一起打包发布。
优点:主应用、子应用之间可以做打包优化,如依赖共享等。
缺点:子应用与主应用之间产品工具链耦合。工具链也是技术栈的一部分。子应用每次发布依赖主应用重新打包发布。
运行时组合
子应用自己构建打包,主应用运行时动态加载子应用资源。
优点:主应用与子应用之间完全解耦,子应用完全技术栈无关
缺点:会多出一些运行时的复杂度和 overhead
真正的技术栈无关跟独立部署两个目标,大部分场景下我们需要使用运行时加载子应用这种方案。
JS Entry vs HTML Entry:子应用渲染入口
JS Entry
JS Entry 的方式通常是子应用将资源打成一个 entry script
优点:主子应用使用同一个 bundler,可以方便做构建时优化
缺点:
- 子应用的发布需要主应用重新打包
- 主应用需为每个子应用预留一个容器节点,且该节点 id 需与子应用的容器 id 保持一致
- 子应用各类资源需要一起打成一个 bundle,资源加载效率变低
HTML Entry
直接将子应用打出来 HTML
作为入口,主框架可以通过 fetch html
的方式获取子应用的静态资源,同时将 HTML document
作为子节点塞到主框架的容器中。
优点:
- 子应用开发、发布完全独立
- 子应用具备与独立应用开发时一致的开发体验
缺点:
- 多一次请求,子应用资源解析消耗转移到运行时
- 主子应用不处于同一个构建环境,无法利用
bundler
的一些构建期的优化能力,如公共依赖抽取等
模块导入
微前端架构下,我们需要获取到子应用暴露出的一些钩子引用,如 bootstrap
、mount
、unmout
等(参考 single-spa
),从而能对接入应用有一个完整的生命周期控制。而由于子应用通常又有集成部署、独立部署两种模式同时支持的需求,使得我们只能选择 umd
这种兼容性的模块格式打包我们的子应用。
UMD 叫做通用模块定义规范(Universal Module Definition)。也是随着大前端的趋势所诞生,它可以通过运行时或者编译时让同一个代码模块在使用
CommonJs
、CMD
甚至是AMD
的项目中运行。未来同一个 JavaScript 包运行在浏览器端、服务区端甚至是 APP 端都只需要遵守同一个写法就行了.
- 最简单的解法就是与子应用与主框架之间约定好一个全局变量,把导出的钩子引用挂载到这个全局变量上,然后主应用从这里面取生命周期函数。
- 通过
umd
包格式中的global export
方式获取子应用的导出。大体的思路是通过给window
变量打标记,记住每次最后添加的全局变量,这个变量一般就是应用export
后挂载到global
上的变量。实现方式可以参考 systemjs global import。
应用隔离
样式隔离
由于微前端场景下,不同技术栈的子应用会被集成到同一个运行时中,所以我们必须在框架层确保各个子应用之间不会出现样式互相干扰的问题。
1 Shadow DOM
Shadow DOM
:基于 Web Components
的 Shadow DOM
能力,我们可以将每个子应用包裹到一个 Shadow DOM
中,保证其运行时的样式的绝对隔离。
致命缺陷:由于子应用的样式作用域仅在 shadow 元素下,那么一旦子应用中出现运行时越界跑到外面构建 DOM 的场景,必定会导致构建出来的 DOM 无法应用子应用的样式的情况。例如: modal
是动态挂载到 document.body
的。
2 CSS Module BEM
通过约定 css
前缀的方式来避免样式冲突,即各个子应用使用特定的前缀来命名 class
,或者直接基于 css module
方案写样式
3 Dynamic Stylesheet
在应用切出/卸载后,同时卸载掉其样式表即可,原理是浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构,从而达到 插入、卸载 样式的目的。这样即能保证,在一个时间点里,只有一个应用的样式表是生效的。
<link rel="stylesheet" href="//xxxx.com/subapp.css">
JS隔离
JS沙箱
在应用的 bootstrap
及mount
两个生命周期开始之前分别给全局状态打下快照,然后当应用切出/卸载时,将状态回滚至 bootstrap
开始之前的阶段,确保应用对全局状态的污染全部清零。其他的还包括一些对全局事件监听的劫持等,以确保应用在切出之后,对全局事件的监听能得到完整的卸载,同时也会在 remount
时重新监听这些全局事件,从而模拟出与应用独立运行时一致的沙箱环境。
而当应用二次进入时则再恢复至 mount 前的状态的,从而确保应用在 remount
时拥有跟第一次 mount
时一致的全局上下文。
解决方案
方案1:运行时
工程划分为 portal 工程、业务工程、common 工程。
中心路由基座式-主从应用设计:
- 子业务线之间使用相同技术栈-
VUE
。 - 基座工程为项目工程模板,或进一步使用
npm
包的方式引入common
工程类库;项目开发使用git
拉取,在src
目录中使用项目名称为文件名创建新的文件夹,并在此文件夹下进行项目功能的开发。 - 基座工程和子工程可以单独开发单独部署。
- 基座工程发布为主站点,使用子工程资源发布端口获取子工程静态资源。
- 子工程可以发布为站点,也可以发布为资源。基座工程提供站点打包命令和模块资源发布打包命令。
- 使用基座路由系统,子应用把自己的路由注册到主应用中。
- 通过动态资源加载的方式,加载
Vue
组件资源。
关键技术:
- 应用注册和发现:云平台应用配置中心配置应用;云平台导航菜单配置具体的用于模块;云平台权限控制需要加载的模块;实现应用资源加载和
Vue
动态组件注入。 - 生命周期:集成为标准的
Vue APP
应用,使用Vue Component
的生命周期。 - 路由:使用基座工程的
Vue Router
,调整路由加载机制为动态加载。 App Entry
主框架与子应用集成:子应用构建Vue Component
组件资源包; 主框架运行时使用JS Entry
方式,异步动态加载子应用资源。- 模块导入:子应用使用
Vue Component
动态加载方式 - 应用隔离:云平台模块配置中心【组件唯一标识】保证应用路由的唯一性、保证页面组件隔离、由云平台基座工程提供通用控件保存控件组件的隔离;应用页面级组件创建自动添加应用名称
className
限定和CSS打包自动添加类名称限定的方式保证样式隔离。
优点:产品应用资源动态加载,后续在此方案下还可以做进一步的优化提升。 缺点:还需要对Vue
、and design pro vue
组件动态加载、打包等进行进一步验证,开发周期较长。
方案2:构建时
- 子业务线之间使用相同技术栈-
VUE
。 - 基座工程为项目工程模板;项目开发使用
git
拉取,在src
目录中使用项目名称为文件名创建新的文件夹,并在此文件夹下进行项目功能的开发。 - 基座工程和子工程可以单独开发单独部署。
- 子工程只发布项目创建的文件夹至
git
。 - 云平台(用户平台)构建整合。首先拉取平台库,通过产品配置文件,分别拉取各个产品库,复制到平台库
src
文件夹下,整合完成后,作为一个整体发布版本进行生产模式的文件打包发布。
关键技术:
- 应用注册和发现:云平台提供平台库和各产品库的配置文件,提供后续的整体构建使用。
- 应用隔离:云平台模块配置中心【组件唯一标识】保证应用路由的唯一性、保证页面组件隔离、由云平台基座工程提供通用控件保存控件组件的隔离;应用页面级组件创建自动添加应用名称
className
限定和CSS打包自动添加类名称限定的方式保证样式隔离。
优点:最简单实现现有需求的方式,对现有平台调整最小
缺点:平台、各产品的调整(新增、删除、更新)都需要重新进行重新构建打包
框架及参考代码
参考资料
Single-SPA 通用中心路由基座式-可能是你见过最完善的微前端解决方案