微前端总结
微前端总结
2023-10-23|最后更新: 2024-2-5
type
status
date
slug
summary
tags
category
icon
password
Blocking
Blocked by
top
URL
Sub-item
Parent item

概念

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行独立开发独立部署

基本原理

监听路由,当路由切换,加载对应应用的Entry,让其跑在容器里。

解决痛点

  • 拆分细化:当前前端的项目通常以spa的形态出现,即一个应用所有的相关页面都在一个项目里面。随着项目的不断迭代,维护成本也会越来越高,很容易出现“牵一发而动全身”的窘境。微前端的意义可以拆分项目,细化为若干个可以单独部署的子项目,子项目独立运行独立开发独立部署
  • 兼容技术栈:在传统的前端开发中,技术栈不兼容也是一个头疼的问题,我们经常会因为技术栈不兼容而做出让步。而在微前端的结构下,不同技术栈的子项目可以和谐共存减小迁移成本
  • 新旧项目整合:就是可以低成本地整合已有项目。因为子项目是独立的,因此不需要太大的工作量,就可以将已有的项目迁移至新的项目

特点

  • 技术栈无关 主框架不限制接入应用的技术栈,子应用可自主选择技术栈
  • 独立开发/部署 各个团队之间仓库独立,单独部署,互不依赖
  • 增量升级 当一个应用庞大之后,技术升级或重构相当麻烦,而微应用具备渐进式升级的特性
  • 独立运行时 微应用之间运行时互不依赖,有独立的状态管理
  • 提升效率 应用越庞大,越难以维护,协作效率越低下。微应用可以很好拆分,提升效率

技术选型

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

微服务功能设计

 
notion image
notion image
notion image
 
 

缺点

  1. 性能问题: 如果不同的微前端应用使用了不同的库或框架,可能会导致加载和运行的性能问题。
  1. 一致性: 保持不同的微前端应用在用户体验、设计和行为上的一致性可能会比较困难。一般引入微前端后,都需要对导航栏进行改造,使用宿主应用导航栏。
  1. 状态共享: 在微前端应用之间共享状态可能会比较复杂,需要使用特殊的工具或模式。
  1. 复杂性: 尽管微前端可以解决大型项目的复杂性问题,但是它自身也带来了一些复杂性,比如需要管理和协调多个独立的应用,避免每个应用的路由冲突,如何共用一些通用的依赖库,如何解决js、css隔离。
  1. 安全性: 微前端架构可能会增加跨域等安全问题。
目前市面有各类不同的微前端方案,但没有完美的解决方案。微前端方案通常需要考虑:应用加载机制、通信机制、代码隔离机制等问题。
 
 
notion image
 

工程化问题

共享

  1. 工具共享
  1. 状态共享
  1. 依赖共享
  1. 消息通信
    1. 父子通信
    2. 子子通信

隔离

  • 样式隔离
  • js隔离,全局变量和事件
  • storage隔离

性能

  • 按需加载
  • 预加载
  • 多子应用共存、子应用嵌套
  • 缓存子应用
  • tree-sharking
 

基座应用

下微前端主要采用的是组合式应用路由方案,该方案的核心是“主从”思想,即包括一个基座(MainApp)应用和若干个微(MicroApp)应用,基座应用大多数是一个前端SPA项目,主要负责应用注册,路由映射,消息下发等,而微应用是独立前端项目,这些项目不限于采用React,Vue,Angular或者JQuery开发,每个微应用注册到基座应用中,由基座进行管理,但是如果脱离基座也是可以单独访问,

架构图

 
notion image
 

基座引用功能

  • 权限控制
  • 路由控制
  • 全局方法注入
  • 异常处理
  • 菜单空盒子
  • 请求拦截,处理异常状态码,如果监听到 401 状态码,则跳转到登录页面进行授权。
  • 全局样式,例如主题色
  • 状态管理

子应用注入方式

Entry 用于父应用引入子应用相应的资源文件(包括 JSCSS),主要分为两种方式:
  • JS Entry
  • HTML Entry

JS Entry 方式

