2022年1月最新最全的微前端框架调研


Micro-FrontEnds Research in Jan 2022

原文写自 Notion, 推荐直接点击此处阅读。

Why

系统现状

  1. 巨石应用(Monolithic-Applications)
    1. 开发体验(开发仓单的真实体验) - JSX 刷新有问题?!template 未更新?!再刷新。emmm...
      1. 启动慢 - ?? 首次启动(无缓存的冷启动)要3~6分钟,二次启动(有缓存的热启动) 1 分钟,修改代码后的热更新 HMR 5~10s。
      2. 构建慢 - 在等待构建的时间中虚度生命????♂? 构建一次 10~20 分钟?
    2. 用户体验 - 举个例子,刷新一下浏览器查看枚举值(页面打开速度)要 10s。我不想再等了 ???♂?...
    3. 维护体验 - 好多代码,好多组件,好多依赖, emmm...
  2. 技术栈 - Vue + VueRouter + Vuex + ElementUI + axios
  3. 依赖耦合 - 究竟是谁依赖了谁?不知道,不想查,不用管。因为项目前身已经被拆分,所以依赖关系还算好找。
    1. 使用 @ 作为项目 src/* 别名,find @\/ in ./src/modules
    2. 使用 @name 作为模块 src/modules/name 别名
  4. 跨团队开发 - 协同/沟通/开发成本,我们团队怎么做?
    1. 熟悉组件和其 API
    2. 编写 README.md
    3. 编写 component demo 页面
    4. 开发业务组件并集成到 usage examples 到 component demo 供开发参考

How

解决方案

使用微前端技术分拆应用,以解决上述问题。理想目标是:

  • 程序低入侵
  • 开发体验
  • 用户体验
  • 理解成本

微前端的定义

微前端是一种应用架构,通过拆分应用远程加载应用(通过组件/模块/包的运行时加载)达到团队/工程/应用解藕的目的以实现独立开发、独立部署。

微前端的优点

  1. 专注开发体验 - 开发爽,用户才能爽
    1. 应用更轻
    2. 依赖解耦
    3. 同步更新 - 如果多个业务应用依赖同一个服务应用的功能模块,只需要更新服务应用,其他业务应用就可以立马更新,从而缩短了更新流程和节约了更新成本。
    4. 增量升级
    5. 公共文档
    6. 技术无关
  2. 提升开发效率 - 开发快,下班才能早
    1. 启动更快
    2. HMR更快
    3. 构建更快
  3. 降低协同成本 - 针对跨团队场景
    1. 命名空间
    2. 独立开发
    3. 独立测试
    4. 独立部署
  4. 提升用户体验
    1. 按需加载
    2. 增量加载
    3. 预加载
  5. 软件工程优点
    1. 康威定律

      软件架构反映了组织架构。因此通过调整组织架构,反过来也能推动软件架构的演进。

    2. 分而治之
      1. 单一职责
      2. 边界清晰
      3. 更易维护
    3. 团队管理 - 同上


—— 来自 Dan 的 tweet 吐槽

微前端的缺点

  • 复杂度从代码转向基础设施
    • 微前端架构框架 - Micro-Frontends framework
    • 微前端构建工具 - Webpack/Vite/Babel plugins
    • 公共文档 - 组件/模块/物料共享。除了看文档和代码,否则我不知道暴露出了哪些内容?又该怎么用? - Docs system for shared components/modules/materials etc.
    • 调试工具 —— 单框架则使用框架本身,若涉及到跨框架的事件分发,数据通信,状态隔离呢?没有调试工具,打 log 也能将就?emmm... Devtools for micro-frontends communication.
    • [监控系统] —— Sentry?略
    • [部署平台] —— CI/CD Jenkins?Travis? 略
  • 调试成本因为应用依赖拆分而变得更高
    • 父子嵌套依赖关系
      • 基座应用/主应用 依赖 微应用/子应用
      • 微应用/子应用 依赖 基座应用/父应用
    • 应用嵌套依赖关系
      • SubApp A 依赖 SubApp B 某组件
      • SubApp B 依赖 SubApp C 某模块
      • SubApp C 依赖 SubApp A 某枚举
  • 框架的学习和理解成本
    • 可以不用理解 VS 理解更利于开发

总结

从长期收益上来说,利大于弊。

  1. 快,提高生产力。
  2. 解藕,只关注业务,不关注整体。
  3. 独立,多团队并行,你开发你的我开发我的。我不想管你写了什么,除非你写进了公共文档/组件库。如果你没把公共模块放进去,那你很危险啊。

微前端的分类

按项目类型:

  1. 主应用 —— 聚合共性,加载子应用。
  2. 子应用 —— 解藕特性,分治业务。可能依赖其他应用。

按技术栈:

  1. 支持跨技术栈 —— 微前端化。基座应用 MainApp / 微应用 MicroApp,跨技术栈框架,React/Preact/Vue/Angular/Svelte/Cycle/Ember/Backbone/jQuery etc.
  2. 不支持跨技术栈 —— 微应用化。主应用 App / 子应用 SubApp,来自统一技术栈,支持跨技术栈,Works for only one framework.

按构建方式:

编译时微前端,通常将第三方库中的组件作为包,在构建时引入依赖。这种实现引入新的微前端需要重新编译,不够灵活。

运行时微前端,全量加载或增量加载将微应用注入到当前应用程序中。当引入新的微前端的时候,不需要构建,可以在代码中动态加载。

—— 摘自一文读懂微前端架构

按开发职责:

  1. 客服端实现 —— 微前端,微应用,Module federation,iframe,公共包 等
  2. 服务端实现 —— nginx 反向代理,域名映射 等

按实现方式:

基座模式:通过搭建基座、配置中心来管理子应用。 —— 容器(包含)

约定模式: 通过约定进行互调,但会遇到处理第三方依赖等问题。—— 拼图(拼接)

去中心模式: 脱离基座模式,每个应用之间都可以彼此分享资源。—— 胶水(粘贴)

—— 摘自《什么是微前端》#四-微前端方案种类

改造方案

下面这张图最早看到是在 18 年底还是 19 年初的样子,出自 Umi 团队(即乾坤 Qiankun) sorrycc 的一篇微前端的文章:

真正的微前端跨技术栈的场景,但不多。大多数公司,应该是统一技术栈的。所以,从某种程度上讲,微应用比微前端更实际。

社区调研

调研了社区目前已知的微前端框架及库的实现,分析微前端几大核心模块及其实现成本和优缺点。

Framework/Features Sandbox - 隔离沙盒 Router - 中心化路由 Loader - 模块加载器 State/Event communication - 跨应用数据通信 Registry - 应用注册中心 Lifecycles - 应用生命周期 Module - 独立模块 Builder - 构建与部署
single-spa - library agnostic - reroute.js load.js window.dispatchEvent, CustomEvent apps.js timeout.js parcel -
QianKun - library agnostic sandbox.ts ? 基于 ? 和 single-spa 的 loadApp globalState.ts, use single-spa use single-spa use single-spa - Webpack Entry - Webpack Jsonp + UMD
icestark - library agnostic @ice/sandbox icestark-sandbox checkAlive, appHistory @ice/stark-module loader @ice/stark-data store,event, 事件通信 @ice/stark apps.ts @ice/stark appLifeCycle.ts @ice/stark-module
Garfish - library agnostic @garfish/browser-snapshot Snapshot, @garfish/browser-vm VM router/src/context.ts
router/src/linkTo.ts @garfish/loader loader Garfish.loadApp @garfish/hooks @garfish/core garfish.ts @garfish/hooks @garfish/remote-module Webpack Entry
alibabacloud-alfa - ng/vue/react @alicloud/console-os-browser-vm browser-vm 子应用自身处理,通过 iframe 同步到主应用。History.js @alicloud/console-os-loader src/requireEnsure.ts nodejs module {eventEmitter} from events @alicloud/console-os-kernal src/application/createApp.ts @alicloud/console-os-kernal createAppLoader.ts Webpack Plugin
Mooa - Angular/iFrame - src/router.ts
reRouter 同 single-spa 的 reroute helper/loader.helper.ts src/helper/app.helper.ts customEvent,
src/model/constants.ts MOOA_EVENT src/mooa.ts registerApplication, registerApplicationByLink src/lifecycles 代码 80% 与 single-spa 雷同 - -
VueMFE v1.0 - Vue - core/router/index.js helpers/loader.js app.$emit , app.$on, Vuex Dynamic Module Registration app.$store.registerModule / app.$store.unregisterModule src/core/app/config.js registerApp - core/lazy.js vue-cli-plugin-mfe
EMP - 基于 module federation - - Webpack5 module federation - - - - Webpack5 module federation

核心模块

  • Sandbox - 隔离沙盒。JS 被放入沙盒执行,以此隔离全局副作用,实现应用独立运行时。每个微应用之间状态隔离,运行时状态不共享

    • 副作用类型:
      • 静态副作用,HTML 中静态标签内容:Script 标签、Style 标签、Link 标签。
      • 动态副作用,由 JavaScript 调用 BOM/DOM 动态创建出来的:动态创建 Style、动态创建 Script、动态创建 Link、动态执行代码、动态添加 DOM 元素、添加全局变量、添加定时器、网络请求、localStorage 等对当前页面产生副作用的内容。
    • 实例分类:
      • 单实例:同一个时刻只有一个微应用实例存在,此刻浏览器所有浏览器资源都是这个应用独占的,方案要解决的很大程度是应用切换的时候的清理和现场恢复。比较轻量,实现起来也相对简单。
      • 多实例:资源不是应用独占,就要解决资源共享的情况,比如路由,样式,全局变量读写,DOM。可能需要考虑的情况比较多,实现较为复杂。
    • 实现原理:
      • snapshot:在应用运行前通过快照的模式来保存当前执行环境,在应用销毁后恢复会应用之前的执行环境,用于实现应用间副作用的隔离和清除。同时运行多个快照沙箱实例时,在代码执行顺序非线性的场景下,并不能有效的收集和处理应用的副作用。
      • vm: 核心逻辑是创建一个 fakeWindow 并使用 proxy 代理真实的 nativeWindow 的属性和方法,并在当前 context 中标记和收集。在退出或卸载 context 时清空标记及其引用。
        • Window
          • 用于隔离全局环境
        • document
          • 收集 DOM 副作用
          • 收集 Style 副作用
          • 收集 Script 继续放入沙箱执行
          • 用于捕获动态创建的 DOM 节点、Style、Script
        • timeout、interval
          • 处理定时器
        • localStorage
          • 隔离 localStorage
        • listener
          • 收集全局事件
    • 框架对比:
      • QianKun

        • ProxySandbox - 使用 proxy 拦截,并在 active/inActive 时分别从缓存的 map 中 set/delete 相关 modified/added 属性。
        • SnapshotSandbox - 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
      • Garfish - 默认情况下使用 VM 沙箱(VM 沙箱支持多实例),不使用快照沙箱。原理同上,但细节场景覆盖得更齐全。

        • VMSandbox - 复制 window, document 等对象,使用 Object.defineProperty 冰冻 native window & document,使用 proxy 拦截并收集

          import { historyModule } from './modules/history';
          import { networkModule } from './modules/network';
          import { documentModule } from './modules/document';
          import { UiEventOverride } from './modules/uiEvent';
          import { localStorageModule } from './modules/storage';
          import { listenerModule } from './modules/eventListener';
          import { observerModule } from './modules/mutationObserver';
          import { timeoutModule, intervalModule } from './modules/timer';
          import { makeElInjector } from './dynamicNode';
          
        • SnapshotSandbox - 快照沙箱。

          • snapshot.take() 对之前的状态执行快照然后通过 diff 回滚状态。
          • PatchGlobalVal
          • PatchStyle
          • PatchEvent
          • PatchHistory
          • PatchInterval
          • PatchWebpackJsonp
      • alibabacloud-alfa - 使用 iframe 的 contextWindow 作为隔离上下文,处理了一部分副作用。属于最简单实用的方案了,但问题是 iframe 对于 history 的 path 需要通过 postMessage 与主应用同步。如何“取巧”实现一个微前端沙箱?

        import Window from './Window';
        import Document from './Document';
        import Location from './Location';
        import History from './History';
        
  • Router - 中心化路由,拦截符合规则的路由并加载其对应的微应用。

    • 实现原理
      1. 路由拦截 - 适用于框架无关
        1. 收集框架监听的 popstate 事件
          1. 重写 window.addEventListener
          2. 判断 eventName 是否是路由事件
        2. 主动触发 popstate 事件
      2. 路由注入 - 适用于单一框架
        1. 将子应用路由动态注入进主应用路由。
    • 框架对比
      • 拦截和重写 - SingleSPA/Garfish
      • 追加事件 - icestark
  • Module - 微模块,通常是一个模块或页面,跟页面路由无关,可以随处挂载,也会出现多个微模块同时渲染运行。比如说:Component、Service、ES Module、CSS/image/icon 等静态资源。其实现是:

    • UMD
    • Webpack jsonp
    • Webpack5 Module Federation
    • ESM - Bundless
  • Builder - 构建与打包。

    • Webpack4 - UMD/jsonp
    • Webpack5 - Module Federation
    • Vite?
  • Loader - 用于加载 MicroApp or MicroModule.

    • 程序入口
      • 进入到微应用时解析微应用入口资源,自动触发 Loader 加载
        • html-entry
        • config-entry/javascript-entry
      • 手动的编程式触发 Loader.load(path) 加载远程模块
    • 实现原理
      • 通过 XHR/fetch/request or script/link/meta tag 获取入口文件内容
      • html-entry 当入口文件是 html 文档时:
        • 解析 html 内容生成 ast
        • 获取 script/link/mata/style 等资源标签
        • 添加对应标签并插入到主应用 html 文档流中
      • config-entry 当入口为 js 文件时:
        • [启用沙箱]
        • 加载 js 文件资源
        • 执行 js 文件
      • 微应用 mount() 成功
    • 框架对比
      • import-html-entry - QianKun 解析 html 模版中