1. 前言
随着Web应用程序规模的日益扩大和复杂性的增加,传统的前端开发模式逐渐显现出其在维护、扩展以及团队协作方面的局限性。微前端作为一种新兴的前端架构模式,正是为了应对这些挑战而诞生的。
微前端(Micro-Frontends)并没有定义框架或 API,它其实是一个类似微服务架构的概念,将微服务的概念扩展到了前端世界。其核心思想是将大型的前端应用拆分成多个小型、独立、可独立运行和部署的前端应用或服务(通常称为“微应用”或“微服务前端”),每个微应用都拥有自己独立的技术栈、开发团队和生命周期,但它们之间通过共享公共资源(如样式、组件等)来实现数据和状态的同步,通过定义好的接口和协议进行通信和协作,共同组成了一个完整的前端应用(尽管我们将前端应用拆分为多个项目,但它们最终还是会被集成到一个单页前端应用程序中)。
总结:微前端是一种前端架构模式,通过将单个应用程序分解为多个小型、独立的部分来实现应用程序的组合。每个小型部分都由独立的团队开发、测试和部署,然后将它们组合成为一个完整的应用程序。
2. 什么是微前端
微前端借鉴了微服务的架构理念,它既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活,有一个基座应用(主应用),来管理各个子应用的加载和卸载,所以微前端不是指具体的库,不是指具体的框架,不是指具体的工具,而是一种理想与架构模式,微前端的核心三大原则:独立运行、独立部署、独立开发。
核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。
3. 微前端解决了什么问题
它主要解决了几个问题:
-
随着项目迭代,应用越来越庞大,难以维护
项目规模不断增大,代码库不断膨胀,使得维护和扩展变得愈发困难。微前端将应用程序分解为多个小型、独立的部分,每个部分都可以独立扩展和维护。
-
跨团队或跨部门协作开发项目导致效率低下的问题
大型应用往往需要多个团队协同开发。使用微前端架构模式使得每个团队可以独立开发和维护自己的部分,无需担心与其他团队的技术冲突。
-
技术栈不一致的问题
不同的团队可能使用不同的技术栈来开发应用程序的不同部分。微前端架构模式允许使用不同的技术栈来开发每个微前端,从而避免了技术栈不一致的问题。
这几个问题在公司中很常见,一个成熟且稳定的大项目,一般都有一个庞大且臃肿的代码仓库,很多前端项目在经过多年不断迭代或项目交接之后,技术架构早已落后,新接手的人在此基础上做一些业务上的修改以及扩展一定是无可奈何、极其难受的。微前端可以让我们跳出这个陷阱,将多个项目融合为一,减少新旧项目的耦合,提升项目扩展性。相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更简单。
同时,许多企业在升级或重构Web应用时,都需要考虑与遗留系统的集成与兼容问题。微前端允许将遗留系统封装为微应用,与新开发的应用一起集成到统一的界面中,既保留了遗留系统的功能,又使得新开发的应用能够采用最新的技术和框架。
4. 为什么要用微前端
技术栈无关
主框架不限制接入子应用的技术栈,每个微应用都可以自主选择技术栈,具备完全自主权,降低了技术选型的难度和成本。每个微应用都独立运行,能够避免 DOM、CSS、JS 受到外部的影响,或者对其它应用产生影响。
独立开发/部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新,每个微前端都可以独立地进行部署,无需对整个应用进行整体部署,还可以独立地进行升级、更新甚至重写部分前端功能,不会影响其他应用,这有助于保持整个系统的稳定和可靠。
团队自治
微前端允许不同团队开发和维护不同的模块,并且允许不同团队使用各自擅长的技术栈进行独立开发,然后将这些的应用集成到一个统一的界面中,每个团队不需要了解彼此的任何信息,可以并行独立开发自己的应用,团队代码相互隔离,无需等待其他团队完成,从而实现团队自治。
构建更快
在构建打包上线时,项目越大,构建所需的时间就越长,通过将项目拆分成多个小项目,每个小项目都独立部署构建,从而实现了并行构建、缓存和增量构建等优化策略,这些策略共同作用,使得无论项目如何增长,每个项目都能快速构建。
增量平滑升级
在微前端架构中,可以逐个升级微应用,而无需将整个应用一起升级。在面对各种复杂场景时,通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略,这种增量升级的特性使得应用的更新更加灵活和可控,减少了升级过程中的风险。
模块化和独立性
将庞大的整体拆分成可控的小块,并明确他们之间的依赖关系,使得代码库更小、更内聚、可维护性更高。将大型应用拆分为多个小型应用,每个应用负责一个特定的功能或模块,这种模块化的设计使得开发者可以更加专注于某个功能的开发,提高开发效率。
独立运行
微应用之间运行时互不依赖,有独立的状态管理,每个子应用之间状态隔离,运行时状态不共享,应用之间的耦合度较低,可以独立地进行升级和修复,降低了对整个系统的影响。
可维护性和可扩展性
微前端架构通过将应用拆分成多个小型的、可独立部署的微应用,使得每个微应用都可以独立地进行升级和扩展,而无需影响整个应用。可以将原本运行已久、没有任何关联的几个应用融合为一个应用,或者将很多个小型单个应用融合为一个完整的应用,减少项目之间的耦合,提升项目扩展性。当需要添加新功能时,只需开发一个新的微应用并将其集成到主应用中,而无需修改现有的代码。
5. 使用微前端的风险点
增加了系统复杂度
尽管微前端可以解决大型项目的复杂性问题,但是它自身也带来了一些复杂性,比如需要管理和协调多个独立的应用。微前端需要对系统进行拆分,将单体应用拆分成多个独立的微前端应用,可能导致系统整体变得更加复杂,这主要体现在跨应用通信、状态管理、路由管理等方面,因为需要处理跨应用之间的通信和集成问题,在微前端应用之间共享状态可能会比较复杂,需要使用特殊的工具或模式。
需要依赖于额外的工具和技术
实现微前端需要使用一些额外的工具和技术,例如模块加载器、应用容器、路由管理器等。这些工具和技术需要额外的学习和维护成本,也可能会导致一些性能问题。
安全性问题
由于微前端应用是独立的,它们之间可能存在安全隐患,可能会增加跨域请求、数据泄露等安全问题。例如,如果某个微前端应用存在漏洞,攻击者可能会利用这个漏洞来攻击整个系统。
兼容性问题
由于微前端应用是独立的,不同的微前端应用可能使用不同的库、框架或技术栈,它们之间可能存在兼容性问题。例如,某个微前端应用可能使用了一些不兼容的依赖库,这可能会导致整个系统出现问题。如果不同的微前端应用使用了不同的库或框架,可能会导致加载和运行的性能问题。
开发团队需要有一定的技术水平
实现微前端需要开发团队有一定的技术水平,包括对模块化、代码复用、应用集成等方面有深入的了解。如果团队缺乏这方面的技能,可能会导致微前端实现出现问题。
6. 应用场景
微前端的应用场景主要体现在以下几个方面:
拆分大型Web应用
对于体量庞大的大型Web应用,随着项目体积越来越大,后续难以维护和扩展,打包时间越来越长。微前端架构能够将其拆解成多个可以独立开发、部署和运行的微型应用。这样不仅可以促进并行开发与快速迭代,还能降低开发和维护成本。
兼容历史应用与增量开发
一些非常老旧的项目,技术栈老旧、维护成本高且业务逻辑复杂,但一时半会又不能全部重构,这时就可以新创建一个新技术新项目的基座,把老项目的页面接入到新项目里面,后面新需求都在新项目里面开发就好,不用再动老项目。在需要兼容遗留系统的同时,使用新框架或技术去开发新功能时,微前端架构是一个理想的选择。遗留系统可以保持其原有的稳定性和功能性,而新开发的功能则可以使用最新的技术和框架,实现平滑过渡与增量升级。
应用聚合
大型互联网公司或企业内部通常会部署大量的应用和服务,这些应用和服务可能由不同的团队或部门开发,使用不同的技术和框架,为了向用户或员工提供统一、高效的体验,可以使用微前端技术将这些应用和服务高效聚合到一个统一的界面中,用户可以在一个统一的界面中访问和使用不同的应用和服务,提升体验与工作效率。
资源共享
基于多页的子应用缺乏管理,规范/标准不统一,无法统一控制视觉呈现、共享功能和依赖,会造成重复工作。在多页应用或分布式系统中,不同的应用之间可能存在可以共享的功能和服务,通过微前端架构,这些共享的功能和服务可以被封装成独立的模块或组件,并在不同的团队之间进行高质量的共享,从而促进跨团队功能复用,加速研发进程。
不同技术栈开发
前端技术更新太快,一个项目历经两三年也许就需要进行项目升级,甚至是切换技术栈,但仍需要老项目的代码。在一些大厂,经常会有跨部门和跨团队协作开发项目,团队技术栈不一,又需要保证同一项目的开发,这时我们可以使用微前端,每个团队或者每个部门单独维护自己的项目,我们只需要一个主项目来把分散的子项目汇集到一起即可,更容易地实现技术栈的升级和切换,以适应不断变化的技术环境。
7. 微前端的核心原理
微前端的核心原理是通通过一个主应用容器来管理并协调多个独立开发、部署和运行的微应用,通过定义统一的通信协议和API实现微应用之间的数据共享和交互,并通过构建工具和模块化技术实现资源的共享、提取和复用。
具体来说,微前端主要包括以下几个部分:
主应用
主应用作为系统的入口和核心,它扮演着“容器”或“宿主”的角色,负责加载、展示和管理各个微应用。主应用提供一个框架或容器,用于集成和展示各个微应用的内容。此外,主应用还需要提供一些基础设施服务,以确保微应用能够顺利运行和相互协作。这些基础设施服务可能包括:
-
路由管理:主应用负责整个系统的路由,包括微应用的路由注册、解析和跳转。这允许用户在不同的微应用之间导航,而无需重新加载整个页面
-
状态管理:为了保持微应用之间状态的一致性,主应用可能需要提供一个全局状态管理方案。这可以基于Redux、Vuex等状态管理库来实现,确保微应用能够共享和同步状态
-
生命周期管理:主应用需要管理微应用的加载、卸载和更新等生命周期事件,以确保微应用的正确加载和及时清理
微应用
微应用是主应用中的一个子应用,是微前端架构中的基本单元,它可以独立开发、部署和运行,它们封装了特定的业务功能或页面,并通过定义好的接口与主应用和其他微应用进行交互。微应用的特点包括:
-
独立性:每个微应用都是一个独立的实体,可以拥有自己的技术栈、构建工具和部署流程
-
封装性:微应用封装了特定的业务逻辑和界面,对外提供清晰的接口,隐藏内部实现细节
-
可替换性:由于微应用之间的耦合度较低,因此可以轻松地替换或升级某个微应用,而不会影响其他部分的运行
通信机制
为了实现微应用之间的数据共享和交互,需要定义一套统一的通信协议和API。这可以包括事件总线、消息队列、HTTP请求等机制,确保微应用之间的信息流通和协同工作。
-
事件总线(Event Bus):通过发布/订阅模式,微应用可以发布事件并通知其他感兴趣的微应用。这种方式适用于松耦合的通信场景
-
消息队列(Message Queue):对于需要异步处理或高可靠性的通信场景,可以使用消息队列来实现微应用之间的数据交换
-
HTTP请求:对于跨域或需要RESTful API的通信场景,微应用之间可以通过HTTP请求来交换数据
资源共享
通过构建工具(如Webpack、Rollup等)和模块化技术,实现资源的共享、提取和复用。这有助于减少重复代码和资源加载时间,提高整体应用的性能和用户体验。资源共享可以包括以下几个方面:
-
代码拆分(Code Splitting):将代码拆分成多个块(chunk),并在需要时按需加载。这有助于减少初始加载时间,提高应用的响应速度
-
公共依赖提取(Commons Chunk Extraction):将多个微应用之间共享的依赖库或模块提取到公共块中,并在多个微应用之间共享。这有助于减少重复加载和缓存利用
-
样式隔离(Style Isolation):通过CSS模块、CSS-in-JS等技术实现样式的封装和隔离,避免不同微应用之间的样式冲突
8. 微前端的架构模式
常见的微前端实现方式包括以下几种:
基于路由分发:不同的子应用负责不同的路由空间,根据用户访问的路由,加载对应的前端应用
Web Components 或 Custom Elements:利用标准化的Web组件技术封装各个子应用,将每个子应用设计成Web Components或其他框架的自定义组件,可以直接在DOM中插入并独立运行
iframe集成:每个子应用在一个单独的iframe中运行,通过消息传递机制进行通信,通过在主应用中嵌入多个iframe,每个iframe承载一个独立的子应用
动态加载JS模块:利用JavaScript模块加载器,按需加载不同团队编写的模块,使用模块加载器(如Webpack、SystemJS)将子应用分割成可动态加载的部分,主应用根据需要动态请求并执行子应用的代码
微应用沙箱:使用类似Shadow DOM或者其他隔离技术,在同一页面上运行多个互不干扰的前端环境。使用专门的微前端框架(如single-spa、qiankun等)来管理和调度多个微应用的生命周期,包括注册、加载、挂载、卸载等
容器化方案:通过容器化技术实现前端应用的模块化和共享,如Dockers、webpack模块联邦、single-spa等工具提供的微前端解决方案
微前端架构是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将将庞大的单体Web应用拆解成多个小巧、独立的前端应用(微应用),这些微应用能够协同工作,共同组成完整的用户界面。微前端架构主要关注前端应用的模块化和独立部署,利用浏览器端技术(诸如Web Components和主流JavaScript框架)来实现微应用之间的无缝集成,与后端的交互上,则通常依赖于API Gateway等后端服务来管理跨应用的API调用,确保数据流通的顺畅与安全。
基于浏览器端集成的微前端
这是微前端架构中最常见和直接的方式。它主要通过前端技术(如Web Components、JavaScript框架等)在浏览器端实现微应用的集成。
-
Web Components:利用浏览器原生的Web Components技术(Custom Elements、Shadow DOM、HTML Templates)来构建独立的微应用组件,确保样式和逻辑的封装与隔离。
-
JavaScript框架:使用React、Vue等现代JavaScript框架来构建微应用,并通过这些框架提供的组件化、路由等功能来实现微应用之间的集成和通信。
-
动态加载:通过动态加载技术(如ES Modules的import())按需加载微应用,实现按需加载和懒加载,提高应用性能和用户体验。
微前端与后端的交互
虽然微前端主要关注前端应用的模块化和独立部署,但它仍然需要与后端服务进行数据交互,即微前端应用如何与后端服务通信。
-
API Gateway:在微服务架构中,API Gateway通常用于聚合后端服务的接口,为前端提供统一的访问入口,简化了前端与多个后端服务之间的通信复杂度。微前端应用可以通过API Gateway与后端服务进行交互。
-
反向代理:如Nginx等反向代理服务器可用于处理HTTP请求,实现负载均衡、SSL加密等功能,但主要作为后端服务的入口,不直接参与微前端应用的集成逻辑,而是作为后端服务的入口点。
-
服务网格:服务网格(如Istio)主要用于微服务之间的通信和治理,虽然它可以改善服务的可靠性和可扩展性,但通常不直接用于微前端架构中微应用之间的集成。
9. 微前端项目管理方案
大多数的微服务体系都鼓励拆分的微前端项目独立的代码仓库、构建和部署。在微前端架构中,项目管理是确保各个微前端应用能够高效协同、独立开发并顺利整合的关键。以下是几种常见的微前端项目管理方案:
基座模式
-
通过一个中心化的主应用(基座)来管理所有子应用(微前端)。
-
主应用不仅负责应用的布局、路由管理和状态同步,还可能包含一些共享的业务功能,如用户认证、导航菜单等。子应用则作为独立的实体进行开发、构建和部署,它们通过主应用提供的集成点(如API、事件总线或自定义通信协议)与主应用进行交互。整体应用通过主应用进行整合和统一管理。
-
提供了统一的应用入口和布局管理,简化了子应用的集成过程,但要求设计好应用加载、卸载及状态管理的机制,主应用与子应用之间的通信机制和状态同步策略,确保子应用能够平滑地接入和退出系统。
适用场景:当微前端应用之间存在较强的交互和依赖关系时,或者需要统一处理用户认证、路由导航等全局功能时。
NPM包模式
-
每个微前端应用都被打包成独立的NPM包,并发布到NPM仓库中。便于版本控制、依赖管理和分发。
-
每个微前端应用都可以作为一个独立的NPM包,具有自己的版本控制和依赖管理。主应用通过NPM安装依赖的方式引入所需的微前端应用包,并在运行时动态加载这些包。这种方式要求微前端应用遵循一定的接口规范,以便主应用能够正确地加载和调用它们。
-
NPM包模式可以方便地进行微前端应用的分发、更新和依赖管理,但需要确保微前端应用的接口规范一致,避免版本冲突和依赖冲突。
适用场景:当微前端应用需要频繁更新,或者需要被多个项目共享时。
动态加载模块模式
-
这种模式下,微前端应用不是通过传统的路由跳转或者iframe嵌入的方式进行加载,而是通过动态加载模块的方式实现,允许主应用在运行时根据需要动态地加载和卸载微前端应用模块。
-
通常通过现代JavaScript框架提供的动态导入(如ES Modules的import())功能实现。主应用可以根据路由变化或用户操作动态地加载对应的微前端应用模块,并在不再需要时卸载它们。
-
动态加载模块模式提供了良好的按需加载能力,有助于提升应用的加载速度和性能,但要仔细控制模块加载和卸载的时机,以避免内存泄漏和性能问题。
适用场景:当应用规模较大,或者需要支持懒加载以提升用户体验时。
配置中心模式
-
通过一个集中的配置服务来管理微前端应用的配置信息(如路由、版本、依赖关系等)。
-
主应用或微服务网关从配置中心获取最新的配置信息,并根据这些信息来加载和整合微前端应用。
-
配置中心模式便于在运行时动态调整微前端应用的配置,支持快速迭代和灰度发布等场景。但同时,它也需要一个可靠、高效的配置服务来确保配置信息的准确性和实时性。
适用场景:当微前端应用需要频繁变更配置,或者需要支持多环境部署时。
服务网格与API网关
-
服务网格(如Istio):虽然主要用于微服务间的通信治理,但在微前端架构中,它可能不是直接相关的组件,而API网关仍然扮演着重要的角色。
-
API网关:在微前端架构中,API网关是前端与后端服务之间的关键桥梁。它负责请求的路由、认证、限流、熔断等功能,确保微前端应用能够高效、安全地与后端服务进行交互。
-
在复杂场景中,服务网格也可以被用来帮助优化微前端应用与后端服务之间的通信质量和安全性,但这通常不是微前端架构的核心关注点。
10. 微前端核心功能点
监听路由变化
监听路由变化是微前端架构中实现应用间导航和页面跳转的关键。通过监听路由的变化,微前端框架可以判断当前需要加载或卸载哪个子应用,允许主应用根据当前的URL路径来动态加载和渲染对应的子应用。实现方式:
-
监听 hash 路由变化:通过window.onhashchange事件监听URL中hash部分的变化。
-
监听 history 路由变化:使用history.pushState、history.replaceState等方法进行路由变化时,需要通过重写这些方法或使用window.onpopstate事件来监听路由变化。
-
监听到路由变化后,主应用会根据当前的路由路径去匹配对应的子应用,并加载和渲染该子应用。
子应用加载
子应用加载涉及到从服务器获取子应用的资源(如HTML、CSS、JS等),并在主应用中动态地插入和渲染这些资源。子应用加载的具体实现方式可能因框架或库的不同而有所差异,但通常包括以下几个步骤:
-
获取子应用的入口资源链接:根据当前路由确定需要加载的子应用,然后通过HTTP请求、fetch等方法获取子应用的HTML、CSS、JS等资源。
-
解析HTML:解析子应用的HTML模板,提取出其中的内联和外联资源(如CSS、JS)。
-
加载资源:加载提取出的CSS和JS资源,并插入到主应用的DOM中。
-
渲染子应用:将子应用的HTML模板插入到主应用指定的容器中,加载并执行子应用的JS脚本,初始化子应用的状态和逻辑。
HTML Entry
通常指的是通过加载一个远程的HTML文件,并解析其中的JavaScript、CSS等资源来渲染子应用的方式。这种方式需要处理HTML文档的加载、解析以及资源(如JS、CSS)的依赖关系,以确保子应用能够正确地渲染和执行。允许主应用动态地加载和渲染子应用的内容,同时可以实现较好的样式隔离和JS沙箱(作用域隔离),减少了应用之间的相互影响。HTML Entry方式通常需要配合微前端框架(如qiankun、single-spa等)来实现,这些框架会提供资源加载、沙箱隔离、路由管理、应用通信等功能,以简化开发过程。
子应用插槽绑定好一个dom节点,然后动态地插拔加载页面内容。首先importHTML的参数为需要加载的页面url,拿到后会先通过fetch方法读取页面内容。例如:
export const importHTML = url => {
const html = await fetch(currentApp.entry).then(res => res.text()
const template = document.createElement('div')
template.innerHTML = html
const scripts = template.querySelectAll('script')
const getExternalScripts = () => {
console.log('解析所有脚本: ', scripts)
}
const execScripts = () => {}
return {
template, // html 文本
getExternalScripts, // 获取 Script 脚本
execScripts, // 执行 Sript 脚本
}
}
最后的返回为一个对象,属性为:
-
template:处理过的html模板
-
assetPublicPath:静态资源地址
-
getExternalScripts:获取前面解析的脚本数组的方法
-
getExternalStyleSheets:获取前面解析的样式表数组的方法
-
execScripts:执行该模板文件中所有的 JS 脚本文件,并且可以指定脚本的作用域 - proxy 对象
注意:内联script一般会用eval去执行。
接下来是处理模板的逻辑,js脚本和css链接会通过特定的方法去执行请求,请求回来后经过一层处理后才会作用于子应用中,最后处理完了就会渲染到绑定的节点上。
IFrame
IFrame是一个HTML元素,它允许在当前页面中嵌入另一个HTML页面,并且这个嵌入的页面与主页面在DOM、样式、脚本等方面都是隔离的。它的加载和渲染是由浏览器直接处理的,简单省事,给个链接直接搞定,自带加载体系,JS、CSS隔离,但是弊端也很多,如内存和计算资源消耗大、事件冒泡不穿透到主文档树、路由状态丢失、样式和布局限制等。
JavaScript Entry
可以将其理解为一种通过JavaScript的动态加载和执行机制来加载和渲染子应用的方式,这种方式主要关注的不是HTML或IFrame的完整页面加载(不依赖于完整的HTML页面加载),而是直接加载和执行子应用的JavaScript代码。这种方式需要更多的开发工作和配置,因为需要手动处理JavaScript代码的加载、解析和执行过程。
具体来说,JavaScript Entry可能指的是以下几种情况之一:
-
动态加载JavaScript模块:主应用通过JavaScript的模块加载机制(如ES Modules、RequireJS、SystemJS等)动态地加载子应用的JavaScript代码。这些代码可能是构建好的模块,也可能是通过代码分割(Code Splitting)技术从大型应用中分割出来的部分。加载后,这些JavaScript模块将负责渲染子应用的内容。
-
JavaScript SDK:子应用提供一个JavaScript SDK,主应用通过引入这个SDK来加载和渲染子应用。SDK内部可能封装了子应用的初始化逻辑、UI组件渲染、事件处理等功能。
-
Web Components:虽然Web Components本身不是一种微前端架构,但它们提供了一种封装独立、可复用的UI组件的方式。在这种情况下,子应用可以被封装成一个或多个Web Components,主应用通过引入这些组件的JavaScript和HTML定义来加载和渲染子应用。
-
远程渲染(Server-Side Rendering, SSR)后的JavaScript接管:虽然这种情况更多地与服务器端渲染相关,但也可以被视为一种JavaScript Entry的形式。子应用在服务器端渲染成HTML后发送到客户端,然后通过JavaScript(如React、Vue等框架的客户端渲染逻辑)接管页面的交互和动态更新。
应用隔离
在微前端架构中,应用隔离是至关重要的,以确保不同技术栈或团队的子应用能够安全、独立地运行在同一个主应用中,而不会相互干扰。
JS沙箱(作用域隔离)
通过JavaScript沙箱技术(如Proxy、WeakMap等)为每个子应用创建一个独立的执行环境,使得每个子应用都有自己的全局变量、函数、事件监听等,互不干扰。
每当微应用的 JavaScript 被加载并运行时,它的核心实际上是对全局对象 Window 的修改以及一些全局事件的改变,例如 jQuery 这个 js 运行后,会在 Window 上挂载一个 window.$ 对象,对于其他库 React,Vue 也不例外,为此,需要在加载和卸载每个微应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。
以下是几种常见的JS沙箱实现方式:
-
ProxySandbox
-
直接基于Proxy实现一个fakeWindow,直接完全隔离:使用ES6的Proxy API来拦截对全局对象(如window)的访问和修改,从而模拟出一个独立的“fakeWindow”对象
-
ProxySandbox 是最完备的沙箱模式,可以非常精细地控制子应用对全局状态的访问,完全隔离了主子应用的状态,是实现完全隔离的有效手段
-
-
Web Components隔离
-
使用Web Components技术(如Custom Elements、Shadow DOM)将子应用封装为自定义元素,每个自定义元素都拥有自己的DOM和样式作用域,从而实现应用间的隔离,但Shadow DOM并不能完全隔离JavaScript的执行环境
-
-
IFrame
-
天然的JS沙箱可以直接使用隐藏的Iframe来专门执行js
-
将每个子应用嵌入到独立的iframe中,每个iframe都是一个独立的浏览器上下文,从而实现应用间的隔离,但这种方式可能会带来性能问题和跨域通信的复杂性
-
-
worker
-
虽然Web Workers主要用于执行后台任务而不直接操作DOM,但可以将某些非UI相关的逻辑放入Worker中执行,以实现一定程度的隔离
-
每个子应用对应一个worker脚本,但由于Worker无法直接访问DOM,其适用范围有限
-
-
LegacySandbox
-
手动管理一个与DOM分隔开的对象,用于存储子应用的状态或全局变量,动态地记录和保存
-
不依赖于特定的技术或框架,但需要手动管理沙箱对象,容易出错,难以确保所有对全局状态的访问都通过沙箱对象进行
-
-
SnapshotSandbox
-
使用Proxy对象来“拦截”对全局对象(如
window
)的访问和修改 -
有污染window的风险,实现更加复杂
-
除了window上的东西还有一些需要相互隔离的副作用,比如事件监听、定时器等。监听劫持主要做了以下处理:
patchTimer(计时器劫持)
patchWindowListener(window 事件监听劫持)
patchHistoryListener(路由监听)
CSS沙箱(样式隔离)
由于在微前端场景下,在微前端架构中,由于不同技术栈的子应用可能共享同一个运行时环境,因此需要确保它们的样式不会相互干扰。
例如:一个团队的微应用的样式表为 h2 { color: black; }
,而另一个团队的微应用则为 h2 { color: blue; }
,而这两个选择器都附加在同一页面上,就会冲突;
-
使用CSS作用域前缀
-
通过在每个子应用的根元素上添加一个唯一的类名,并在子应用的CSS样式中使用这个类名作为选择器的前缀,从而限制样式的作用范围
-
-
CSS-in-JS库
-
如styled-components、emotion等,这些库允许我们编写组件级别的样式,并通过生成唯一的类名来避免样式冲突
-
-
Shadow DOM
-
与JS沙箱中的用法类似,Shadow DOM不仅可以封装DOM结构,还可以封装样式,在它内部定义的样式不会影响到外部DOM,从而实现样式隔离
-
跨应用通信
在微前端架构中,各个微前端应用之间需要进行通信和协作,以实现整体的功能和业务流程,因此,微前端中,允许不同子应用之间进行数据交换和事件通知的通信机制是非常重要的。
一般而言,我们建议让微应用之间尽可能少地交流,因为这通常会重新引入我们最初试图避免的那种不适当的耦合代码。也就是说,通常我们只需要某种程度的跨应用通信即可。
主要方案:
-
事件总线(Event Bus发布订阅模式):主应用可以创建一个事件总线,用于发布和订阅事件,各个子应用可以通过订阅事件的方式获取其他子应用发送的消息,也可以通过发布事件的方式向其他子应用发送消息
-
Ajax/HTTP请求:一个微应用可以向主应用发送请求,主应用可以将请求转发给其他微应用,并将响应返回给发起请求的微应用
-
全局状态管理:主应用可以提供一个全局状态管理的机制(如Redux、Vuex等),各个子应用可以通过读取和修改全局状态的方式进行通信
-
Web组件通信:基座应用可以通过props向子应用的Web组件注入数据和方法,子应用可以通过监听属性变化或触发事件的方式来获取或发送数据
-
window 通信:由于在设计上子应用运行的iframe的src和主应用是同域的,所以相互可以直接通信
-
URL参数传递:不同子应用之间可以通过URL参数的方式进行通信。主应用可以将需要传递的数据作为URL参数添加到子应用的URL中,其他子应用可以通过解析URL参数来获取数据
-
消息队列:异步传递消息,实现高效、解耦的数据交换,适用于高并发场景
常见的通信方式:
使用 自定义事件通信,是降低耦合的一种好方法
可以考虑 React 或 Vue 应用中常见的 全局 state store 机制
发布-订阅(pub/sub)模式的通信机制
使用 地址栏作为通信机制
11. 微前端需要考虑的问题
使用具体的微前端框架时,需要考虑多个方面的问题来确保应用的稳定性、性能以及可维护性,主要考虑的点有:
应用是否支持保活
实际上就是路由状态管理和Future State 问题,即如何在应用重新加载或路由切换时保持子应用的状态。
解决策略:
-
可以使用路由懒加载和状态持久化技术(如localStorage、sessionStorage或服务端状态管理)来保存子应用的状态,确保子应用在重新加载或路由切换时能够恢复到之前的状态,同时,主应用需要能够感知子应用的状态变化,并相应地调整路由或状态
-
在路由变化时,通过主应用拦截并恢复子应用的状态
-
考虑使用SPA(单页应用)路由管理库来管理路由状态,确保子应用之间的无缝切换
问题:浏览器重新刷新时,主框架的资源会被重新加载,主应用的路由系统已激活, 但子应用的资源可能还没有完全加载,从而导致路由没有匹配到规则,此时就会导致跳NotFound页或者直接路由报错。
方法:设计一套路由机制,框架需要先加载entry资源,待entry资源加载完毕,确保子应用的路由系统注册进主框架之后,再去由子应用的路由系统接管url change事件。子应用路由切出时,主框架触发响应的destroy事件,子应用在坚听到该事件时,调用自己的卸载方法卸载应用。
组合模式App Entry
主框架和子应用集成的方式,是在构建时组合还是运行时组合?构建时组合适合于那些对性能要求极高、子应用改动不频繁的场景;运行时组合则更加灵活,适合子应用独立性强、技术栈多样、频繁更新迭代的场景。
构建时组合:
-
在构建阶段将主框架与子应用合并为一个整体包。子应用通过 Package Registry(可以是 npm package,也可以是 git tags 等)的方式,与主应用一起打包发布。
-
优点:提升加载速度,简化部署。主应用、子应用之间可以做打包优化如依赖共享等
-
缺点:技术栈限制,灵活性差。子应用与主应用之间工具链耦合,子应用发布依赖主应用重新打包发布
-
可以通过Webpack的插件或脚本将主应用和子应用打包成一个整体
运行时组合:
-
子应用自己构建打包,主框架在运行时根据需要动态加载子应用资源
-
优点:主应用与子应用之间完全解耦,子应用完全技术栈无关,独立开发部署,灵活性高
-
缺点:可能增加加载时间和性能开销,系统复杂性增加。会多出一些运行时的复杂度和 overhead
-
主应用通过动态加载(如使用SystemJS、ES Module动态导入等)来加载子应用
入口加载HTML Entry 还是 JS Entry
子应用是通过HTML文件还是JavaScript文件加载?HTML Entry 适用于子应用需要完全独立开发和部署的情况,每个子应用都有自己的HTML模板;JS Entry 更适合那些希望利用构建时优化(如代码拆分、公共依赖提取)的场景。
HTML Entry:
-
可以使用iframe或类似技术加载子应用的HTML页面,但需注意性能和跨域问题
-
优点:子应用开发、发布完全独立。子应用具备与独立应用开发时致的开发体验
-
缺点:多一次请求,子应用资源解析消耗转移到运行时。主子应用不处于同一个构建环境,无法利用 bundler 的一些构建期的优化能力,如公共依赖抽取等。
JS Entry:
-
主应用为子应用预留DOM容器,并在运行时动态加载子应用的JavaScript文件
-
优点:主子应用使用同一个 bundler,可以方便做构建时优化
-
缺点:子应用的发布需要主应用重新打包。主应用需为每个子应用预留一个容器节点,且该节点id 需与子应用的容器 id 保持一致。子应用各类资源需要一起打包成一个bundle,资源加载效率变低
模块导入
使用 UMD 模块格式打包子应用,那么如何在浏览器运行时获取远程脚本中导出的模块引用。
解决策略:
-
使用ES Module的import()语法进行动态导入。
-
可以使用动态import()语法或RequireJS等模块加载器来异步加载模块
-
可以在全局作用域中注册模块,但需要注意避免全局命名冲突
css 隔离
在微前端场景下,不同技术栈的子应用会被集成到同一个运行时中,所以必须在框架层确保各个子主应用之间不会出现样式互相干扰的问题。避免不同子应用之间样式附加在同一页面上产生冲突。 例如:一个团队的微应用的样式表为 h2 { color: black; },而另一个团队的微应用则为 h2 { color: blue; },而这两个选择器都附加在同一页面上,就会冲突; 为了避免这个问题,常见的解决方案有:
-
CSS命名约定(如BEM):通过严格的命名规范来减少样式冲突
-
CSS Module:为每个类名生成唯一的标识符,避免全局冲突
-
CSS-in-JS:如styled-components、emotion等,可以在组件级别定义样式,避免全局污染
-
Shadow DOM:将每个子应用包裹到一个Shadow DOM中,为子应用创建一个封闭的DOM环境,其中的样式不会影响到外部,保证其运行时的样式的隔离
-
Dynamic Stylesheet (动态样式表 ):应用切出卸载后,同时卸载掉其样式表(浏览器会对所有的样式表的插入、移除做整个CSSOM的重构,从而达到插入,卸载样式的目的)
js 沙箱
避免不同微前端应用之间因为加载和执行脚本而导致的冲突和干扰,确保各个子应用之间的全局变量不会互相干扰,从而保证每个子应用之间的隔离。
解决策略:
-
Proxy代理:利用ES6的Proxy特性,对全局对象进行拦截和修改,避免直接修改全局状态
-
沙箱快照:在加载子应用前保存全局状态的快照,卸载时恢复快照
-
使用Web Workers在后台线程中运行代码(但注意Web Workers的限制)
-
给一些全局副作用变量添加前缀从而避免冲突
-
微前端框架提供的内置沙箱隔离机制
运行时js沙箱
即在应用的bootstrap和mount两个生命周期开始之前分别给全局状态打快照。
应用切出/卸载时,将状态回滚至bootstrap开始之前的阶段,确保对全局状态污染清零。
而应用二次进入时,则在恢复至mount前的状态,确保应用在remount时拥有跟第一次mount时一致的全局上下文
应用间的通讯
如何实现子应用之间的通信以及子应用与主应用之间的通信?在微前端架构中,每个微前端应用都是独立的,这些微前端应用之间又需要相互通信以协同工作,比如共享数据、触发事件等。一般而言,建议让微应用之间尽可能少地交流,因为这通常会重新引入我们最初试图避免的那种不适当的耦合代码。也就是说,通常我们只需要某种程度的跨应用通信即可。
常见的通信方式:
-
自定义事件:通过监听和触发自定义事件来传递数据或指令
-
全局状态管理:如Redux、MobX等,主应用维护一个全局状态,子应用通过读取和修改这个状态来进行通信
-
发布-订阅模式:如使用事件总线(EventBus)来解耦不同应用之间的通信
-
地址栏通信:通过修改URL的hash或query参数来传递信息,但这种方式通常用于页面间的通信
-
使用微前端框架提供的通信API(如single-spa的custom-props、custom-events等)
应用间的状态管理
如何管理跨应用的状态。
解决方案:
-
使用Redux、MobX等全局状态管理库,在主应用中维护一个全局状态树
-
子应用可以通过特定的API(如Redux的dispatch和subscribe)来读取和修改状态
-
使用localStorage、sessionStorage或服务端状态管理来持久化状态
-
通过主应用来协调子应用之间的状态同步
-
需要确保全局状态的安全性和一致性,避免数据冲突和脏读问题
公共依赖的处理
如何避免公共依赖的重复加载和版本冲突?在微前端架构中,每个微前端应用可能都会依赖一些公共的库或框架。如果管理不好可能导致依赖重复加载和版本重提等问题。
-
重复加载:如果每个微前端应用都自行加载自己所需的公共依赖,那么这些依赖可能会被多次加载到浏览器中,导致资源的浪费和性能的下降
-
版本冲突:不同的微前端应用可能依赖相同公共依赖的不同版本,这可能导致版本冲突和不可预测的行为
-
开发效率低下:开发人员需要在每个微前端应用中单独管理公共依赖,这会增加开发成本和维护成本
解决策略:
-
在构建时通过Webpack等构建工具进行依赖分析和去重
-
使用CDN来加载公共依赖,确保版本一致性
-
在主应用中统一管理公共依赖的版本和加载
预加载
如何提高应用的加载速度和用户体验?
解决策略:
-
在主应用的HTML中预先加载所有微前端应用可能用到的公共依赖
-
在用户导航到下一个页面之前,使用preload或prefetch标签来预加载资源
-
在主应用中根据用户行为预测性地加载子应用资源
-
使用浏览器缓存(如HTTP缓存)来存储和复用资源
-
使用代码拆分和懒加载来优化资源加载,将应用拆分成多个块,并延迟加载非关键资源
路由管理
如何确保主应用和子应用之间的路由能够无缝衔接,特别是在子应用之间跳转时,主应用能够正确处理并渲染新的子应用。处理子应用内的路由,以确保在子应用内部导航时不会意外地卸载和重新加载子应用。
解决方案:
-
中心化路由配置:在主应用中维护一个全局路由表,管理所有子应用的路由
-
路由劫持与转发:主应用监听URL变化,根据路由表将请求转发给相应的子应用
-
路由前缀:为每个子应用分配独特的路由前缀,确保路由的唯一性
-
无缝切换:在子应用之间切换时,保持UI的流畅性,避免用户感知到明显的加载或卸载过程
-
子应用内部路由:子应用内部使用独立的路由管理机制,确保内部导航不会影响到主应用或其他子应用
参考资料:
前端老赵一次给你讲透“微前端”架构 - 前端老赵 - 博客园 (cnblogs.com)