跳到主要内容

认识

一、认识


微前端 是一种前端架构模式,是一种将前端应用程序拆分为更小、更独立的部分,并使用不同的技术栈来构建和部署这些部分的方法。这些独立的部分可以是单个页面、组件、功能模块或应用程序。这种方法的目的是使团队能够更轻松地开发、测试、部署和维护前端应用程序,每个部分都可以使用不同的技术栈和独立的团队进行开发,这样可以更好地满足团队的需求和能力,同时还能够实现更高的可扩展性和灵活性。

传统 Web 应用的利与弊

在日常工作中需要使用到非常多的研发系统, 例如: 代码管理、代码构建、域名管理、应用发布、CDN 资源管理, 对象存储等。站在整个公司研发的角度考虑, 最好的产品形态就是将所有的研发系统都放置到同一个产品内,用户是无法感知他在使用不同的产品;对于用户而言,单个产品不存在割裂感,也不需要去学习多个平台,仅仅需要学习和了解当前平台即可。

其实不光是研发系统,比如电商系统也是一样的,都会存在大量的业务线,每一条业务线都会诞生大量的中台系统,并且还在指数增长。由于整个系统设计范围较广,在实际的研发过程中必然会以功能或业务需求垂直的切分成更小的子系统,切分成各种小系统后尽管由于分支的设计理念提升了开发者体验,但是在一定程度上降低了用户体验。那能否以一种新的架构模式,即保开发者体验,又能提升用户体验呢?

以研发系统和电商系统为例,各个业务线是研发系统和电商系统能力的一部分,而不是独立之间的孤岛。若以传统的前端研发模式进行开发,那么此时有两种项目设计策略:

  1. 将平台内多个系统放置到同一个代码仓库维护,采用 SPA 单页应用模式: 优势是统一的权限管控、统一的 Open API 开发能力;更好的代码复用、基础库复用;统一的运行管理能力;劣势是:代码权限管控的问题;项目构建时间长;需求发布相互阻塞;代码 commit 混乱、分支混乱;技术体系要求统一,无法快速引入新的技术体系提高生产力;无法同时灰度发布多条产品功能,无法局部灰度局部升级;代码回滚相互影响;错误监控无法细粒度拆分;

    因此,尽管降低了开发体验,如果对项目整体的代码拆分,懒加载控制得当,其实对于使用平台的用户而言体验确实提升的,这一切都归因于 SPA 应用带来的优势。SPA 应用跳转页面无需刷新整个页面,路由变化时,仅更新局部,不用让用户产生 MPA 应用切换时整个页面刷新带来的抖动感而降低体验,并且由于页面不刷新的特性可以极大程度的复用页面间的资源,降低切换页面时带来的性能损耗,用户也不会感知他在使用不同平台。

  2. 将系统分为多个仓库维护,在首页聚合所有平台入口,采用 MPA 多页面应用模式: 优势是可以以项目维度拆分代码,解决权限管理问题;仅构建本项目代码,构建速度快;可以使用不同的技术体系;不存在同一个仓库维护时的 commit 混乱和分支错乱等问题;功能灰度互不影响;劣势是:用户在使用时体验割裂,会在不同平台间跳转,无法达到 SPA 应用带来的用户体验;只能以页面维度拆分,无法拆分至区块部分,只能以业务维度划分;多系统同灰度策略困难;公共包基础库重复加载;不同系统间不可以直接通信;公共部分只能每个系统独立实现,同一运维通知困难;产品权限无法进行统一收敛;

    因此,这个方案一定程度上提升了开发体验,但却降低了用户体验,研发在日常开发工作中,需要使用大量的平台,但是却需要跳转到不同的平台上进行日常的研发工作,整体使用体验较差。体验较差的原因在于将由于通过项目维度拆分了整体研发中台,使各个产品之间成为独立的孤岛,系统间相互跳转都是传统意义上的 MPA,跳转需要重新加载整个页面资源,除了性能是远不如 SPA 应用的,并且应用间无法直接通信,这就进一步增强了用户在使用产品时的割裂感。

基于以上两种方案, 传统的分而治之策略已经无法应对现在 Web 应用的复杂性,因此衍生出微前端这样一种新的架构模式,与后端微服务相同,它同样是延续了分而治之的设计模式,不过却以全新的方法来实现。

微前端 它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发独立运行独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。

微前端 的核心价值在于 技术栈无关, 这是它诞生的理由。它主要解决了两个问题:

  1. 随着项目迭代应用越来越庞大,难以维护

  2. 跨团队或跨部门协作开发项目导致效率低下的问题

二、优势


2.1 解耦

微前端 业务之间低耦合,互不影响,方便升级,更新,重构/重写,尝试不同的技术。业务内部功能高内聚,业务纵向分离,独立自治。而不是像单体应用那样各个业务放在一起,大家用统一的技术栈,共用一个开发部署流程。

2.2 同步更新

对比npm包方式抽离,让我们意识到更新流程和效率的重要性,微前端由于是多个子应用的聚合,如果多个业务应用依赖同一个服务应用的功能模块,只需要更新服务应用,其他业务应用就可以立马更新,从而缩短了更新流程和节约了更新成本。