JS Entry 的原理是:
  1. CSS 打包进 JS,生成一个 manifest.json 配置文件
  1. manifest.json 中标识了子应用资源文件的相对路径地址
  1. 主应用通过插入 script 标签 src 属性的方式加载子应用资源文件(子应用域名 + manifest.json 中的相对路径地址)
基于这样的原理,因此 JS Entry 有缺陷:
  • 打包时,需要额外对工程化代码做修改,生成一份资源配置文件 manifest.json 给主应用加载
  • 打包时,需要额外对样式打包做修改,需要把 CSS 打包进 JS 中,也增加了编译后的包体积
  • 打包时,不能在 html 中插入行内 script 代码。因为 manifest.json 中只能存放地址路径。因此要禁止 webpack 把配置代码直接打入 html
  • 父子应用域名不一致,父应用加载子应用 manifest.json 会发生跨域,需要额外处理

HTML Entry 方式

HTML Entry 是利用 import-html-entry 直接获取子应用 html 文件,解析 html 文件中的资源加载入主应用

HTML Entry 优于 JS Entry 的地方

  1. 不用生成额外的 manifest.json
  1. 不用把 css 打包进 js 中
  1. 全局 css 独立打包,不会冗余
  1. 不使用生成 script 的方式插入子应用 JS 代码,不会生成额外的 DOM 节点

微前端方案

  • iframe(天然的微前端方案,但是弊端很多)
  • single-spa
  • web components(最适合但是兼容性最差)

iframe

优点

  • 浏览器原生的硬隔离方案,改造成本低
  • 天然支持 CSS 隔离、JS 隔离

缺点

  • URL 不同步
    • iframe 内部页面跳转,url 不会更新
    • 浏览器刷新导致 iframe url 状态丢失、后退前进按钮无法使用。
  • UI 不同步
    • DOM 结构不共享。iframe 里的弹窗遮罩无法在整个父应用上覆盖
  • 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  • 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
  • 无法共享基础库进一步减少包体积

single-spa

原理

监听路由+卸货+装货。

问题

  • 需要主应用指定加载哪些 js、css,如果子应用打包逻辑发生变化,主应用也要跟着修改
  • 一个页面加载多个子应用时可能会存在样式冲突,js冲突
  • 多个子应用之间的通信问题
  • single-spa 的入口是一个 js 文件,需要代码里手动指定要加载啥 js、css 等,不方便维护。

qiankun

qiankun 是基于 single-spa 的微前端框架。qiankun 是对 single-spa 的一层封装,核心做了构建层面的一些约束以及沙箱能力,支持多子应用并存,但是接入的修改成本较高。

应用加载

利用import-html-entry,根据子应用url入口的html 文件,解析出 scripts、styles 去单独加载,其余部分做转换后放到 dom 里。

生命周期管理

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

js全局变量沙箱隔离

  • 快照:加载子应用前先记录下 window 的属性,卸载后恢复之前的快照
  • diff:加载子应用后记录对 window 属性的修改,卸载之后恢复回去
  • Proxy:创建一个代理对象,每个子应用访问到的都是这个代理对象
快照和diff都不能同时存在多个子应用,一般使用Proxy

css样式隔离

挂载子应用时将子应用的css样式以style标签的形式插入并做快照,卸载子应用时再将快照内的style样式删除。
css隔离:shadow dom 和 scoped css
  • 严格沙箱(shadow dom)
    • 将整个子应用放到Shadow DOM内进行嵌入,完全隔离了主子应用。
      缺点:
    • 子应用的弹窗、抽屉、popover因找不到主应用的body会丢失,或跑到整个屏幕外(具体原因作者并未详细研究)
    • 主应用不方便去修改子应用的样式
  • 实验性沙箱(scoped css)
    • 实现形式类似于vue中style标签中的scoped属性,qiankun会自动为子应用所有的样式增加后缀标签,如:div[data-qiankun-microName]
      缺点:
      • 子应用的弹窗、抽屉、popover因插入到了主应用的body,所以导致样式丢失或应用了主应用了样式

