如何设计实现微前端框架-qiankun
前端技术优选
以下文章来源于前端早早聊 ,作者方涣
本文是第七届微前端,前端早早聊第 42 场,来自蚂蚁金服体验技术部 qiankun 的核心贡献者 - 方涣的分享 - 讲稿简要整理版(完整版含演示请看录播视频和 PPT):
一、自我介绍
大家好,今天由我带来前端早早聊本次微前端专题的最后一场,《如何设计实现微前端框架 - qiankun》。首先呢,我做一个简单的自我介绍。
自我介绍
我叫方涣,在一些平台上,比如知乎和语雀上,用的就是左边这个头像。
我现在是蚂蚁金服体验技术部的一名前端工程师,和有知一起维护和迭代 qiankun。现在也正在做蚂蚁内部新一代的前端运行时平台,正是基于微前端技术的。
qiankun 简介
首先开始之前,我先给大家简单的介绍一下乾坤,大家可以在 <qiankun.umijs.org> 的官网上,看到 qiankun 的介绍和 qiankun 的文档,诚如这张官网截图所示,我们对 qiankun 的一句话定义就是:“可能是你见过最完善的微前端解决方案”,它有以下 3 个特点:
· 简单。无论你是什么技术栈,你用的是什么 js 框架,你都可以使用 qiankun 来把你的子应用接入到你的框架应用中,接入非常简单,就像接入一个 iframe 系统一样。
· 完备。我们基于 single-spa 封装了非常多的能力,把你在构建微前端系统中遇到的一些问题,如样式隔离、沙箱、预加载等等这些你需要的能力全部内置到了 qiankun 里面,所以它是一个比较完备的微前端解决方案。
· 生产可用。到目前为止,已经大概有 200+ 的应用,使用 qiankun 来接入自己的微前端体系。我们在蚂蚁内外受过了大量线上系统的考验,所以它是一个值得信赖的生产可用的解决方案。
qiankun 名字由来
很多人可能会好奇 qiankun 这个名字是怎么来的。实际上源自于这句话:小世界有大乾坤。我们希望在微前端这个小世界里面,通过 qiankun 这个框架鼓捣一个大乾坤。
二、QIANKUN 的诞生与设计理念
在第一小节,我会跟大家聊一聊 qiankun 的诞生,关于 qiankun 是怎么来的;然后聊一聊 qiankun 的设计理念,就是 qiankun 怎么看待微前端的,qiankun 认为微前端最重要的是什么?
缘起:一次统一上云战役
qiankun 的源起,实际上是一次统一上云战役,不过这次战役具体是怎么样的并不重要。大家可以看到在这张图里看到我们这次战役所制作的控制台,我们通过红框把它划分成了好几个部分,大家可以看到最顶上的导航头、左边的菜单、右边的帮助引导以及中间的接入产品,这也就是我们子应用的主体。
控制台结构
把控制台抽象一下,它的结构就是这个样子的。
其中顶部的导航头、身份认证和左边的侧边栏、新手引导在内的所有部分加起来就是我们的主应用,或者说是框架应用。而中间那个框也就是应用主体,应用 body 部分,它就是我们接入的一系列子应用,它可能是应用 a、应用 b 、应用 c 等等一系列的应用。
我们把这一系列的应用一起集成到了一个大的控制台应用里面,这就是我们这次所做的事情。而这件事情正好是微前端技术的一个典型应用场景,所以 qiankun 就从这个项目开始了它的第一步。
我们需要一个“友好”的微前端方案
对这个项目而言,我们所需要的微前端方案,可以概括为一个 “友好” 的微前端解决方案,那怎么样算友好的微前端方案呢?我们提炼出两个要求:
技术栈无关。就是说我们的微前端方案不应该限制技术栈,我们不应该要求说我们的子应用是 React 技术栈、Vue 技术栈或者说 Angular 技术栈。除了不应该限制技术栈,我们需要解除应用之间的一些隐性依赖,因为有的时候虽然我们没有限制应用的技术栈,但是我们的应用之间很可能有一些耦合,这些耦合发生在很多方面,我们需要消灭这些耦合,才能做到技术栈无关。 接入简单,用起来就像 iframe 一样。大家知道 iframe 使用是非常简单的,我只要建一个 iframe 标签,把我的页面嵌进去就可以了。如果我们的微前端方案能够做到和 iframe 一样简单,我们就可以尽可能的避免旧应用改造。大家看到在我们之前的那次战役中,也就是我们缘起里,我们需要把非常多的应用接入到框架应用、接入到控制台里,这就涉及到大量的应用改造,而我们希望把应用改造的工作量降到最低,不然的话我们肯定非常疲惫。
技术栈无关
这也正是 qiankun 所认为的微前端的核心价值。后面 qiankun 面临的很多技术选择和最终的实现方式,都是出于技术栈无关的出发点的。当我们思考:“在很多的解决方案中,哪个方案被 qiankun 最后所选用”的时候,我们会想哪个方案是最技术栈无关的,最技术栈无关的方案,往往就是 qiankun 最后所选择的方案。
“技术栈无关”为什么重要
这里举一个例子说明,假如我们要做一个统一控制台,我们不妨就把控制台叫做 ABC,然后控制台需要接入三个不同的项目:项目 A 是一个月前新建的,还处在积极迭代中。项目 B 可能是一年前建的,现在已经缺乏维护。当然最难的可能是项目 C,它可能是一个的三年前项目,现在已经无人维护了。
我列出这几个项目的技术栈,大家会发现它们之间有很大的不同,可能项目 A 现在用着最新的 React 版本,用着最新的技术栈,甚至超前的用了 Webpack5 作为打包工具;与此同时,项目 B 可能就是大家一年前的主流技术选型,而项目 C 已经是非常旧的技术栈了。在这种情况下,如果我们微前端方案是技术栈相关的,那就势必得把项目 B 做升级,得把项目 C 做改造,而这样的成本很多时候是我们在做 ABC 这个项目的时候无法接受的。再者随着时间的正常推移,项目 B 也会变成无人维护的状态,项目 A 技术栈也终归会陈旧。
所以只有做到技术栈无关,才能保证在过了很久之后,我们的统一控制台 ABC,还是能够安稳的继续维护下去,还是能够安稳的接入新的项目。
玉伯有一段话是这么点评微前端的:微前端的前提还是得有主体应用,然后才有微组件或微应用,解决的是可控体系下前端协同开发问题。这里开发问题有两部分:一部分是空间分离带来的协作,另一部分是时间延续带来的升级维护。空间分离带来的协作,需要微前端方案能够提供独立开发、独立部署的特性,来处理协作问题。而时间带来的延续其实是需要我们做到技术栈无关,做到随着时间的推移,当我们技术栈陈旧的时候,还是能够正常的接入我们的框架应用。
“技术栈无关”的价值
可能大家都比较熟悉阿里使命:让天下没有难做的生意。而技术栈无关的价值,也正是 qiankun 口号:让天下没有短命的控制台。
我们不限制接入前端的技术栈,做到接入范围广。 我们能够做到向后兼容,能兼容年久的应用。 在这个技术栈无关的架构下,我们也能做到向前兼容,这个架构是稳定的、面向未来的。
三、QIANKUN 的技术实现与选择
简单的讲了一下 qiankun 所坚持的理念之后,我们接着来聊一聊 qiankun 具体的技术实现和最终选择使用什么方案来实现微前端。
微前端框架面临的两大共性问题
实际上所有的微前端框架都面临这两大共性问题。当你解决了这两大问题之后,你的微前端框架的运行时,就已经基本可用了。
问题一是应用的加载与切换。包括路由的处理、应用加载的处理和应用入口的选择。 问题二是应用的隔离与通信。这是应用已经加载之后面临的问题,它们包括 JS 的隔离(也就是副作用的隔离)、样式的隔离、也包括父子应用和子子应用之间的通信问题。
应用路由与 Future State
首先我们来看应用路由的问题。在微前端体系结构下,路由的划分往往是如图这个样子的,这也是一种比较简单的方案。
我们主应用加载了应用 A 和应用 B。这个时候对于主应用来讲,我就有两个路由 /a/*
和 /b/*
。在路由 A 下我会加载应用 A,在另一个路由 B 下加载应用 B。
那当我在路由 /b/list
下重刷页面的时候会发生什么过程?实际上我需要先加载我的主应用,我的主应用检测到这是一个 /b/
打头的路由,于是它知道我应该去加载应用 B,随后去加载了应用 B。到了此时应用 B 接管了路由,它发现后面的路由是 /list
,于是它显示出 /list
的正确页面,总体上就是这么一个过程。
如果你没有做一些处理的话,在最早加载主应用的时候,就会面临 404 和路由报错,因为这个时候应用 B 的路由系统并没有加载进来,你并不知道 /list
需要显示什么。这实际上是懒加载就会面临的问题。早年 Angular 社区把这个问题叫做 Future State。要解决这个问题,比较简单的方式就是我们去劫持一下路由系统,做一些改造,同样的,通过借助像 react-router 之类的路由库,你也可以选用它的动态路由方案,这些都是可以的。
社区已有的成熟方案
在 qiankun 这里,我们就直接选用了社区成熟的方案 Single-SPA。Single-SPA 已经劫持的路由,帮我们做好了加载这件事情,也帮我们做好了路由切换这件事情,在这个方面我们就没有自己造轮子了。
应用接入:协议接入
同时由于我们使用了 Single-SPA,它顺便帮我们解决另一个问题:就是应用接入的问题。
什么样的一个应用能够成为子应用,能够接入到我们的框架应用里?由于我们需要技术栈无关,所以我们希望接入是一个 协议接入。只要你的应用实现了 bootstrap
、mount
和 unmount
三个生命周期钩子,有这三个函数导出,我们的框架应用就可以知道如何加载这个子应用
这三个钩子也正好是子应用的生命周期钩子。当子应用第一次挂载的时候,我们会执行 bootstrap
做一些初始化,然后执行 mount
将它挂载。如果你是一个 React 技术栈的子应用,你可能就在 mount
里面写 ReactDOM.render
,把你的 ReactNode 挂载到真实的节点上,把应用渲染出来。当你应用切换走的时候,我们会执行 unmount
把应用卸载掉,当它再次回来的时候(典型场景:你从应用 A 跳到应用 B,过了一会儿又跳回了应用 A),这个时候我们是不需要重新执行一次所有的生命周期钩子的,我们不需要从 bootstrap
开始,我们会直接从 mount
阶段继续,这就也做到了应用的缓存。
App Entry 抉择
在这个方面,qiankun 还面临着另一些选择,其中一个就是 App Entry 选择,也就是如何设计子应用的加载入口,在这个问题上,我们有两个地方需要面临抉择:一个是什么时候去组合,另一个是加载子应用的入口是什么。
组合时机选择
在组合时机的选择上,我们有两个选择:第一个是在构建时,把主子应用打包在一起,着实际上就是一种多包的方案。这个方案它的好处是构建的时候可以做公共依赖的提取,但是它的缺点在于我们把主子应用构建方案和工具都耦合在一起了,这非常不灵活地,这样也没办法做到动态加载。所以在绝大部分情况下,我们都会选择运行时去组合,就是运行的时候才去动态的加载子应用,把它加载、渲染到框架应用里。
应用入口选择:HTML Entry
那我们子应用的入口又如何选择?qiankun 在这里选择的是 HTML,就是以 HTML 作为入口。
大家看了刚才的分享,其实已经清楚了:加载子应用用的时候,我们实际上是需要提供一份资源列表,在这份资源列表里,我们可能列出了这个子应用使用了哪些 JS,有哪些 CSS。
但是 qiankun 的第一选择其实是 HTML 入口,就是提供一份 HTML 文件。因为这份 HTML 中其实包含了子应用的所有信息。它包含网页的结构,包含了一些元信息。大家可以看到在这份 HTML 里我们有 CSS、JS 链接、有应用要挂载的根路由 root 的 DOM。这些信息是非常全面的,比单纯你拿 JS 和 CSS 组成一份资源列表作为入口,要清晰和完整得多。同时 HTML Entry 这样的设计,也使得我们在接入一些老旧应用的时候,更加简单。
应用入口选择
这里做一个简单的对比,用了 HTML Entry 可以把子应用和主应用更加的解耦了。但是如果你是选择资源列表或者 JS 作为子应用的入口,那么子应用挂载的时候,DOM 节点往往需要和主应用作一个约定,这就产生了一定程度的耦合。
应用隔离:JS沙箱
实际上刚才那些抉择完了之后,尤其是当我们借用了 Single-SPA 能力之后,我们已经基本解决了应用的加载与切换。我们接下来要解决的另一块事情是应用的隔离和通信。
首当其冲就是应用的隔离问题,隔离问题最开始我们就是要做 JS 的隔离。在这个问题上,有的时候你不隔离其实也可以运行的,只不过不隔离,直接裸用 Single-SPA 的话,一旦应用之间的发生了冲突,你的应用很可能就跑不下去了,所以 JS 沙箱大部分情况是必要的。
再 qiankun 沙箱的实现里,我们会有两个环境:一个代表的是外部的部分,就是我们的全局环境 Global Env,指的是你框架应用所运行的全局环境。而子应用加载的时候,实际上是应该跑在一个内部的沙箱环境里面,就是这张图上所表示的 Render Env。
沙箱实现思路有两条:其中最经典的实践思路其实是快照沙箱。
快照沙箱就是在应用沙箱挂载和卸载的时候记录快照,在应用切换的时候依据快照恢复环境,
当我子应用加载、启动了之后,此时的环境其实就是 Render Environment,是内部沙箱环境里,这个时候我记录一下当时的快照状态。而在我最后子应用 unmount 的时候,我把当前的环境和记录的快照进行一个对比,把它恢复回原来的全局状态。这就是说当我应用挂载了又卸载了,这个过程走了一遍之后,我当前整个 Windows 运行环境恢复成原来的样子,应用内部所做的修改,在它卸载的时候就会被恢复,这是快照沙箱思路。
举个例子,假如我们在 A 应用运行时,追加了一个全局变量,我们说 window.a = 123
,这个时候 window.a
就变成了 123
,但是在应用 A 卸载之后,快照还原, window.a
会被重新删除,你在全局环境中并不会继续看到 a
变量
快照怎么打?其实也有两种思路:
一种是直接用 windows diff。把当前的环境和原来的环境做一个比较,我们跑两个循环,把两个环境作一次比较,然后去全量地恢复回原来的环境。
另一种思路其实是借助 ES6 的 proxy 就是代理。通过劫持 window ,我们可以劫持到子应用对全局环境的一些修改。当子应用往 window 上挂东西、修改东西和删除东西的时候,我们可以把这个操作记录下来。当我们恢复回外面的全局环境的时候,我们只需要反向执行之前的操作即可。比如我们在沙箱内部设了一个新的变量 window.a = 123
。那在离开的时候,我们只需要把 a 变量删了即可。
快照沙箱这个思路也正是 qiankun1.0 所使用的思路,它相对完善,但是缺点在于无法支持多个实例,也就是说我两个沙箱不可能同时激活,我不能同时挂载两个应用,不然的话这两个沙箱之间会打架,导致错误。
所以说我们把目光又投向了另一条思路,我们让子应用里面的环境和外面的环境完全隔离。就如图所示,我们的 A 应用就活在 A 应用的沙箱里面,B 应用就活在 B 应用的沙箱里面,两者之间要不发生干扰,这个沙箱的实现思路其实也是通过 ES6 的 proxy,也通过代理特性实现的。
应用隔离:样式隔离
无论是刚才的快照沙箱还是另外的代理沙箱,其实都可以解决 JS 之间的副作用冲突。解决完 JS 之后,我们接下来解决的是 CSS 之间的冲突。
在微前端框架里所面临的样式冲突器就两种:一种是主子应用样式冲突,你的主应用和你的子应用两者之间样式会发生冲突,另一种是子应用之间样式冲突,当你挂载了应用 A 又挂载了应用 B 的时候,这两个应用是平级的,它们之间样式也会发生冲突。
样式隔离:Dynamic Stylesheet
qiankun 所做的第一件事情其实是动态样式表。当你从子应用 A 切换到子应用 B 的时候,这个时候需要把子应用的样式表 A 的样式给删除,把子应用 B 的样式表给挂载。这样就避免了子应用 A 的样式和子应用 B 的样式同时存在于这个项目中,就做到了最基本的隔离。当你从应用 A 切到应用 B 的时候,你的样式也会自然的从应用 A 的样式切换到应用 B 的样式,我们会自动的帮你做应用样式的删除和加载。当然这样做只仅仅能够保证你在单应用模式下(就是同时只能有一个应用活跃的情况下)保证子应用和子应用之间的样式不会冲突。
当然动态样式表实现之后,我们还是没有解决主应用和主应用之间的样式冲突。实际上这个问题最好的手段还是通过一些工程化的手段来解,比如说 BEM,就是说大家约定一下,你应用 A 样式就统一加一个 a-
的前缀,应用 B 样式就统一加一个 b-
的前缀,你的框架应用也统一加个前缀。大家通过一些约定(比如约定大家都加上一个应用名的前缀)来避免冲突,这是非常有效的一个方案。尤其是主子应用之间的冲突,大部分情况下你只要保证主应用的样式做好改造,保证主应用的样式是很具象的规则,不会跟子应用冲突,那主子应用之间的冲突其实也解决了。不过这种方案终归是依赖约定,往往容易出纰漏。
样式隔离:工程化手段
当前 css module 其实非常成熟一种做法,就是通过编译生成不冲突的选择器名。你只要把你想要避免冲突的应用,通过 css module (在构建工具里加一些 css 预处理器即可实现)就能很简单的做到。css module 构建打包之后,应用之间的选择器名就不同了,也就不会相互冲突了。
css-in-js 也是一种流行的方案,通过这种方式编码的样式也不会冲突,这几个方案实现起来都不复杂,而且都非常行之有效。所以绝大部分情况下,大家手动用工程化手段处理一下主子应用之间的样式冲突,就可以解决掉样式隔离的问题。
样式隔离方案:Shadow DOM
当然来到 qiankun2.0 之后,我们追加了一个新的选项,叫作严格样式隔离,不知道大家有没有使用过。
其实严格样是隔离代表 Shadow DOM。Shadow DOM 是可以真正的做到 CSS 之间完全隔离的,在 Shadow Boundary 这个阴影边界阻隔之下,主应用的样式和子应用的样式可以完完全全的切分开来。
但是绝大部分情况下,你还是不能无脑的开启严格样式隔离的。原因之前的同学也已经提到过一些了,比如说你在使用一些弹窗组件的时候(弹窗很多情况下都是默认添加到了 document.body
)这个时候它就跳过了阴影边界,跑到了主应用里面,样式就丢了。又比方说你子应用使用的是 React 技术栈,而 React 事件代理其实是挂在 document
上的,它也会出一些问题。所以实践里当你开启 Shadow DOM 之后,当你的子应用可能会遇到一些奇怪的错误,这些错误需要你一一的去手动修复,这是比较累的一个过程。
我们提供了 Shadow DOM 这么一种样式隔离方式。但是实际使用中还是工程化的手段最为可靠、最为简单。当然在允许的情况下,大家还是可以去尝试开启严格样式隔离,毕竟这才是真正的隔离。
样式隔离 RFC:runtime css transformer
qiankun 还收到过一个提案,让我们可以动态运行时地去改变 css。
如图,比如说原来子应用里面的样式是这个样子,那转换之后,我们可以把它前面加一个限定,我们规定只有在 data-qiankun-app1
这个子应用里面(这个 div
就是包裹子应用的最外层的容器), .main
这条样式才能真正的生效。通过这样一个运行时的转换,我们也能够达到样式隔离的目的。当然这个方案的问题在于这个转换不是那么好做,首先我们增加了一定的运行时开销,其次对于一些媒体查询,对于一些动画很可能会出一些意料之外的情况。这个 RFC 至今至今没有合并,不知道大家对此有什么看法?欢迎来到 PR 下发表你的见解。
在我们通过 JS 沙箱解决 JS 之间的副作用;通过不管用工程化手段也好,动态样式也好,或者说开启 Shadow DOM,解决掉 CSS 冲突之后,我们就要考虑应用之间的通信问题了。
应用通信:基于 URL
其实有一种最朴素的通讯方案,就是基于 URL。前端有一种设计叫做 URL 中心设计,就是说你的 URL 完全决定了你页面展示出来是什么样子。
假如我的应用里有一个列表,有一个分页,当你点下一页的时候,是不是就产生了一个在第二页的 query 参数?你可能会把这个参数同步到路由上,这样你把这个链接分享给别人的时候,别人就能看到跟你一样的页面。
我们其实完全可以把这种路由翻译成看作是一个函数调用,比如说这里的路由 b/function-log,query 参数 data 是 aaa
,我们可以把这个路由 URL 理解为我在调用 B 应用的 log
函数,这就像一次函数调用一样。当我们从应用 A 跳去应用 B,对应路由发生变化的时候,就是触发了一次函数调用,触发了一次通信。
所以路由实际上也有通信的功能。这种通信方式是完全解耦的,但是缺点就是比较弱。
应用通信:发布/订阅模型
另一种应用间通信的模型就是我们可以挂一个事件总线。应用之间不直接相互交互,我都统一去事件总线上注册事件、监听事件,通过这么一个发布订阅模型来做到应用之间的相互通信。
有趣的是,我们什么框架都不需要引入,什么第三方库都不需要引入,这里我们有一个天然的事件总线:window 的 CustomEvent
。我们可以在 window 上监听一个自定义事件,然后在任意地方派发一个自定义事件,我们可以天然的通过自定义事件来做到应用之间相互通信。
应用通信:基于 props
第三种方案其实是基于 props
,这也是我们比较熟悉的模式了。
大家写过 React 或 Vue 都会知道,我们在写一个 input
的时候,会把 value
和 onChange
两个 props
都传给了底下的输入框。这里我们也可以同样做,我们主应用是可以传递一些 props
给子用的。我们把 state
和 onGlobalStateChange
(就是监听函数),还有我们的 onChange
(就是 setGlobalState
)三个都传给子应用。我们基于 props
也就可以实现一个简单的主子应用之间通信。
那当我们这样子实现了主子应用之间通信之后,我们子应用与子应用之间通信怎么做?让大家都跟主应用通信就行了。子应用和子应用之间就不要再多加一条通信链路了,我们大家都基于 props
和主应用通信,这样也能解通信问题。
应用通信
qiankun 本身在 2.0 的时候提供了最简单的基于 props
通信的 API。
在我们提供 API 之前,就有非常多的同学来问我们应该应用和应用之间怎么通信?我们当时都没有给出具体的解决方案,给出具体的 API 或者给出具体的实践和引导。实际上在你的应用需求、复杂度不同的时候,你应该相应的自己挑选自己适合的应用间通信方案。
对 qiankun 来看,qiankun 关注的是你应用和应用之间是不是技术栈无关的,也就是说应用和应用之间的耦合是不是尽可能弱的。从这个角度来讲,如果我们基于自定义事件来做通信,那是一个非常弱的耦合。我不依赖什么特殊的东西,我只是借用了浏览器原生 API,技术实现总线,当我脱离它的时候,我的应用也是能够自己独立的工作的。而如果我们约定一个 window 上的全局变量,或者说我们做了一个全局 Redux,大家都去这个数据源消费数据,这个时候这个方案就可能是一个比较强耦合的方案。
具体用什么样的通信方案,还是需要根据大家的实际情况和面临的业务场景自行决定。
qiankun 没有解答的工程与平台问题
大家可以看到 qiankun 帮你做了很多事情,但是这个事情并没有帮你一揽子全部做完,并不是用了 qiankun 之后,就完全地一劳永逸地解决了微前端的所有麻烦。#克军 在 D2 的微前端的专场分享的时候,有提到过微前端的整个大图,他把整个微前端体系分层了非常多的模块,qiankun 所帮你解决的这一块实际上是微前端的运行时容器,这是整个微前端工程化里面的一个环节。
从这个角度来讲,qiankun 不算是一个完整的微前端解决方案,而是微前端运行时容器的一个完整解决方案,当你用了 qiankun 之后,你能解决几乎所有的微前端运行时容器的问题,但是更多的一些涉及工程和平台的问题,
你的版本管控、配置下发、监控发布,安全检测、等等这些怎么做,都不是 qiankun 作为一个库所能解答的。这些问题还是得看大家的具体情况,来选择自己适合的解决方案。这也需要在微前端这方面做一些基础设施建设的投入,才能比较好的解答工程问题与平台建设问题。
四、QIANKUN 的发展与未来
聊了一下 qiankun 具体的一些技术实现的方案选择,最后一节聊一下 qiankun 的发展情况,与 qiankun 未来会做成什么样子。
qiankun 历程回顾
先做一个简单的历程回顾。
qiankun 是在去年 6 月份发布的 1.0 版本,那个时候全平台的官宣文章可能是你见过最完善的微前端解决方案。 在去年 12 月的时候,在 D2 技术论坛上,在微前端专场里面,大家可以找到有之的演讲,标准微前端架构在蚂蚁的落地实践。 今年 4 月份的时候,我们发布了 2.0 版本。在这两篇文章和演讲中,大家可以更详细的了解到 qiankun 设计的一些思考和 qiankun 的落地实践。 到今天为止,qiankun 大概已经累积了 5.1k star。我们也受到了来自 Single-SPA 团队的肯定,我们也被非常多的团队,比如说像之前的飞猪团队选用为他们微前端体系中里面的一环。
微前端的两种形态
实际上在最早 qiankun 1.0 发布的时候,我们就觉得微前端是有两种形态的。
在我们缘起所解决的业务场景里面,在 qiankun 第一次出场的时解决其实是一个单实例的场景。我们控制台上同时运行的只有一个应用,我们会从应用 A 切换到应用 B,从应用 B 切换到应用 C,但我们不会同时把应用 A 和应用 B 都挂载在页面上,这是一种单实例的场景。
实际上我们觉得微前端还是有一种多实例场景。你在一个页面里完全可以挂载多个微应用,你可以同时把应用 A、应用 B、应用 C 和应用 D 合计 4 个应用全部在同一个页面里。
这个时候就面临一个问题,应用和组件有什么区别?这也很像之前的几位同学所提到的,weiget 或者微模块,因为在这种情况下子应用往往是没有自己的路由的,与路由无关,它确实在某种程度上很像一个组件,页面的这一部分是一个组件或者说是一个微应用,表现出来的差距并不大。
那怎么样说你是一个组件,又怎么样说你是一个微应用?对我们来说,我们认为能独立开发,独立部署,自己是能够独立完成一些功能的,其实就是一个微应用。它和 weiget 或者说微模块能做到的事情是类似的,两者之间并没有明确的界限,比如说你完全可以把一个组件升格为一个应用,或者把一个应用做成一个组件,这看你的具体场景和选择。
qiankun@2.x 定位转变
当我们 qiankun2.0 决定支持多应用多实例场景的时候,实际上 qiankun 的定位也在悄然发生一定的转变,这也正是 qiankun2.0,没有改特别多的 API 保持了大部分兼容兼容的,却说是一次重大的变化的原因。
我们原先是一个基于路由的微前端框架,而现在实际上可以说是一个微应用加载器。你可以用 qiankun@2.x,更灵活的加载微应用,我们提供了组件化的加载能力,用来支持做更复杂的应用编排,并且支持更多的微前端应用场景。
qiankun@2.x 加载微应用
我们给大家看一下代码:如何用 qiankun2.0 来加载一个微应用。
我们提供提供了一个新的 API loadMicroApp
,通过这个 API 可以加载一个微应用。我们可以简单的基于该 API 封装一下,封装出一个 MicroApp 的 React 组件。当我拿到 MicroApp 组件之后,我们就可以很简单的使用它。
比如说我们可以和 react-router 这样的路由库一起使用,我们可以选择在某一个路由上挂载组件,这个场景下就跟单实例的场景非常像,跟 qiankun@1.x 一样,是一个路由级别的使用场景。另外一种用法是我们是可以把它当做一个组件使用,我们可以同时在一个页面里挂载应用 A 挂载应用 B,我们可以在一个场景里挂载多个应用,这是一种组件级的使用。
那从单实例的场景转到了多实例的场景,有什么用武之地?
微应用:混合研发
有一个场景是混合研发。
现在随着前端的发展,其实很多人都在思考通过低代码提效的问题,我们已经有一些比较成熟的低代码平台了,比如说像云凤蝶,你可以靠拖拖拽拽,做出一个好用的中后台页面,也有一些配置式的平台,就是你依靠配置一些 json、配置一些数据,你就可以生成前端页面
但是与此同时,我们也存在着低代码平台无法支持的一些,只能用 ProCode 来支撑的一些场景。很可能你一个应用中同时存在着两种场景,那这个时候你就可以把他们视为是不同的两个微应用。LowCode的微应用和 ProCode 的微应用。你可以把两者同时放在一个项目里面,通过 qiankun 去加载两者来做到一种混合研发。这样你可以同时享受 NoCode 或者 LowCode 的快速开发,又可以享受 ProCode 的完整能力,一旦你发现低代码不符合你的需求的时候,你可以无缝地切换到 ProCode,留有后路。
欢迎加入
最后 qiankun 是非常欢迎大家参与共建,大家遇到什么问题可以来 Github 提 issue。大家可以加入我们 qiankun 的交流群,跟我们分享你的看法。未来的 qiankun3.0,也期待大家的加入和共建。
书籍推荐
最后一个环节是向大家推荐一本书,我今天推荐的这本书是《松本行弘的程序世界》。通过这本书大家可以深入地理解程序设计背后的故事。
QA
我们的评论区非常火爆,提了非常多的问题。因为时间的关系,我们只有 4 个机会,大家可以关心一下自己的问题是不是被回答到。
请教方涣:qiankun 框架是否不关心不参与处理微应用之间可能存在的共同依赖,怎么解决公共依赖的问题?
qiankun 本身是没有帮助处理公共依赖的。这个事情上,可以通过一些构建时候的工具来解决,比如说像简单的 externals 和一些库,或者说你可以尝试 Webpack5 的联邦模块,或者说是用一些 import map 之类的特性来做这件事情。qiankun 这方面也在探索路上,包括我们也再想自己如何和 Webpack5 的联邦模块结合,我们后续可能会出一些有关这方面的实践。如果你有好的实践欢迎提出来跟大家分享。但是同时我们有另一种观点,你真的需要把这些依赖提出来吗?很多时候你的应用之间的依赖库的版本不同,这些不同的你可以认为依赖库其实也是应用的一部分,你把它提出去之后,其实也是造成了你应用之间的一些耦合。
请教方涣:是否有对于子应用服务异常时,主应用的容灾机制的推荐方法?
这个我觉得是取决于具体场景和业务诉求的。因为子应用异常:一种可能是你代码就写错了,子应用挂了。这种情况下你主应用可能只能感知到子应用出一些异常故障,你知道这个子应用挂了,你可以做一些上报,或者尝试做一些恢复。还有可能你是想做一些监控实践,你想捕获子应用里面运行时的一些错误情况,它是否发生了一些不到崩溃程度的一种报错,你希望把它记录下来。这方面其实 qiankun 本身是不关注这个的,你完全可以自己来实现这些逻辑。
请教方涣:目前 qiankun 是如何在原有的老项目中引入的?比如说主应用的路由是如何对接的?现在的样式应该是如何隔离?
你的场景应该是已经有一个应用 A,然后你想通过 qiankun 把应用 B 作为一个子应用引入到你的应用 A 中。在这个场景下,其实比较简单的方式是你在应用 A 里面,就是你的主应用里面,单独分配一个路由给应用 B。因为 qiankun 实际上是主应用加载了之后,去检查匹配子应用是否激活。如果子应用激活,我们就会把子应用挂载,然后再去把路由交给自己的子应用。所以这个场景下最简单的方式就是分配一个路由给你的子应用。
样式隔离的问题,其实刚才讲了。可以有两种方式:一种是使用一些工程化的手段,不管是约定前缀,或者说使用 css module 做一些改造都可以避免主子应用之间的样式冲突。你也可以尝试去开启严格的样式隔离,使用Shadow DOM 的方式来让子应用样式真正地做隔离,但是这个时候你的子应用可能会遇到一些新的问题需要你解决。
请教方涣:qiankun 的子应用是 jQuery 的老项目,老项目中图片的地址是相对路径,加载图片时地址的域名和端口却变成了主应用的地址,应该怎么解决?
这个问题可以到 Github 上提个 issues 帮你看一看。