Micro-FrontEnds Research in Jan 2022
原文写自 Notion, 推荐直接点击此处阅读。
Why
系统现状
- 巨石应用(Monolithic-Applications)
- 开发体验(开发仓单的真实体验) - JSX 刷新有问题?!template 未更新?!再刷新。emmm...
- 启动慢 - 首次启动(无缓存的冷启动)要3~6分钟,二次启动(有缓存的热启动) 1 分钟,修改代码后的热更新 HMR 5~10s。
- 构建慢 - 在等待构建的时间中虚度生命?♂️ 构建一次 10~20 分钟?
- 用户体验 - 举个例子,刷新一下浏览器查看枚举值(页面打开速度)要 10s。我不想再等了 ♂️...
- 维护体验 - 好多代码,好多组件,好多依赖, emmm...
- 开发体验(开发仓单的真实体验) - JSX 刷新有问题?!template 未更新?!再刷新。emmm...
- 技术栈 - Vue + VueRouter + Vuex + ElementUI + axios
- 依赖耦合 - 究竟是谁依赖了谁?不知道,不想查,不用管。因为项目前身已经被拆分,所以依赖关系还算好找。
- 使用
@
作为项目 src/* 别名,find @\/ in ./src/modules
- 使用
@name
作为模块 src/modules/name 别名
- 使用
- 跨团队开发 - 协同/沟通/开发成本,我们团队怎么做?
- 熟悉组件和其 API
- 编写 README.md
- 编写 component demo 页面
- 开发业务组件并集成到 usage examples 到 component demo 供开发参考
How
解决方案
使用微前端技术分拆应用,以解决上述问题。理想目标是:
- 程序低入侵
- 开发体验好
- 用户体验快
- 理解成本低
微前端的定义
微前端是一种应用架构,通过拆分应用、远程加载应用(通过组件/模块/包的运行时加载)达到团队/工程/应用解藕的目的以实现独立开发、独立部署。
微前端的优点
- 专注开发体验 - 开发爽,用户才能爽
- 应用更轻
- 依赖解耦
- 同步更新 - 如果多个业务应用依赖同一个服务应用的功能模块,只需要更新服务应用,其他业务应用就可以立马更新,从而缩短了更新流程和节约了更新成本。
- 增量升级
- 公共文档
- 技术无关
- 提升开发效率 - 开发快,下班才能早
- 启动更快
- HMR更快
- 构建更快
- 降低协同成本 - 针对跨团队场景
- 命名空间
- 独立开发
- 独立测试
- 独立部署
- 提升用户体验
- 按需加载
- 增量加载
- 预加载
- 软件工程优点
- 康威定律
软件架构反映了组织架构。因此通过调整组织架构,反过来也能推动软件架构的演进。
- 分而治之
- 单一职责
- 边界清晰
- 更易维护
- 团队管理 - 同上
- 康威定律
—— 来自 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 理解更利于开发
总结
从长期收益上来说,利大于弊。
- 快,提高生产力。
- 解藕,只关注业务,不关注整体。
- 独立,多团队并行,你开发你的我开发我的。我不想管你写了什么,除非你写进了公共文档/组件库。如果你没把公共模块放进去,那你很危险啊。
微前端的分类
按项目类型:
- 主应用 —— 聚合共性,加载子应用。
- 子应用 —— 解藕特性,分治业务。可能依赖其他应用。
按技术栈:
- 支持跨技术栈 —— 微前端化。基座应用 MainApp / 微应用 MicroApp,跨技术栈框架,React/Preact/Vue/Angular/Svelte/Cycle/Ember/Backbone/jQuery etc.
- 不支持跨技术栈 —— 微应用化。主应用 App / 子应用 SubApp,来自统一技术栈,支持跨技术栈,Works for only one framework.
按构建方式:
编译时微前端,通常将第三方库中的组件作为包,在构建时引入依赖。这种实现引入新的微前端需要重新编译,不够灵活。
运行时微前端,全量加载或增量加载将微应用注入到当前应用程序中。当引入新的微前端的时候,不需要构建,可以在代码中动态加载。
—— 摘自一文读懂微前端架构
按开发职责:
- 客服端实现 —— 微前端,微应用,Module federation,iframe,公共包 等
- 服务端实现 —— 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
- 收集全局事件
- Window
- 框架对比:
-
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 - 中心化路由,拦截符合规则的路由并加载其对应的微应用。
-
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) 加载远程模块
- 进入到微应用时解析微应用入口资源,自动触发 Loader 加载
- 实现原理
- 通过 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 模版中
<script src="/main.js" entry />
中标记了entry
属性的 script 标签,并返回所有 JS 序列化执行后的结果 - templateParse - Garfish 通过 fetch 拿到 html 的文本内容并赋值给自定义的
html = document.createElement('html')
的 innerText 后遍历 meta, link, style, script 节点,拿到特定元素后执行loader.load
加载。
- import-html-entry - QianKun 解析 html 模版中
- 程序入口
-
State/Event communication - 微前端跨应用间通信。
- CustomEvent - SingleSPA/QianKun
- Event - icestark
- EventEmitter2 - Garfish
-
Registry - 微应用/微模块的注册中心。
apps: App[]
- 使用数组。registerApp/registerApplication
- single-spa/icestark/Garfish/mooa<App />
<AppRouter />
- 同时支持 JSX/Template 的有 icestark
Map<string, App>
- 使用 Map 储存所有注册的 App.createSubApp
registerApp
- VueMfe
-
Lifecycles - 微应用的生命周期。
- bootstrap - single-spa 启动
- registerAppEnter - icestark 加载前
- beforeLoad - Garfish 加载前
- afterLoad - Garfish 加载后
- beforeMount - Garfish 挂载前
- mount - single-spa 挂载
- afterMount - Garfish 挂载后
- registerAppLeave - icestark 卸载前
- beforeUnmount - Garfish 卸载前
- unmount- single-spa 卸载
- afterUnmount - Garfish 卸载后
- unload - single-spa 移除
图片摘自字节跳动是如何落地微前端的
成本分析
模块/类型\实现难度 | 跨技术栈 | 单技术栈 | 必要 |
---|---|---|---|
Sandbox | 高 | 高 | 否 |
Router | 中 | 低 | 否 |
Module | 中 | 低 | 否, Use UMD |
Builder | 低 | 高 | 是 |
Loader | 中 - HTML entry | 低 - JavaScript entry | 是 |
State/Event | 低 | 无 - 框架自带 | 否 |
Registry | 低 | 低 | 是 |
Lifecycles | 中 | 低 | 否 |
- Sandbox:
实现成本 | 优点 | 缺点 | 适用场景 | |
---|---|---|---|---|
VM | 高 | 支持多实例 | 实现起来比较复杂,需要考虑的细节太多 | 嵌套、多实例、跨技术栈 |
Snapshot | 低 | 相对简单,不需要考虑太多细节 | 不支持多实例嵌套,嵌套导致内部快照依赖关系混乱 | 无嵌套 |
图片摘自字节跳动是如何落地微前端的
- Router
实现成本 | 优点 | 缺点 | 适用场景 | |
---|---|---|---|---|
事件劫持 | 中 | 通用 | 拦截全局事件,派发全局事件,重写全局事件。 | 跨技术栈 |
路由注入 | 低 | 简单 | 不支持跨技术栈 | 单技术栈 |
- Module
实现成本 | 优点 | 缺点 | 适用场景 | |
---|---|---|---|---|
UMD Module | 低 | 通用 | 污染全局命名空间 | all |
Module Federation | 高 | 支持多实例共享 | ||
多实例不会污染 | 编译时配置,添加全局变量 | |||
需启动多个实例 runtime | ||||
支持运行时,但配置复杂 | shared modules |
- Builder
实现成本 | 优点 | 缺点 | 适用场景 | 备注 | |
---|---|---|---|---|---|
UMD | 低 | 简单,基础设施不需要做任何修改,只需要修改 webpack 构建配置。 | |||
全局共享,代码执行完成后可通过全局变量访问模块引用。 | 暴露全局变量 | ||||
污染全局命名空间 | 跨技术栈 | ||||
有公共依赖 | |||||
shard modules + External | |||||
Webpack Jsonp + UMD | 低 | 同上,额外配置 jsonp name。 | 同上 | 同上 | Why need different JSONP name? |
- Same moduleId
- Same global variable |
| Webpack5 Module Federation | 高 | 通用 | 构建系统整体迁移 Webpack 5 | 跨技术栈
有公共依赖 | TODO |
| Webpack Entry | 无 | 无成本 | 无法暴露公共模块以共用 | 跨技术栈
无公共依赖 | |
- Loader
实现成本 | 优点 | 缺点 | 适用场景 | |
---|---|---|---|---|
HTML entry | 高 | 通用 | 额外 html parser 的解析开支 | 跨技术栈 |
Config entry | 低 | 简单 | Module Only | 跨技术栈/单技术栈 |
方案对比
技术难度 | 实施成本 | 维护成本 | 理解成本 | |
---|---|---|---|---|
跨技术栈 | 高 | 中 | 高 | 高 |
单技术栈 | 低 | 中 | 中 | 低 |
- 微前端化,针对跨技术栈 - 路由拦截流派。
- 设计哲学:胶水,可以卸载即“撕开”。
- 框架方案:
- Single-SPA - 主应用重写
window.addEventListener
拦截监听路由的时间,执行内部的reroute
逻辑,加载子应用。 - QianKun - 基于 single-spa,增加了
html-entry
,sandbox
,globalSate
等核心功能。 - icestark - 同上。把大部分配置通过 cache 写进了
window['icestark']
全局变量。 - Garfish - 看起来是对市场所有 MFE 框架功能和实现调研后的写的增强版。
- feature-hub - 未做深入调查。
- Mooa - single-spa 的 angular 版本。看源码中除了 NG,并没有对 Vue/React 的 adapter/bridge。但不排除魔改一下也能 work,因为有做 router 的魔改与适配。
- Single-SPA - 主应用重写
- 微应用化,针对单技术栈 - 路由注入流派。
- 设计哲学:模块,加水进水桶,用完从桶中”倒"出来。
- 怎么把加进去的水倒出来? - 隔离标记
- 注入之后如何销毁?- 隔离删除
- 桶什么时候崩溃?- 装不下了?!
- 水什么时候可能出问题。- 沙箱?!
- 技术方案:
- 路由钩子
router.beforeHook
探测 - 继承 Vue-Router 重写
router.push
- 路由钩子
- 设计哲学:模块,加水进水桶,用完从桶中”倒"出来。
理想方案
这套方案很理想。开发成本最高,做出来后值得推广。
Application/Mode | Dev - 开发调试 | Build - 构建部署 | Prod - 生产环境 |
---|---|---|---|
App | App Local + SubApp Remote | HTML entry | HTML entry |
SubApp | App Remote + Current SubApp Local + Other SubApp Remote | JavaScript Entry + Module Federation Exposes | JavaScript Entry + Module Federation Exposes/Module Federation Remotes |
Module | Local Module + Remotes | Module Federation Exposes | Module Federation Exposes/Module Federation Remotes |
- 开发方案最佳实践:
- 主应用需要更新时 - 本地使用 localhost,而子应用都通过 Module Federation Remote 远程拉取。
- 主应用线下环境 - App Local
- 所有子应用线上环境 - SubApp Remote
- 子应用需要更新时 - 本地开发公共依赖即主应用依赖用
https://mainApp/cdn/remoteEntry.js
,而子应用依赖用本地 host 的静态入口http://localhost:PORT/subAppName/remoteEntry.js
。- 主应用线上环境 - App Remote
- 需要开发的子应用线下环境 - Current SubApp Local
- 不需要开发的子应用使用线上环境 - Other SubApp Remote
- 同时调试主应用和子应用
- 主应用线下环境 - App Local
- 调试的子应用也配置成线下环境 - Current SubApp Local
- 不需要开发的子应用使用线上环境 - Other SubApp Remote
- 主应用需要更新时 - 本地使用 localhost,而子应用都通过 Module Federation Remote 远程拉取。
- 模块共享最佳实践:
- Webpack5 Module Federation
- 编译时自动配置 remotes 与 exposes,生成 remoteEntry
- 启动应用时 html 需要注入依赖的 remotes 应用与 remoteEntry 列表
- 更偏向运行时通过 getResources 接口获取各个子应用的 entry 并在 runtime 注入
- 理想的方案是 Runtime Module Federation, 假如有的话,需要调研
- Umi 的 依赖预构建 Pre-build 与 MFSU 技术,提升开发体验 ?!
- 主应用
- shared 公共库,公共功能,公共组件,公共配置等。
- 分别单独作为 exposes 被构建以供子应用启动时设置 federate。
- 子应用
- shared 在编译时注入主应用公共依赖
- remotes 在编译时注入主应用与其他子应用依赖
- exposes 组件,模块,配置等
- 多个子应用间互相依赖时自动生成运行时 自动配置 Federation
- 多个子应用间的 exposes 如何统一管理?因为开发者并不知道 moduleA 暴露出了啥啥啥?这里还是有着增量的沟通/理解成本。增量加载 VS 全量加载。
- Webpack5 Module Federation
- 设计思路:
- 基座应用/主应用
- 公共部分 - Common parts
- 公共资源 - fonts/images/styles/scripts etc.
- 公共依赖 - libraries/plugins/hooks/lifecycles
- 公共组件 - components
- 公共布局 - layouts
- 公共数据 - store/data
- 公共服务 - services/events/utils
- 公共路由 - router/routes
- 公共配置 - configurations
- App 基类 - Micro-FrontEnds
- Sandbox
- Router
- Loader
- Registry
- Lifecycles
- ModuleManager
- Builder for App prod from Html-Entry
- 公共部分 - Common parts
- 子应用/微应用
- 私有部分 - 业务代码
- Assets
- Routes
- Views
- Servies
- Components
- Utils
- Entry - 构建入口
- SupApp 继承自 App 基类 - Micro-FrontEnds
- SubApp 自带了 App 的功能并支持重写
- 开发构建 - Builder for Dev
- 注册应用 - Registry
- 依赖加载 - Sandbox
- 路由注入 - Router
- 模块加载 - Loader
- 模块共享 - ModuleManager
- 应用加载 - Lifecycles:bootstrap/onLoad/afterLoad
- 应用渲染 - Lifecycles:beforeMount/afterMount
- 应用卸载 - Lifecycles:beforeUnload/afterUnload
- 生产构建 - Builder for SubApp prod from config-entry/js-entry
- SubApp 自带了 App 的功能并支持重写
- 私有部分 - 业务代码
- App & SubApp 都需要整合 Webpack 的 module federation 构建功能
- 基座应用/主应用
现有方案
目前一体化平台采用的方案,Vue-MFE 1.0 + vue-cli-plugin-mfe。19年投入使用,已稳定运行 2.5年。
- 风险最低
- 迁移成本低
- 技术栈稳定
- 开发模式:
折中方案
对 Vue-MFE 的功能做增强(具体内容见 Vue-MFE 2.0 Roadmap),重写 Loader 与 Builder 以实现 Runtime Module federation 。
社区方案
社区现有的微前端方案,建议尝试 Garfish。
- 功能实现完善
- 代码写得好
- API/Document 虽简,但扩展性强
Module Federation
Module Federation 实现了类似动态链接库的能力,可以在运行时加载远程代码,远程代码本质上是一个加载在 window 上的全局变量,Module Federation 可以帮助解决依赖的问题。—— 一文读懂微前端架构
- 首先,mf 会让 Webpack 以
filename
作为文件名生成入口文件,Just like JS entry. - 其次,文件中以 var 的形式暴露了一个名为
name
的全局变量,其中包含了exposes
以及shared
中配置的内容 - 最后,作为
host
时,先通过remote
的init
方法将自身shared
写入remote
中,再通过get
获取remote
中expose
的组件,而作为remote
时,判断host
中 是否有可用的共享依赖,若有,则加载host
的这部分依赖,若无,则加载自身依赖。
Motivation(动机)
将多个独立的构建可以组成一个应用程序,这些独立的构建之间不存在依赖关系,因此可以单独开发和部署它们。 —— 微前端,微模块。
Low Level Concepts
- 本地模块:即为普通模块,是当前构建的一部分。
- 远程模块:不属于当前构建,并在运行时从所谓的容器加载。
High Level Concepts
- 容器:每个构建都充当一个容器,也可将其他构建作为容器。通过这种方式,每个构建都能够通过从对应容器中加载模块来访问其他容器暴露出来的模块。
- 共享模块:指既可重写的又可作为向嵌套容器提供重写的模块。它们通常指向每个构建中的相同模块,例如相同的库。
Building Blocks
- ContainerPlugin - 使用指定的公开模块来创建一个额外的容器入口。
- ContainerReferencePlugin - 将特定的引用添加到作为外部资源(externals)的容器中,并允许从这些容器中导入远程模块。
- ModuleFederationPlugin -
ContainerPlugin
+ContainerReferencePlugin
。
Concept Goals
Features
- 动态远程容器 - 在远程容器中用作共享作用域对象,并由 host 提供的模块填充。 可以利用它在运行时动态地将远程容器连接到 host 容器。
- 基于 Promise 的动态 Remote - 向 remote 传递一个 promise,其会在运行时被调用。你应该用任何符合上面描述的
get/init
接口的模块来调用这个 promise。当使用该 API 时,你 必须 resolve 一个包含 get/init API 的对象。{ get: () ⇒ any, init: () ⇒ any}
- 动态 Public Path - 可以允许 host 在运行时通过公开远程模块的方法来设置远程模块的 publicPath。
- output.uniqueName - 在全局环境下为防止多个 webpack 运行时冲突所使用的唯一名称。默认使用
[output.library](https://webpack.docschina.org/configuration/output/#outputlibrary)
名称或者上下文中的package.json
的包名称(package name), 如果两者都不存在,值为''
。 - output.publicPath - 在编译时(compile time)无法知道输出文件的
publicPath
的情况下,可以留空,然后在入口文件(entry file)处使用自由变量(free variable)__webpack_public_path__
,以便在运行时(runtime)进行动态设置。
Usage
对 Module Federation 的 API 不太熟悉,需要时间摸索,目前存以下疑问:
- Module Federation 在运行时如何暴露 Module ?
- 在编译时写 expose 声明暴露的模块感觉有点冗余。
- 取巧的做法是,固定暴露出的单个文件,然后在单个文件内给出所有模块内容,同 一体化平台 中导出模块的实现。
- SubApp 集成插件,默认暴露出
module.entry.js
作为 入口。
- Module Federation 在运行时如何动态植入跨应用的 Module?
- App 则在
remotes
使用 基于 Promise 的动态 Remote 通过接口接收所有 domain 的资源列表。
- App 则在
- Module Federation 如何处理嵌套的跨 domain 依赖?
- App → SubApp:接口获取所有 SubApp > 生成基于 Promise 的 Remotes 。
- SubApp → App:接口获取所有 SubApp > 生成基于 Promise 的 Remotes 。
- SubApp1 → SubApp2:接口获取所有 SubApp > 生成基于 Promise 的 Remotes 。