2.3 增量升级

迁移是一项非常耗时且艰难的任务,比如有一个管理系统使用AngularJS开发维护已经有三年时间,但是随着时间的推移和团队成员的变更,无论从开发成本还是用人需求上,AngularJS已经不能满足要求,于是团队想要更新技术栈,想在其他框架中实现新的需求,但是现有项目怎么办?直接迁移是不可能的,在新的框架中完全重写也不太现实。

使用微前端架构就可以解决问题,在保留原有项目的同时,可以完全使用新的框架开发新的需求,然后再使用微前端架构将旧的项目和新的项目进行整合,这样既可以使产品得到更好的用户体验,也可以使团队成员在技术上得到进步,产品开发成本也降到的最低。

2.4 颗粒度小

微前端 业务级或更小的粒度,一个业务团队可以独立管理自己的业务代码,更容易维护,同时方便独立建立 CI/CD 流水线,独立部署。而不是像单体应用是一个庞大的代码库。

2.5 技术栈无关

微前端的核心价值--技术栈无关, 主框架不限制接入应用的技术栈,子应用具备完全自主权

2.6 独立运行时

独立运行时 每个子应用之间状态隔离,运行时状态不共享

2.7 独立团队决策

因为微前端架构与框架无关,当一个应用由多个团队进行开发时,每个团队都可以使用自己擅长的技术栈进行开发,也就是它允许适当的让团队决策使用哪种技术,从而使团队协作变得不再僵硬。

2.8 独立部署与发布

独立开发、独立部署 子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

在目前的单页应用架构中,使用组件构建用户界面,应用中的每个组件或功能开发完成或者bug修复完成后,每次都需要对整个产品重新进行构建和发布,任务耗时操作上也比较繁琐。

在使用了微前端架构后,可以将不能的功能模块拆分成独立的应用,此时功能模块就可以单独构建单独发布了,构建时间也会变得非常快,应用发布后不需要更改其他内容应用就会自动更新,这意味着你可以进行频繁的构建发布操作了。

三、方案


3.1 Iframe

3.2 QianKun

3.3 MicroApp

3.4 Single-Spa

四、架构


任何一个 微前端 框架都应该提供以下能力:

  1. 加载应用

  2. 生命周期管理:

  3. 沙箱隔离:

  4. 样式隔离:

  5. 通信机制

  6. 路由管理

4.1 加载应用

微前端加载器应具备以下能力:

  1. 异步加载组件资源

  2. 可以预加载资源

  3. 可以缓存组件资源

  4. 可以缓存组件实例

加载器Loader: 注册应用,加载和解析应用入口资源,并提供预加载能力,解析应用导出的内容

  1. JS 入口类型: 通过动态创建 script 标签的方式加载子应用的入口文件,提供基础的 DOM 容器

  2. HTML 入口类型: 通过创建一个 <link> 标签来加载子应用的 HTML 入口文件,拆解 HTML DOMScriptStyle

4.2 生命周期管理

每个子应用都需要暴露出 bootstrapmountunmount 三个生命周期函数。bootstrap 函数在应用加载时被调用,mount 函数在应用启动时被调用,unmount 函数在应用卸载时被调用

4.3 沙箱隔离

通过 Proxy 对象创建了一个 JavaScript 沙箱,用于隔离子应用的全局变量,防止子应用之间的全局变量污染

4.4 样式隔离

通过动态添加和移除样式标签的方式实现了样式隔离。当子应用启动时,会动态添加子应用的样式标签,当子应用卸载时,会移除子应用的样式标签。

4.5 通信机制

4.6 路由管理

微前端应用拆分成子应用后,子应用路由应具备自治能力,可以充分的利用解耦后的开发优势,但与之对应的是应用间的路由可能会发生冲突,两种路由模式下可能产生用户难以理解的路由状态、无法激活不同的前端框架下带来的试图无法更新等问题。因此,主要提供了三种策略:

  1. 提供 Router Map, 自动化完成子应用的调度, 降低开发者理解成本

    XXX.run({
    domGetter: "#submodule",
    apps: [
    {
    name: "vue-app",
    activeWhen: '/vue-app',
    entry: "http//localhost:9090"
    },
    {
    name: "react-app",
    activeWhen: '/react-app',
    entry: "http//localhost:9091"
    }
    ]
    })
  2. 为不同子应用提供不同的 basename 用于隔离应用间的路由抢占问题: 当应用处于激活状态时, 根据应用的激活状态条件自动计算出应用所需的基础路径,并在渲染时告诉框架,以便于应用路由不发生冲突。

    export function provider({ dom,basename }){
    return {
    render(){
    ReactDOM.render(<App basename={basename} />);
    },
    destroy(){
    ReactDOM.unmountComponentAtNode(dom.querySelector("#app"));
    }
    }
    }
  3. 路由发生变化时能准确激活并触发应用试图更新: 点击某个子应用, 手动触发对应路由的 push 函数, 并监听路由变化, 每当路由变化时, 开始更新对应的子应用视图。

4.7 应用接口代理

参考资料