更多请关注微前端专题https://codeteenager.github.io/Micro-Frontends/
介绍
微前端官网:https://micro-frontends.org/
问题:如何实现多个应用之间的资源共享?
之前比较多的处理方式是npm包形式抽离和引用,比如多个应用项目之间,可能有某业务逻辑模块或其他是可复用的,便抽离出来以npm包的形式进行管理和使用。但这样却带来了以下几个问题:
- 发布效率低下:如果需要迭代npm包内的逻辑业务,需要先发布npm包之后,再每个使用了该npm包的应用都更新一次npm包版本,再各自构建发布一次,过程繁琐。如果涉及到的应用更多的话,花费的人力和精力就更多了。
- 多团队协作容易不规范:包含通用模块的npm包作为共享资产,每个人拥有它,但在实践中,这通常意味着没有人拥有它,它很快就会充满杂乱的风格不一致的代码,没有明确的约定或技术愿景。
这些问题让我们意识到,扩展前端开发规模以便多个团队可以同时开发一个大型且复杂的产品是一个重要但又棘手的难题。因此,早在2016年,微前端概念诞生了。
微前端概念
“微前端”一词最早于 2016 年底在 ThoughtWorks Technology Radar 中提出,它将后端的微服务概念扩展到了前端世界。微服务是服务端提出的一个有界上下文、松耦合的架构模式,具体是将应用的服务端拆分成更小的微服务,这些微服务都能独立运行,采用轻量级的通信方式(比如 HTTP )。
微前端概念的提出可以借助下面的 Web 应用架构模式演变图来理解。
最原始的架构模式是单体 Web 应用,整个应用由一个团队来负责开发。
随着技术的发展,开发职责开始细分,一个项目的负责团队会分化成前端团队和后端团队,即出现了前后端分离的架构方式。
随着项目变得越来越复杂,先感受到压力的是后端,于是微服务的架构模式开始出现。
随着前端运行环境进一步提升,Web 应用的发展趋势越来越倾向于富应用,即在浏览器端集成更多的功能,前端层的代码量以及业务逻辑也开始快速增长,从而变得越来越难以维护。于是引入了微服务的架构思想,将网站或 Web 应用按照业务拆分成粒度更小的微应用,由独立的团队负责开发。
从图上可以看出,微前端、微服务这些架构模式的演变趋势就是不断地将逻辑进行拆分,从而降低项目复杂度,提升可维护性和可复用性。
所以说微前端是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。有一个基座应用(主应用),来管理各个子应用的加载和卸载。
微前端的应用场景
从上面的演变过程可以看出,微前端架构比较适合大型的 Web 应用,常见的有以下 3 种形式。
- 公司内部的平台系统。这些系统之间存在一定的相关性,用户在使用过程中会涉及跨系统的操作,频繁地页面跳转或系统切换将导致操作效率低下。而且,在多个独立系统内部可能会开发一些重复度很高的功能,比如用户管理,这些重复的功能会导致开发成本和用户使用成本上升。
- 大型单页应用。这类应用的特点是系统体量较大,导致在日常调试开发的时候需要耗费较多时间,严重影响到开发体验和效率。而且随着业务上的功能升级,项目体积还会不断增大,如果项目要进行架构升级的话改造成本会很高。
- 对已有系统的兼容和扩展。比如一些项目使用的是老旧的技术,使用微前端之后,对于新功能的开发可以使用新的技术框架,这样避免了推翻重构,也避免了继续基于过时的技术进行开发。
微前端的架构模式
微前端架构按集成微应用的位置不同,主要可以分为 2 类:
- 在服务端集成微应用,比如通过 Nginx 代理转发;
- 在浏览器集成微应用,比如使用 Web Components 的自定义元素功能。
服务端集成
服务端集成常用的方式是通过反向代理,在服务端进行路由转发,即通过路径匹配将不同请求转发到对应的微应用。这种架构方式实现起来比较容易,改造的工作量也比较小,因为只是将不同的 Web 应用拼凑在一起,严格地说并不能算是一个完整的 Web 应用。当用户从一个微应用跳转到另一个微应用时,往往需要刷新页面重新加载资源。
这种代理转发的方式和直接跳转到对应的 Web 应用相比具有一个优势,那就是不同应用之间的通信问题变得简单了,因为在同一个域下,所以可以共享 localstorage、cookie 这些数据。譬如每个微应用都需要身份认证信息 token,那么只需要登录后将 token 信息写入 localstorage,后续所有的微应用就都可以使用了,不必再重新登录或者使用其他方式传递登录信息。
浏览器集成
浏览器集成也称运行时集成,常见的方式有以下 3 种。
- iframe。通过 iframe 的方式将不同的微应用集成到主应用中,实现成本低,但样式、兼容性方面存在一定问题,比如沙箱属性 sandbox 的某些值在 IE 下不支持。
- 前端路由。每个微应用暴露出渲染函数,主应用在启动时加载各个微应用的主模块,之后根据路由规则渲染相应的微应用。虽然实现方式比较灵活,但有一定的改造成本。
- Web Components。基于原生的自定义元素来加载不同微应用,借助 Shadow DOM 实现隔离,改造成本比较大。
这也是一种非常热门的集成方式,代表性的框架有 single-spa 以及基于它修改的乾坤。
微前端的优势:
同步更新
对比npm包方式抽离,让我们意识到更新流程和效率的重要性,微前端由于是多个子应用的聚合,如果多个业务应用依赖同一个服务应用的功能模块,只需要更新服务应用,其他业务应用就可以立马更新,从而缩短了更新流程和节约了更新成本。
增量升级
迁移是一项非常耗时且艰难的任务,比如有一个管理系统使用AngularJS开发维护已经有三年时间,但是随着时间的推移和团队成员的变更,无论从开发成本还是用人需求上,AngularJS已经不能满足要求,于是团队想要更新技术栈,想在其他框架中实现新的需求,但是现有项目怎么办?直接迁移是不可能的,在新的框架中完全重写也不太现实。
使用微前端架构就可以解决问题,在保留原有项目的同时,可以完全使用新的框架开发新的需求,然后再使用微前端架构将旧的项目和新的项目进行整合,这样既可以使产品得到更好的用户体验,也可以使团队成员在技术上得到进步,产品开发成本也降到的最低。
独立部署与发布
在目前的单页应用架构中,使用组件构建用户界面,应用中的每个组件或功能开发完成或者bug修复完成后,每次都需要对整个产品重新进行构建和发布,任务耗时操作上也比较繁琐。
在使用了微前端架构后,可以将不能的功能模块拆分成独立的应用,此时功能模块就可以单独构建单独发布了,构建时间也会变得非常快,应用发布后不需要更改其他内容应用就会自动更新,这意味着你可以进行频繁的构建发布操作了。
独立团队决策
因为微前端架构与框架无关,当一个应用由多个团队进行开发时,每个团队都可以使用自己擅长的技术栈进行开发,也就是它允许适当的让团队决策使用哪种技术,从而使团队协作变得不再僵硬。
如何实现微前端
多个微应用如何进行组合?
在微前端架构中,除了存在多个微应用以外,还存在一个容器应用,每个微应用都需要被注册到容
器应用中。微前端中的每个应用在浏览器中都是一个独立的 JavaScript 模块,通过模块化的方式被容器应用启
动和运行。使用模块化的方式运行应用可以防止不同的微应用在同时运行时发生冲突。
在微应用中如何实现路由?
在微前端架构中,当路由发生变化时,容器应用首先会拦截路由的变化,根据路由匹配微前端应
用,当匹配到微应用以后,再启动微应用路由,匹配具体的页面组件。微应用与微应用之间如何实现状态共享?
在微应用中可以通过发布订阅模式实现状态共享,比如使用 RxJS。
微应用与微应用之间如何实现框架和库的共享?
通过 import-map 和 webpack 中的 externals 属性。
微前端落地方案
- 自组织模式:通过约定进行互调,但会遇到处理第三方依赖等问题
- 基座模式:通过搭建基座、配置中心来管理子应用。如基于Single Spa的乾坤方案,也有基于本身团队业务量身定制的方案。
- 去中心模式:脱离基座模式,每个应用之间都可以彼此分享资源,如基于Webpack 5 Module Federation实现的EMP微前端方案,可以实现多个应用彼此共享资源。
其中,目前值得关注的是去中心模式中的EMP微前端方案,既可以实现跨技术栈调用,又可以在相同技术栈的应用间深度定制共享资源。
微前端的建设
微前端不是框架、不是工具/库,而是一套架构体系,它包括若干库、工具、中心化治理平台以及相关配套设施。
微前端包括 3 部分:
- 微前端的基础设施。这是目前讨论得最多的,一个微应用如何通过一个组件基座加载进来、脚手架工具、怎么单独构建和部署、怎么联调。
- 微前端配置中心:标准化的配置文件格式,支持灰度、回滚、红蓝、A/B 等发布策略。
- 微前端的可观察性工具:对于任何分布式特点的架构,线上/线下治理都很重要。
微前端具体要解决好的 10 个问题:
- 微应用的注册、异步加载和生命周期管理
- 微应用之间、主从之间的消息机制
- 微应用之间的安全隔离措施
- 微应用的框架无关、版本无关
- 微应用之间、主从之间的公共依赖的库、业务逻辑(utils)以及版本怎么管理
- 微应用独立调试、和主应用联调的方式,快速定位报错(发射问题)
- 微应用的发布流程
- 微应用打包优化问题
- 微应用专有云场景的出包方案
- 渐进式升级:用微应用方案平滑重构老项目
微前端框架
- Single-SPA:是一个用于前端微服务化的JavaScript前端解决方案,实现了路由劫持和应用加载,但是没有处理样式隔离,js执行隔离。https://github.com/single-spa...
- qiankun:基于Single-SPA提供了更多开箱即用的API(single-spa + sandbox + import-html-entry),做到了与技术栈无关,并且接入简单,靠的是协议接入(子应用必须导出bootstrap、mount、unmount方法)。 https://github.com/umijs/qiankun
- 腾讯的无界:https://github.com/Tencent/wujie
- 腾讯的hel-micro:https://github.com/tnfe/hel
- 美团的Bifrost:Bifrost微前端框架
- 字节的Garfish:https://github.com/modern-js-...
- 阿里的icestark:https://github.com/ice-lab/ic...
- 京东的MicroApp:https://zeroing.jd.com/micro-...
- EMP:https://github.com/efoxTeam/emp
- 阿里云:https://github.com/aliyun/ali...
实现微前端的几种方式:
- 基于single-spa的qiankun:single-spa的实现原理是首先在基座应用中注册所有App的路由,single-spa保存各子应用的路由映射关系,充当微前端控制器Controler,URL响应时,匹配子应用路由并加载渲染子应用。相比于single-spa,qiankun他解决了JS沙盒环境,不需要我们自己去进行处理。在single-spa的开发过程中,我们需要自己手动的去写调用子应用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你传入响应的apps的配置即可,会帮助我们去加载。
- 基于WebComponent的micro-app:micro-app并没有沿袭single-spa的思路,而是借鉴了WebComponent的思想,通过CustomElement结合自定义的ShadowDom,将微前端封装成一个类WebComponent组件,从而实现微前端的组件化渲染。并且由于自定义ShadowDom的隔离特性,micro-app不需要像single-spa和qiankun一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改webpack配置,是目前市面上接入微前端成本最低的方案。
- 基于webpack5提供的Module Federation的hel-micro框架:Module Federation是Webpack5提出的概念,module federation用来解决多个应用之间代码共享的问题,让我们更加优雅的实现跨应用的代码共享。
- 基于iframe的wujie
iframe方案
qiankun技术圆桌中有一篇关于微前端Why Not Iframe的思考,主要有以下几点:
- iframe 提供了浏览器原生的硬隔离方案,不论是样式隔离、 js 隔离这类问题统统都能被完美解决。
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
因为这些原因,最终大家都舍弃了 iframe 方案。
Web Component
MDN Web Components由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。
- Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。
- Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
- HTML templates(HTML模板):
和
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
但是兼容性很差,查看can i use WebComponents。
single-spa 微前端方案
spa 单页应用时代,我们的页面只有 index.html 这一个 html 文件,并且这个文件里面只有一个内容标签
,用来充当其他内容的容器,而其他的内容都是通过 js 生成的。也就是说,我们只要拿到了子项目的容器 和生成内容的 js,插入到主项目,就可以呈现出子项目的内容。
我们只需要拿到子项目的上面四个标签,插入到主项目的 HTML 中,就可以在父项目中展现出子项目。
这里有个问题,由于子项目的内容标签是动态生成的,其中的 img/video/audio 等资源文件和按需加载的路由页面 js/css 都是相对路径,在子项目的 index.html 里面,可以正确请求,而在主项目的 index.html 里面,则不能。
举个例子,假设我们主项目的网址是 www.baidu.com
,子项目的网址是 www.taobao.com
,在子项目的 index.html 里面有一张图片 ,那么这张图片的完整地址是
www.taobao.com/logo.jpg
,现在将这个图片的 img 标签生成到了父项目的 index.html,那么图片请求的地址是 www.baidu.com/logo.jpg
,很显然,父项目服务器上并没有这张图。
解决思路:
- 这里面的 js/css/img/video 等都是相对路径,能否通过 webpack 打包,将这些路径全部打包成绝对路径?这样就可以解决文件请求失败的问题。
- 能否手动(或借助 node )将子项目的文件全部拷贝到主项目服务器上,node 监听子项目文件有更新,就自动拷贝过来,并且按 js/css/img 文件夹合并
- 能否像 CDN 一样,一个服务器挂了,会去其他服务器上请求对应文件。或者说服务器之间的文件共享,主项目上的文件请求失败会自动去子服务器上找到并返回。
通常做法是动态修改 webpack 打包的 publicPath,然后就可以自动注入前缀给这些资源。
single-spa 是一个微前端框架,基本原理如上,在上述呈现子项目的基础上,还新增了 bootstrap 、 mount 、 unmount 等生命周期。
相对于 iframe,single-spa 让父子项目属于同一个 document,这样做既有好处,也有坏处。好处就是数据/文件都可以共享,公共插件共享,子项目加载就更快了,缺点是带来了 js/css 污染。
single-spa 上手并不简单,也不能开箱即用,开发部署更是需要修改大量的 webpack 配置,对子项目的改造也非常多。
运行时框架
运行时框架主要做了以下这些事:
- 应用加载 - 根据注册的子应用,通过给定的 url,加载约定格式的子应用入口,并挂载到给定位置
部分框架是根据类似 manifest 的数据,来获取子应用注册情况以及入口地址
部分框架支持和管理平台配合,运行时接受平台动态注入的入口地址 (也有框架宣称运行时注入和管理平台解耦,但实际是如果不用,就得自己实现注入逻辑)
JS 做入口更纯粹,用 HTML 做入口更易于旧项目改造
业界目前常用两种入口格式, HTML 和 JS
父子入口组合(即确定依赖关系)也有两种模式,构建时组合 和 运行时组合 - 生命周期 - 加载 / 挂载 / 更新 / 卸载 等
加载 / 挂载时做的初始化、权限守卫、i18n 语言等
卸载时做清理,如卸载 script 标签、style 标签、子应用 dom 等
以及路由、父子通信时做双向更新的桥梁 - 路由同步 - 子应用的路由切换时,同步更新 url;url 跳转 / 更新时,同步更新子应用
也就是对子应用做到路由等同于 url - 应用通信 - 是说支持父子应用之间便捷地相互通信,不像 postMessage 那样难用 (指字符串)
什么。 - 沙箱隔离 - 为了各个应用「互补干扰」,需要把各个应用在“隔离”的环境中执行
缺少隔离的话,CSS 全局样式可能 冲突混乱,JS 全局变量可能被 污染 / 篡改 / 替换。 - 异常处理 - 以上所有东西在报错时的统一处理,比如加载失败、或者路由匹配失败
应用加载
App Entry作为子应用的加载入口,微前端框架根据注册的子应用,通过给定的 url,加载约定格式的子应用入口,并挂载到给定位置,目前业内有两种entry: JS Entry 和 Html Entry。
说明 | 优点 | 缺点 | |
---|---|---|---|
html | html作为子应用入口 | 解耦更彻底,子应用不依赖于主应用DOM,子应用独立开发,独立部署 | 多了一次对html的请求,解析有性能损耗,无法做构建时优化 |
js | js作为子应用入口 | 便于做构建时优化 | 依赖主应用提供挂载节点,打包产物体积膨胀,资源无法并行加载 |
Js Entry 的缺点是:
- 子应用更新打包后的 js bundle 名称会变化,主应用需要保证每次获取都是最新的 js bundle
- 子应用所有资源打包到一个文件中,会失去 css 提取、静态资源并行加载、首屏加载(体积巨大)等优化。
- 需要在子应用打包过程中,修改相应的配置以补全子应用 js 资源的路径。
而Html Entry只需要指定子应用的 html 入口即可,微前端框架在加载 html 字符串后,从中提取出 css、js 资源,运行子应用时,安装样式、执行脚本,运行脚本中提供的生命周期钩子。因此优点也很明显:
- 无需关心应用打包后的 js 名称变化的问题。
- 仍然可以享受 css提取、静态资源并行加载(内部使用 Promise.all 并行发出请求资源)、首屏加载等优化。
- 请求资源时,自动补全资源路径。
JS Entry
JS Entry 的方式通常是子应用将资源打成一个 entry script,要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。像single-spa通常是结合SystemJS来实现,在single-spa框架中,基座会检测浏览器url的变化,在变化时往往通过SystemJS的import映射,来加载不同的子应用js。
Import maps
Import maps这个功能是Chrome 89才支持的。它是对import的一个映射处理,让你控制在js中使用import时,到底从哪个url获取这些库。
比如通常我们会在js中,以下面这种方式引入模块:
import moment from "moment"
正常情况下肯定是node_modules中引入,但是现在我们在html中加入下面的代码:
这里/moment/src/moment.js这个地址换成一个cdn资源也是可以的。最终达到的效果就是:
import moment from "/moment/src/moment.js"
有了Import maps,import的语法就可以直接在浏览器中使用,而不再需要webpack来帮我们进行处理,不需要从node_modules中去加载库。
Import maps甚至还有一个兜底的玩法:
"imports": {
"jquery": [
"https://某CDN/jquery.min.js",
"/node_modules/jquery/dist/jquery.js"
]
}
当cdn无效时,再从本地库中获取内容。
尽管Import maps非常强大,但是毕竟浏览器兼容性还并不是很好,所以就有了我们的polifill方案:SystemJS
SystemJS
SystemJs是一个通用的模块加载器,有属于自己的模块化规范。他能在浏览器和node环境上动态加载模块,微前端的核心就是加载子应用,因此将子应用打包成模块,在浏览器中通过SystemJs来加载模块。SystemJS可兼容到IE11,但是它对于插件版本要求非常严格,而且变化非常大,兼容性也不是特别好,使用体验也不是很好,所以目前实践中用的非常少。它同样支持import映射,但是它的语法稍有不同:
在浏览器中引入system.js后,会去解析type为systemjs-importmap的script下的import映射。
Html Entry
HTML Entry 是由 import-html-entry 库实现的,这个库主要做了这些事情:
- 加载 entry html (index.html) 的内容到内存。
- 将 entry html 中的 css、js、link 等标签下的内容获取出来(包含外部的和内联的),整理成网页所需的 js、css 列表。并将无用标签去掉(例如注释、ignore 等)。
- 加载所有外链 js 脚本,并将这些外链 js 和内联 js 一起整理为 script list。
- 加载所有外链 css 文件,并将其以内联(
)的方式插入到 entry html 中。
- 将处理后的 entry html 和待执行的 script list 返回给调用方(基座)。
通过 http 请求加载指定地址的首屏内容即 html 页面,然后解析这个 html 模版得到 template, scripts , entry, styles。
{
template: 经过处理的脚本,link、script 标签都被注释掉了,
scripts: [脚本的http地址 或者 { async: true, src: xx } 或者 代码块],
styles: [样式的http地址],
entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最后一个 script 标签的 src
}
然后远程加载 styles 中的样式内容,将 template 模版中注释掉的 link 标签替换为相应的 style 元素。
然后向外暴露一个 Promise 对象
{
// template 是 link 替换为 style 后的 template
template: embedHTML,
// 静态资源地址
assetPublicPath,
// 获取外部脚本,最终得到所有脚本的代码内容
getExternalScripts: () => getExternalScripts(scripts, fetch),
// 获取外部样式文件的内容
getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
// 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行
execScripts: (proxy, strictGlobal) => {
if (!scripts.length) {
return Promise.resolve();
}
return execScripts(entry, scripts, proxy, { fetch, strictGlobal });
}
}
在 import-html-entry 库处理完之后,基座在需要的加载子应用时候将这个 html 放到对应的 DOM 容器节点,并执行 script list,即完成子应用的加载。
了解更多
路由同步
子应用注册的时候,提供子应用激活规则 (路由字符串 或 函数)。因此,监听 hashchange 和 popstate 事件,在事件回调函数中,根据注册的子应用激活规则,卸载/激活子应用。
以 Vue-Router 的 history 模式为例,在切换路由时,通常会做三件重要事情:执行一连串的 hook 函数、更新url、router-view 更新,其中更新 url,就是通过 pushState/replaceState 的形式实现的。因此重写并增强 history.pushState 和 history.replaceState 方法,在执行它们的时候,可以拿到执行前、执行后的 url,对比是否有变化,如果有,根据注册的子应用激活规则,卸载/激活子应用。
以single-spa为例:
// We will trigger an app change for any routing events.
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
// Monkeypatch addEventListener so that we can ensure correct timing
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, fn) {
if (typeof fn === "function") {
if (
routingEventsListeningTo.indexOf(eventName) >= 0 &&
!find(capturedEventListeners[eventName], (listener) => listener === fn)
) {
capturedEventListeners[eventName].push(fn);
return;
}
}
return originalAddEventListener.apply(this, arguments);
};
window.removeEventListener = function (eventName, listenerFn) {
if (typeof listenerFn === "function") {
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
capturedEventListeners[eventName] = capturedEventListeners[
eventName
].filter((fn) => fn !== listenerFn);
return;
}
}
return originalRemoveEventListener.apply(this, arguments);
};
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
以上主要是增加了hashchange、popstate两个监听,监听url的变化。同时重写pushState以及replaceState方法,在方法中调用原有方法后执行如何处理子应用的逻辑监听hashchange及popstate事件,事件触发后执行如何处理子应用的逻辑。
生命周期
single-spa的一个关键点就是生命周期,子应用生命周期包含bootstrap,mount,unmount三个回调函数。主应用在管理子应用的时候,通过子应用暴露的生命周期函数来实现子应用的启动和卸载。
- load:当应用匹配路由时就会加载脚本(非函数,只是一种状态)。
- bootstrap:应用内容首次挂载到页面前调用。
- Mount:当主应用判定需要激活这个子应用时会调用, 实现子应用的挂载、页面渲染等逻辑。
- unmount:当主应用判定需要卸载这个子应用时会调用, 实现组件卸载、清理事件监听等逻辑。
- unload:非必要函数,一般不使用。unload之后会重新启动bootstrap流程。
沙箱隔离
子应用和基座的隔离主要有两点:
- 样式隔离
- js 隔离
样式隔离
shadowDOM
目前相对来说使用最多的样式隔离机制
BEM、CSS Modules
BEM:Block Element Module命名约束
- B:Block一个独立的模块,一个本身就有意义的独立实体,比如:header、menu、container
- E:Element元素,块的一部分但是自身没有独立的含义,比如:header title、container input
- M:Modifier修饰符,块或者元素的一些状态或者属性标志,比如:small、checked
模块:.Block
模块多单词: .Header-Block
模块_状态: .Block_Modifier
模块__子元素: .Block__Element
模块__子元素_状态: .Block__Element_Modifier
CSS Modules:
代码中的每一个类名都是引入对象的一个属性,通过这种方式,即可在使用时明确指定所引用的 css 样式。并且 CSS Modules 在打包的时候会自动将类名转换成 hash 值,完全杜绝 css 类名冲突的问题;
CSS in JS
使用JS写CSS,也是目前比较主流的方案,完全不需要些单独的 css 文件,所有的 css 代码全部放在组件内部,以实现 css 的模块化,但对于历史代码不好处理
postcss
使用postcss,在全局对所有class添加统一的前缀,但是在编译时处理,会增加编译时间;
JS隔离
js 隔离的核心是在基座和子应用中使用不同的上下文 (global env),从而达成基座和子应用之间 js 运行互不影响。
简单来说,就是给子应用单独的 window,避免对基座的 window 造成污染。
qiankun的沙箱机制
qiankun在js隔离上,同样提供了3种方案,分别是:
- LegacySandbox - 传统js沙箱,目前已弃用,需要配置sandbox.loose = true开启。此沙箱使用 Proxy 代理子应用对 window 的操作,将子应用对 window 的操作同步到全局 window 上,造成侵入。但同时会将期间对 window 的新增、删除、修改操作记录到沙箱变量中,在子应用关闭时销毁,再根据记录将 window 还原到初始状态。
- ProxySandbox - 代理js沙箱,非IE浏览器默认使用此沙箱。和 LegacySandbox 同样基于 Proxy 代理子应用对 window 的操作,和 LegacySandbox 不同的是,ProxySandbox 会创建一个虚拟的 window 对象提供给子应用使用,哪怕是在运行时,子应用也不会侵入对 window,实现完全的隔离。
- SnapshotSandbox - 快照 js 沙箱,IE 浏览器默认使用此沙箱。因为 IE 不支持 Proxy。此沙箱的原理是在子应用启动时,创建基座 window 的快照,存到一个变量中,子应用的 window 操作实质上是对这个变量操作。SnapshotSandbox 同样会将子应用运行期间的修改存储至 modifyPropsMap 中,以便在子应用创建和销毁时还原。
基于iframe的沙箱机制
iframe 标签可以创造一个独立的浏览器级别的运行环境,该环境与主环境隔离,并有自己的 window 上下文;在通信机制上,也可以利用 postMessage 等 API 与宿主环境进行通信。具体来说,在执行 JavaScript 代码上,有以下要求:
- 应用间运行时隔离:常见的是使用shadowDOM创建的样式隔离DOM,再使用Proxy拦截JS的执行,代理到shadowDOM所创建的隔离开的DOM上;
- 应用间通信:同域:window.parent,不同域:postMessage;或者eventBus等自定义的方式实现;
路由劫持:
- a. 让 JavaScript 沙箱内路由变更操作在主应用环境生效:但这种对于相对路径的配置,如接口请求处理太繁琐,一般不建议;
- b. 同步沙箱内路由变化至主应用环境:监听iframe路由上下文,同步到主应用路由上,如wujie;
应用通信
- 基于URL来进行数据传递,但是传递消息能力弱
- 基于CustomEvent实现通信
- 基于props主子应用间通信
- 使用全局变量、Redus进行通信
异常处理
当运行中发生错误时,需要对其进行捕获,这里主要监听了error和unhandledrejection两个错误事件。
window.addEventListener('error', errorHandler);
window.addEventListener('unhandledrejection', errorHandler);
相关文章
- 微前端在平台级管理系统中的最佳实践
- 微前端在银行系统中的实践
- 如何落地微前端一体化运营工作台
- 微前端在金融的实践应用
- 字节跳动是如何落地微前端的
- 基于 iframe 的微前端框架 —— 擎天
- 百度关于 EMP 的探索:落地生产可用的微前端架构
- 微前端在得物客服域的实践 | 那么多微前端框架,为啥我们选Qiankun + MF
- 京东出品微前端框架MicroApp介绍与落地实践
- 爱番番微前端框架落地实践
- 推开“微前端”的门
- 如何设计实现微前端框架-qiankun
- 目标是最完善的微前端解决方案 - qiankun 2.0
- iframe 接班人-微前端框架 qiankun 在中后台系统实践
- 微前端在网易七鱼的实践
- 微前端在美团外卖的实践
- 网易严选企业级微前端解决方案与落地实践
- 有赞美业微前端的落地总结
- 前端搞微前端 | 伟林 - 如何分三步实施微前端
- 一体化微前端研发平台的探索和实践
- 美团质效工具产品的微前端实践
- 从场景倒推我们要什么样的微前端体系
- 微前端架构体系思考与实践
- 开源 | 携程度假零成本微前端框架-零界
- 微前端在网易LOFTER中后台业务中的实践(一)——微前端沙箱及微前端应用平台
- 微前端在民生 APaaS/PSET 平台的探索与实践
- 微前端技术在游戏平台后台系统的实践
- ICE 在微前端的探索
- 细述字节的微前端体系
- 飞马-中后台微前端页面搭建平台
- 手把手教你定制一套适合团队的微前端体系
- 微前端如何帮助我们专注业务需求
- 面向大型工作台的微前端解决方案 icestark
- 前端搞微前端 | 鲲尘 - 如何在大型应用中架构设计微前端方案
- 前端搞微前端 | 时光 - 如何在字节设计与实践微前端沙盒
- 标准微前端架构在蚂蚁的落地实践
- 云前端新物种-微前端体系
- 拥抱云时代的前端开发架构—微前端
- 每日优鲜供应链前端团队微前端改造
- 微前端技术原理
- 基于 qiankun 的微前端应用实践
- 浅谈微前端在滴滴车服中的应用实践
- 如何设计微前端中的主子路由调度
- 如何“取巧”实现一个微前端沙箱?
- Garfish 微前端实现原理
- 快手运营中台的通用微前端治理框架(上)
- 快手运营中台的通用微前端治理框架(下)
- 微前端技术在商羚商户端的实战
- 如何打造跨团队微前端生态
- 模块联邦sdk化微模块方案 - hel-micro
- 基于 MF 的组件化共享工作流
- 企业级微前端研发模式的探索
- 阿里本地生活微应用那些事
- 模块联邦在微前端架构中的实践