通信机制

  • props
  • 发布订阅模式。主应用里做全局状态初始化,子应用获取全局状态 getGlobalState 和状态变化时的处理: onGlobalStateChange

优点

  • 降低了应用改造的成本,通过html entry的方式引入子应用;
  • 完备的沙箱方案,js 沙箱做了 SnapshotSandbox、LegacySandbox、ProxySandbox 三套渐进增强方案,css 沙箱做了 strictStyleIsolation、experimentalStyleIsolation 两套适用不同场景的方案;
  • 支持静态资源预加载能力。

缺点

  • 适配成本较高,包括工程化、生命周期、静态资源路径、路由等方面的适配;
  • css沙箱的严格隔离可能引发问题,js沙箱在某些场景下执行性能下降;
  • 无法同时激活多个子应用,不支持子应用保活;
  • 不支持vite等esmodule脚本运行。

无界

css隔离

利用 Web component 的 custom element 和 shadow dom 实现样式隔离。

js隔离

利用 iframe 实现沙箱,让子应用脚本在 iframe 里运行。

js 沙箱和 css 沙箱连接

采用 proxy + Object.defineproperty 的方式将 js-iframe 中对 dom 操作劫持代理到 Web component容器里。

路由同步机制

劫持 iframe 的 history.pushState 和 history.replaceState,就可以将子应用的 url 同步到主应用的 query 参数上,当刷新浏览器初始化 iframe 时,读回子应用的 url 并使用 iframe 的 history.replaceState 进行同步。

通信机制

承载子应用的 iframe 和主应用是同域的,所以可以进行通信。通信方式:
  • 全局变量Props 注入
子应用通过 $wujie.props 可以拿到主应用注入的数据。
  • window.parent 通信
子应用和主应用同源,可以通过 window.parent 和主应用通信。
  • 去中心化的通信
通过 EventBus 事件总线实例,注入到主应用和子应用,实现去中心化通信。

缺点

  1. 要单独开iframe作为js沙箱,内存占用大。
 
 

核心能力

路由管理

一般我们使用 Hash 或者 History 模式来对路由进行监听,如 hashchange 或 popstate 事件。
目前常见的微前端解决方案主要是路由驱动的。在微前端的基座,进行子应用的路由注册,如 { path: '/microA/*' } ,基座根据路由匹配情况,按需挂载子应用。具体路由跳转规则由子应用接管响应。
  1. 作为一个SPA的基座应用,本身是一套纯前端项目,要想展示微应用的页面除了采用iframe之外,要能先拉取到微应用的页面内容, 这就需要远程拉取机制
  1. 远程拉取机制通常会采用fetch API来首先获取到微应用的HTML内容,然后通过解析将微应用的JavaScript和CSS进行抽离,采用eval方法来运行JavaScript,并将CSS和HTML内容append到基座应用中留给微应用的展示区域,当微应用切换走时,同步卸载这些内容,这就构成的当前应用的展示流程。
  1. 而目前针对远程拉取机制这套流程,已有现成的库来实现,可以参考import-html-entrysystem.js
  1. 当然这个流程里会涉及到CSS样式的污染以及JavaScript对全局对象的污染,这个涉及到隔离问题会在后面讨论,

css隔离

  1. 动态样式表,应用卸载之后样式也同时卸载;这种方式需要注意的问题是微应用卸载时也要通知主应用将微应用的样式卸载掉,如果卸载不成功则会造成样式污染
  1. 工程化手段:css module,css in js,无法杜绝项目中使用全局样式,可能需要我们约定只能在主应用中修改全局组件样式
  1. shadow DOM,天然的样式隔离
  1. 运行时样式转换:运行时给所有节点增加一个属性,利用属性选择器创建命名空间div[data-qiankun-subvue1]
  1. BEM约束,或者编译时增加namespace

js的全局变量隔离

快照沙箱
在沙箱激活时,记录window当时的状态作为快照,在沙箱卸载时,恢复快照。
缺点:
  • 会改变全局window的属性,如果同时运行多个微应用,多个应用同时改写window上的属性。所以只能在单个子应用模式下使用。
  • 会通过for(prop in window){}的方式来遍历window上的所有属性,window属性众多,很耗费性能。
