1、微前端的实现方案
微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。
微前端目标直指巨石应用业务难题,旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用 (Frontend Monolith) 后,随之而来的应用不可维护的问题
isMicroApp 的变量可以控制启动微前端渲染还是独立 APP 渲染,这个实际是 qiankun 客户端启动后,会在 window 上挂在 __POWERED_BY_QIANKUN__ 变量标识运行时环境。
qiankun 的渲染过程:微应用的 JS CSS 文件请求是属于 Ftech/XHR 类型,说明 js 文件的请求是 qiankun 客户端自行构造的。至于为何要这么做?我的理解是为了实现微前端的沙箱功能。
这里必然要涉及前端的跨域问题,尤其是当主应用和微应用的域名不一致时,qiankun 客户端如何能够在跨域的限制之下获取到微应用的页面资源?
在 qiankun 的框架下,一个页面集成到另外一个页面系统中,最关键的核心点就是将微应用封装成具有生命周期的页面组件,使得 qiankun 可以调用 React 或者 Vue 的 render 能力,将页面渲染到对应的 DOM 节点。
qiankun 的核心概念
第一步则是注册路由,确定微前端启动的时机和启动渲染的内容(when 和 where)。
主流的沙箱模式是通过创建一个独立的作用域隔离作用域链,同时克隆全局变量来实现的,但是这种隔离 + 克隆方案并不完美,在复杂运行场景中,无论性能还是安全性都是难以保证的,特别是 CSS 的隔离。
无界微前端
模块联邦在微前端架构中的实践
为什么是Webpack模块联邦?
当我们研究了所有的替代方案后,出于以下原因,选择Webpack模块联邦更有意义。
-
没有维护成本(如果你自己建立一个架构,会有维护成本)
-
没有团队特定的学习成本(如果你自己建立一个架构,会有学习成本)
-
向模块联邦过渡的成本很小
-
不需要对每个项目进行重新架构
-
所有的需求都在构建时得到满足
-
在运行时不需要额外的工作
-
分享依赖的成本低
-
库/框架独立
-
你不需要处理所有的压缩和缓存问题
-
你不需要处理路由问题
-
Shell和Micro Apps不是紧耦合的,而是松耦合的
模块联邦的两个伟大的功能了 :)
expose:它允许你从任何应用程序到另一个应用程序共享一个组件、一个页面或整个应用程序。你所暴露的一切都被创建为一个单独的构建,从而创造了一个自然的tree shaking
。每个构建都以文件的MD5哈希值命名,所以你不必担心缓存的问题。
remote: 它决定了你将从哪些应用程序接收一个组件、一个页面或应用程序本身。 每个应用程序都可以同时暴露和定义一个远程,并多次进行。
webpack 模块联邦,动态加载远程模块,远程容器接口支持 get 和 init 方法。
- Webpack 静态引入的实现逻辑,如
import App from './App'
- Webpack 的动态引入原理,也就是动态 import 是怎么实现的,如
import('./App')
Runtime 又叫做运行时,它的作用是串联起各个模块,包括引入模块、下载模块、一些基础的公共方法。通过 Runtime 作为桥梁,我们就能把各个模块联系起来,最终让被 Webpack 打包的应用在浏览器跑起来。
模块被打包后可能需要考虑下面三点:
- 独立的模块作用域,两个模块之间不应该互相影响
- 缓存机制,模块被加载过一次就不用再发起请求了(组件懒加载)
- 环依赖问题
dist 目录有两个 JS 文件:main.js、runtime.js。运行时的代码都在 runtime.js,而我们模块内容相关的都在 main.js。由 index.html 控制二者的下载:
1 2 |
|
可以注意到先下载了 runtime.js, 再下载 main.js。这是必须的,因为首先我们需要在注册一些全局变量,注册好了之后,main.js 才可以通过全局变量来和运行时进行交互。加 defer 的作用是可以不阻塞 DOM 树的解析,异步下载内容,可以减少白屏时间(First Content Paint)。
__webpack_require__.o
:这个就是判断判断 key 值有没有在对象本身上:
__webpack_require__.m
: 它维护的是所有的模块,因为我们可能有 main.js,main-1.js 都有模块需要管理,这时候就通过它去统一的注册上我们的模块里去。
ContainerPlugin用于解析Container的配置信息,ContainerReferencePlugin用于两个或多个不同Container的调用关系的判断,SharePlugin是共享机制的实现,通过ProviderModule和ConsumerModule进行模块的消费和提供
ContainerPlugin的核心是实现容器的模块的加载与导出,从而在模块外侧进行一层的包装为了对模块进行传递与依赖分析
ContainerReferencePlugin核心是为了实现模块的通信与传递,通过调用反馈的机制实现模块间的传递
sharing的整个模块都在实现共享的功能,其利用Provider进行提供,Consumer进行消费的机制,将共享的数据隔离在特定的shareScope中
webpack5的模块联邦是在通过自定义Container容器来实现对每个不同module的处理,Container Reference作为host去调度容器,各个容器以异步方式exposed modules;对于共享部分,对于provider提供的请求内容,每个module都有一个对应的runtime机制,其在分析完模块之间的调用关系及依赖关系之后,才会调用consumer中的运行时进行加载,而且shared的代码无需自己手动打包。webapck5的模块联邦可以实现微前端应用的模块间的相互调用,并且其共享与隔离平衡也把控的较好
Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了!
我们知道 Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。
模块联邦是 Webpack5 新内置的一个重要功能,可以让跨应用间真正做到模块共享
参考文档:https://blog.towavephone.com/module-federation-principle-research/
webpack_require,而这个函数是 webpack 打包后的一个核心函数,就是解决依赖引入的。
function __webpack_require__(moduleId) {
// Check if module is in cache
// 先检查模块是否已经加载过了,如果加载过了直接返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
// 如果一个import的模块是第一次加载,那之前必然没有加载过,就会去执行加载过程
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
});
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
Module federation: 允许运行时动态决定代码的引入和加载。
建议使用异步边界(asynchronous boundary)。它将把初始化代码分割成更大的块,以避免任何额外的开销,以提高总体性能。
模块联邦实现原理:
(1)以app1使用app2导出的Button组件为例,来说明模块联邦内部的实现原理,究竟是怎样将两个不同应用之间的模块关联起来的。
(2)app2在配置提供Button组件时,需要在expose中指明组件名称和组件在文件中路径的对应关系,将打包后的文件命名为app2Entry,最后打包生成的文件导出的是app2的全局变量,这个全局变量上挂载了一个MoudleMap对象,它维护了组件名和组件的地址关系,另外也挂载了一个get方法,这个方法通过传入的模块名,先去moduelMap中找到模块打包后真正的所在地址,然后进行资源的加载。
(3)app1首先会通过模块联邦的配置,将app2打包后生成的app2Entry.js文件加载进来,这样其实在app1中就已经拿到了app2这个全局变量,以及挂载的ModuleMap对象和get方法。
(4)app1在使用app2提供的Button组件时,实际是通过webpack_require方法调用Button组件的Id, 在该方法中会先判断缓存中是否存在,不存在的话,会调用_webpack.require.f.remote方法,该方法接收chunckId参数,然后在chunckMapping中查找该文件下,都使用了app2的那些组件,chunckMapping维护的是chunckId和模块ID的映射关系,找到模块ID之后,在IdNameMapping中获取到该模块所在的应用名,以及模块名,_webpack.require.f.remote底层实际调用的事app2.get(Button)的方法,通过全局变量的关联,获取到app2提供的组件。
相关文章:https://blog.towavephone.com/module-federation-principle-research/
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
- 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
- 🛡 样式隔离,确保微应用之间样式互相不干扰。
- 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
微前端架构具备以下几个核心价值:
-
技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权 -
独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 -
增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
-
独立运行时
每个微应用之间状态隔离,运行时状态不共享
qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。
qiankun 会在跑子应用之前在 window 沙箱设置 POWERED_BY_QIANKUN 的变量,如果有这个变量就不要直接渲染,在 mount 生命周期里做渲染,否则就直接渲染。
还要指定静态资源的加载地址,通过 webpack_public_path 的全局变量。
Why Not Iframe
为什么不用 iframe,这几乎是所有微前端方案第一个会被 challenge 的问题。但是大部分微前端方案又不约而同放弃了 iframe 方案,自然是有原因的,并不是为了 "炫技" 或者刻意追求 "特立独行"。
如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。
iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
1url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
2UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
3全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
4慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。
主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start
即可。
Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。
在单实例模式下,你可以使用 excludeAssetFilter
参数来放行这部分资源请求,但是注意,被该选项放行的资源会逃逸出沙箱,由此带来的副作用需要你自行处理。
如何解决拉取微应用 entry 时 cookie 未携带的问题
1: mode: 'cors' 开启跨域资源共享
2:credentials: 'include', 在当前域名内自动发送 cookie
SanpshotSandbox和LegacySandbox沙箱,都是 单例模式 下使用的沙箱。也即一个页面中只能同时展示一个微应用,而且无论是 set 还是 get 依然是直接操作 window 对象。
在这样单例模式下,当微应用修改全局变量时依然会在原来的 window 上做修改,因此如果在同一个路由页面下展示多个微应用时,依然会有环境变量污染的问题。
import-html-enry
qiankun 框架配套开发支持 html 作为入口(entry) 的资源加载器(loader)。
工作环境:
- qiankun 框架为了解决 JS Entry 的问题,采用更方便的 HTML Entry 方式,目的让微应用接入像 iframe 一样只需配置页面 html 的 url 地址
2、项目中采用的方案
3、微前端中主子应用数据管理和通信的实现
4、沙箱隔离,沙箱的实现原理JS沙箱和样式沙箱
5、表单生成器的实现和其他方案对比?
6、其他印象比较深的项目和重难点,以及收获
7、