diff沙箱
在激活时,利用 proxy API 代理window属性,记录属性的修改,在沙箱卸载时,恢复修改的属性。
缺点:
  • 会改变全局window的属性,如果同时运行多个微应用,多个应用同时改写window上的属性。所以只能在单个子应用模式下使用。
proxy代理沙箱
在激活时,为每个子应用创建一个空的沙盒对象,拦截window访问,对访问的属性先从沙盒对象取,如果没有则从window里取。当修改属性,则直接在空沙盒对象中修改。避免污染全局window。这样就可以在多子应用模式下使用。

js隔离

  • qiankun : function + proxy + with。 function 包裹了一层,所以代码放在了单独作用域跑,又用 with 修改了 window,所以 window 也被隔离了
  • micro-app 是 web components。
  • 而 wujie 是 web components 和 iframe。
 

storage隔离

  • 靠自觉,在key前加上应用名作为前缀。
  • 重写Storage方法,自动在key前加上应用名作为前缀。
  • 封装二方包,使用二方包进行storage操作,自动在key前加上应用名作为前缀。

应用间消息通信

  1. 基于storage,然后用事件监听。
  1. 路由参数。
  1. 通过原生 CustomEvent 类实现,子应用通过 dispatchEvent 和 addEventListener 来对自定义事件进行下发和监听。
  1. 消息订阅(pub/sub)模式,在基座应用中会定义事件中心Event,每个微应用分别来注册事件,当被触发事件时再有事件中心统一分发。

依赖管理

常见的微前端框架中,基座应用统一对子应用的状态进行管理。根据路由和子应用状态,按需触发生命周期函数,做请求加载、渲染、卸载等动作。而多个子应用间可能存在一些公共库的依赖。
为减少这类资源的重复加载,通常可以借助 webpack5 的 Module Federation 在构建时进行公共依赖的配置,实现运行时依赖共享的能力。除了使用打包工具的能力,也可以从代码层面通过实现类 external 功能对公共依赖进行管理。

资源共享

发布/订阅模式 EventBus

主应用注册 EventBus,然后通过 props 下发微应用,这样微应用既有主应用的EventBus 也可以有自己的 EventBus

关联技术

Web Components

Web Components 允许开发者不借助框架,实现一些可重用的自定义组件。构建一个 Web Components 通常使用到 customElements、Shadow DOM 的 API,和 templates、slot 标签。
基于 Web Components 开发,可以天然契合微前端的一些特性:技术栈无关,应用间隔离。但兼容性较差,不支持 IE。

iframe

iframe 常用于将应用嵌入另一个宿主应用中。只使用 iframe 方案引入子应用的好处是浏览器兼容性强,接入成本低,样式及脚本天然隔离。但是由于 iframe 和宿主应用完全隔离,各自独立运行,导致了诸多限制,如:
  1. 资源无法共享;
  1. iframe 中的 UI 无法跨越 iframe 窗口边界;
  1. 刷新页面时 iframe 中的路由状态丢失;
目前腾讯提供了一个新的微前端实施思路:借助 ShadowRoot 渲染子应用的 DOM;iframe 负责运行子应用的 JavaScript 代码,从而实现 JS 沙箱和 CSS 隔离能力。另外,在保证子应用和主应用同源的前提下,将子应用的路由变化同步到主应用中,从而保证刷新页面后,路由地址正常。

webpack5 Module Federation

目前市面上的微前端方案中,有基于 Module Federation 的微前端框架实践。Module Federation 是 webpack 提供的一个插件,他支持通过配置以下核心参数,在打包构建阶段确定集成策略:
  1. exposes 参数,指定应用可以描述当自己作为被加载的远程模块时,可暴露给其他应用使用的模块路径。
  1. remotes 参数,指定应用可以从远端加载的远程模块地址。
  1. shared 参数,指定应用可以与其他远程模块共享的依赖(精确到版本)。
 
 

参考

前端埋点设计关于react 的状态管理
Loading...