加入收藏 | 设为首页 | 会员中心 | 我要投稿 拼字网 - 核心网 (https://www.hexinwang.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 移动互联 > 应用 > 正文

前端搞微前端 | 方涣 - 如何设计实现微前端框架 - qiankun

发布时间:2022-11-26 14:32:35 所属栏目:应用 来源:转载
导读: 前端早早聊大会,前端成长的新起点,与掘金联合举办。 加微信 codingdreamer 进大会专属内推群,赢在新的起跑线。
第十四届|前端成长晋升专场,8-29 即将直播,10 位讲师(蚂蚁金服/税友等)

前端早早聊大会,前端成长的新起点,与掘金联合举办。 加微信 codingdreamer 进大会专属内推群,赢在新的起跑线。

第十四届|前端成长晋升专场,8-29 即将直播,10 位讲师(蚂蚁金服/税友等),点我上车 (报名地址):

应用接入_应用接入_应用宝sdk接入

正文如下

本文是第七届微前端,前端早早聊第 42 场,来自蚂蚁金服体验技术部 qiankun 的核心贡献者 - 方涣的分享 - 讲稿简要整理版(完整版含演示请看录播视频和 PPT):

应用接入_应用接入_应用宝sdk接入

一、自我介绍

大家好,今天由我带来前端早早聊本次微前端专题的最后一场,《如何设计实现微前端框架 - qiankun》。首先呢,我做一个简单的自我介绍。

自我介绍

我叫方涣,在一些平台上,比如知乎和语雀上,用的就是左边这个头像。

我现在是蚂蚁金服体验技术部的一名前端工程师,和有知一起维护和迭代 qiankun。现在也正在做蚂蚁内部新一代的前端运行时平台,正是基于微前端技术的。

应用接入_应用接入_应用宝sdk接入

qiankun 简介

首先开始之前,我先给大家简单的介绍一下乾坤,大家可以在 的官网上,看到 qiankun 的介绍和 qiankun 的文档,诚如这张官网截图所示,我们对 qiankun 的一句话定义就是:“可能是你见过最完善的微前端解决方案”,它有以下 3 个特点:

? 简单。无论你是什么技术栈,你用的是什么 js 框架,你都可以使用 qiankun 来把你的子应用接入到你的框架应用中,接入非常简单,就像接入一个 iframe 系统一样。

? 完备。我们基于 single-spa 封装了非常多的能力,把你在构建微前端系统中遇到的一些问题,如样式隔离、沙箱、预加载等等这些你需要的能力全部内置到了 qiankun 里面,所以它是一个比较完备的微前端解决方案。

? 生产可用。到目前为止,已经大概有 200+ 的应用,使用 qiankun 来接入自己的微前端体系。我们在蚂蚁内外受过了大量线上系统的考验,所以它是一个值得信赖的生产可用的解决方案。

应用宝sdk接入_应用接入_应用接入

qiankun 名字由来

很多人可能会好奇 qiankun 这个名字是怎么来的。实际上源自于这句话:小世界有大乾坤。我们希望在微前端这个小世界里面,通过 qiankun 这个框架鼓捣一个大乾坤。

应用宝sdk接入_应用接入_应用接入

二、QIANKUN 的诞生与设计理念

在第一小节,我会跟大家聊一聊 qiankun 的诞生,关于 qiankun 是怎么来的;然后聊一聊 qiankun 的设计理念,就是 qiankun 怎么看待微前端的,qiankun 认为微前端最重要的是什么?

应用宝sdk接入_应用接入_应用接入

缘起:一次统一上云战役

qiankun 的源起,实际上是一次统一上云战役,不过这次战役具体是怎么样的并不重要。大家可以看到在这张图里看到我们这次战役所制作的控制台,我们通过红框把它划分成了好几个部分,大家可以看到最顶上的导航头、左边的菜单、右边的帮助引导以及中间的接入产品,这也就是我们子应用的主体。

应用接入_应用接入_应用宝sdk接入

控制台结构

把控制台抽象一下,它的结构就是这个样子的。

其中顶部的导航头、身份认证和左边的侧边栏、新手引导在内的所有部分加起来就是我们的主应用,或者说是框架应用。而中间那个框也就是应用主体,应用 body 部分,它就是我们接入的一系列子应用,它可能是应用 a、应用 b 、应用 c 等等一系列的应用。

我们把这一系列的应用一起集成到了一个大的控制台应用里面,这就是我们这次所做的事情。而这件事情正好是微前端技术的一个典型应用场景,所以 qiankun 就从这个项目开始了它的第一步。

应用接入_应用接入_应用宝sdk接入

我们需要一个“友好”的微前端方案

对这个项目而言,我们所需要的微前端方案,可以概括为一个 “友好” 的微前端解决方案,那怎么样算友好的微前端方案呢?我们提炼出两个要求:

应用宝sdk接入_应用接入_应用接入

技术栈无关

这也正是 qiankun 所认为的微前端的核心价值。后面 qiankun 面临的很多技术选择和最终的实现方式,都是出于技术栈无关的出发点的。当我们思考:“在很多的解决方案中,哪个方案被 qiankun 最后所选用”的时候,我们会想哪个方案是最技术栈无关的,最技术栈无关的方案,往往就是 qiankun 最后所选择的方案。

应用接入_应用宝sdk接入_应用接入

“技术栈无关”为什么重要

这里举一个例子说明,假如我们要做一个统一控制台,我们不妨就把控制台叫做 ABC,然后控制台需要接入三个不同的项目:项目 A 是一个月前新建的,还处在积极迭代中。项目 B 可能是一年前建的,现在已经缺乏维护。当然最难的可能是项目 C,它可能是一个的三年前项目,现在已经无人维护了。

我列出这几个项目的技术栈,大家会发现它们之间有很大的不同,可能项目 A 现在用着最新的 React 版本,用着最新的技术栈,甚至超前的用了 Webpack5 作为打包工具;与此同时,项目 B 可能就是大家一年前的主流技术选型,而项目 C 已经是非常旧的技术栈了。在这种情况下,如果我们微前端方案是技术栈相关的,那就势必得把项目 B 做升级,得把项目 C 做改造,而这样的成本很多时候是我们在做 ABC 这个项目的时候无法接受的。再者随着时间的正常推移,项目 B 也会变成无人维护的状态,项目 A 技术栈也终归会陈旧。

所以只有做到技术栈无关,才能保证在过了很久之后,我们的统一控制台 ABC,还是能够安稳的继续维护下去,还是能够安稳的接入新的项目。

应用接入_应用接入_应用宝sdk接入

玉伯有一段话是这么点评微前端的:微前端的前提还是得有主体应用,然后才有微组件或微应用,解决的是可控体系下前端协同开发问题。这里开发问题有两部分:一部分是空间分离带来的协作,另一部分是时间延续带来的升级维护。空间分离带来的协作,需要微前端方案能够提供独立开发、独立部署的特性,来处理协作问题。而时间带来的延续其实是需要我们做到技术栈无关,做到随着时间的推移,当我们技术栈陈旧的时候,还是能够正常的接入我们的框架应用。

应用接入_应用接入_应用宝sdk接入

“技术栈无关”的价值

可能大家都比较熟悉阿里使命:让天下没有难做的生意。而技术栈无关的价值,也正是 qiankun 口号:让天下没有短命的控制台。

应用接入_应用接入_应用宝sdk接入

三、QIANKUN 的技术实现与选择

简单的讲了一下 qiankun 所坚持的理念之后,我们接着来聊一聊 qiankun 具体的技术实现和最终选择使用什么方案来实现微前端。

应用接入_应用宝sdk接入_应用接入

微前端框架面临的两大共性问题

实际上所有的微前端框架都面临这两大共性问题。当你解决了这两大问题之后,你的微前端框架的运行时,就已经基本可用了。

应用接入_应用接入_应用宝sdk接入

应用路由与 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 之类的路由库,你也可以选用它的动态路由方案,这些都是可以的。

应用接入_应用接入_应用宝sdk接入

社区已有的成熟方案

在 qiankun 这里,我们就直接选用了社区成熟的方案 Single-SPA。 Single-SPA 已经劫持的路由,帮我们做好了加载这件事情,也帮我们做好了路由切换这件事情,在这个方面我们就没有自己造轮子了。

应用接入_应用接入_应用宝sdk接入

应用接入:协议接入

同时由于我们使用了 Single-SPA,它顺便帮我们解决另一个问题:就是应用接入的问题。

什么样的一个应用能够成为子应用,能够接入到我们的框架应用里?由于我们需要技术栈无关,所以我们希望接入是一个 协议接入。只要你的应用实现了 bootstrap 、mount 和 unmount 三个生命周期钩子,有这三个函数导出,我们的框架应用就可以知道如何加载这个子应用

这三个钩子也正好是子应用的生命周期钩子。当子应用第一次挂载的时候,我们会执行 bootstrap 做一些初始化,然后执行 mount 将它挂载。如果你是一个 React 技术栈的子应用,你可能就在 mount 里面写 ReactDOM.render ,把你的 ReactNode 挂载到真实的节点上,把应用渲染出来。当你应用切换走的时候,我们会执行 unmount 把应用卸载掉,当它再次回来的时候(典型场景:你从应用 A 跳到应用 B,过了一会儿又跳回了应用 A),这个时候我们是不需要重新执行一次所有的生命周期钩子的,我们不需要从 bootstrap 开始,我们会直接从 mount 阶段继续,这就也做到了应用的缓存。

应用接入_应用宝sdk接入_应用接入

App Entry 抉择

在这个方面,qiankun 还面临着另一些选择,其中一个就是 App Entry 选择,也就是如何设计子应用的加载入口,在这个问题上,我们有两个地方需要面临抉择:一个是什么时候去组合,另一个是加载子应用的入口是什么。

应用接入_应用接入_应用宝sdk接入

组合时机选择

在组合时机的选择上,我们有两个选择:第一个是在构建时,把主子应用打包在一起,着实际上就是一种多包的方案。这个方案它的好处是构建的时候可以做公共依赖的提取,但是它的缺点在于我们把主子应用构建方案和工具都耦合在一起了,这非常不灵活地,这样也没办法做到动态加载。所以在绝大部分情况下,我们都会选择运行时去组合,就是运行的时候才去动态的加载子应用,把它加载、渲染到框架应用里。

应用接入_应用宝sdk接入_应用接入

应用入口选择:HTML Entry

那我们子应用的入口又如何选择?qiankun 在这里选择的是 HTML,就是以 HTML 作为入口。

大家看了刚才的分享,其实已经清楚了:加载子应用用的时候,我们实际上是需要提供一份资源列表,在这份资源列表里,我们可能列出了这个子应用使用了哪些 JS,有哪些 CSS。

但是 qiankun 的第一选择其实是 HTML 入口,就是提供一份 HTML 文件。因为这份 HTML 中其实包含了子应用的所有信息。它包含网页的结构,包含了一些元信息。大家可以看到在这份 HTML 里我们有 CSS、JS 链接、有应用要挂载的根路由 root 的 DOM。这些信息是非常全面的,比单纯你拿 JS 和 CSS 组成一份资源列表作为入口,要清晰和完整得多。同时 HTML Entry 这样的设计,也使得我们在接入一些老旧应用的时候,更加简单。

应用接入_应用宝sdk接入_应用接入

应用入口选择

这里做一个简单的对比,用了 HTML Entry 可以把子应用和主应用更加的解耦了。但是如果你是选择资源列表或者 JS 作为子应用的入口,那么子应用挂载的时候,DOM 节点往往需要和主应用作一个约定,这就产生了一定程度的耦合。

应用宝sdk接入_应用接入_应用接入

应用隔离:JS沙箱

实际上刚才那些抉择完了之后,尤其是当我们借用了 Single-SPA 能力之后,我们已经基本解决了应用的加载与切换。我们接下来要解决的另一块事情是应用的隔离和通信。

首当其冲就是应用的隔离问题,隔离问题最开始我们就是要做 JS 的隔离。在这个问题上,有的时候你不隔离其实也可以运行的,只不过不隔离,直接裸用 Single-SPA 的话,一旦应用之间的发生了冲突,你的应用很可能就跑不下去了,所以 JS 沙箱大部分情况是必要的。

再 qiankun 沙箱的实现里,我们会有两个环境:一个代表的是外部的部分,就是我们的全局环境 Global Env,指的是你框架应用所运行的全局环境。而子应用加载的时候,实际上是应该跑在一个内部的沙箱环境里面,就是这张图上所表示的 Render Env。

应用接入_应用宝sdk接入_应用接入

沙箱实现思路有两条:其中最经典的实践思路其实是快照沙箱。

快照沙箱就是在应用沙箱挂载和卸载的时候记录快照,在应用切换的时候依据快照恢复环境,

当我子应用加载、启动了之后,此时的环境其实就是 Render Environment,是内部沙箱环境里,这个时候我记录一下当时的快照状态。而在我最后子应用 unmount 的时候,我把当前的环境和记录的快照进行一个对比,把它恢复回原来的全局状态。这就是说当我应用挂载了又卸载了,这个过程走了一遍之后,我当前整个 Windows 运行环境恢复成原来的样子,应用内部所做的修改,在它卸载的时候就会被恢复,这是快照沙箱思路。

举个例子,假如我们在 A 应用运行时,追加了一个全局变量,我们说 window.a = 123 ,这个时候 window.a 就变成了 123 ,但是在应用 A 卸载之后,快照还原, window.a 会被重新删除,你在全局环境中并不会继续看到 a 变量

应用接入_应用宝sdk接入_应用接入

快照怎么打?其实也有两种思路:

一种是直接用 **windows diff。**把当前的环境和原来的环境做一个比较,我们跑两个循环,把两个环境作一次比较,然后去全量地恢复回原来的环境。

另一种思路其实是借助 **ES6 的 proxy 就是代理。**通过劫持 window ,我们可以劫持到子应用对全局环境的一些修改。当子应用往 window 上挂东西、修改东西和删除东西的时候,我们可以把这个操作记录下来。当我们恢复回外面的全局环境的时候,我们只需要反向执行之前的操作即可。比如我们在沙箱内部设了一个新的变量 window.a = 123 。那在离开的时候,我们只需要把 a 变量删了即可。

快照沙箱这个思路也正是 qiankun1.0 所使用的思路,它相对完善,但是缺点在于无法支持多个实例,也就是说我两个沙箱不可能同时激活,我不能同时挂载两个应用,不然的话这两个沙箱之间会打架,导致错误。

所以说我们把目光又投向了另一条思路,我们**让子应用里面的环境和外面的环境完全隔离。**就如图所示,我们的 A 应用就活在 A 应用的沙箱里面,B 应用就活在 B 应用的沙箱里面,两者之间要不发生干扰,这个沙箱的实现思路其实也是通过 ES6 的 proxy,也通过代理特性实现的。

应用接入_应用宝sdk接入_应用接入

应用隔离:样式隔离

无论是刚才的快照沙箱还是另外的代理沙箱,其实都可以解决 JS 之间的副作用冲突。解决完 JS 之后,我们接下来解决的是 CSS 之间的冲突。

在微前端框架里所面临的样式冲突器就两种:一种是主子应用样式冲突,你的主应用和你的子应用两者之间样式会发生冲突,另一种是子应用之间样式冲突,当你挂载了应用 A 又挂载了应用 B 的时候,这两个应用是平级的,它们之间样式也会发生冲突。

应用宝sdk接入_应用接入_应用接入

样式隔离:Dynamic Stylesheet

qiankun 所做的第一件事情其实是动态样式表。当你从子应用 A 切换到子应用 B 的时候,这个时候需要把子应用的样式表 A 的样式给删除,把子应用 B 的样式表给挂载。这样就避免了子应用 A 的样式和子应用 B 的样式同时存在于这个项目中,就做到了最基本的隔离。当你从应用 A 切到应用 B 的时候,你的样式也会自然的从应用 A 的样式切换到应用 B 的样式,我们会自动的帮你做应用样式的删除和加载。当然这样做只仅仅能够保证你在单应用模式下(就是同时只能有一个应用活跃的情况下)保证子应用和子应用之间的样式不会冲突。

当然动态样式表实现之后,我们还是没有解决主应用和主应用之间的样式冲突。实际上这个问题最好的手段还是通过一些工程化的手段来解,比如说 BEM,就是说大家约定一下,你应用 A 样式就统一加一个 a- 的前缀,应用 B 样式就统一加一个 b- 的前缀,你的框架应用也统一加个前缀。大家通过一些约定(比如约定大家都加上一个应用名的前缀)来避免冲突,这是非常有效的一个方案。尤其是主子应用之间的冲突,大部分情况下你只要保证主应用的样式做好改造应用接入,保证主应用的样式是很具象的规则,不会跟子应用冲突,那主子应用之间的冲突其实也解决了。不过这种方案终归是依赖约定,往往容易出纰漏。

应用接入_应用接入_应用宝sdk接入

样式隔离:工程化手段

当前 css module 其实非常成熟一种做法,就是通过编译生成不冲突的选择器名。你只要把你想要避免冲突的应用,通过 css module (在构建工具里加一些 css 预处理器即可实现)就能很简单的做到。 css module 构建打包之后,应用之间的选择器名就不同了,也就不会相互冲突了。

css-in-js 也是一种流行的方案,通过这种方式编码的样式也不会冲突,这几个方案实现起来都不复杂,而且都非常行之有效。所以绝大部分情况下,大家手动用工程化手段处理一下主子应用之间的样式冲突,就可以解决掉样式隔离的问题。

应用接入_应用宝sdk接入_应用接入

样式隔离方案:Shadow DOM

当然来到 qiankun2.0 之后,我们追加了一个新的选项,叫作严格样式隔离,不知道大家有没有使用过。

其实严格样是隔离代表 Shadow DOM。Shadow DOM 是可以真正的做到 CSS 之间完全隔离的,在 Shadow Boundary 这个阴影边界阻隔之下,主应用的样式和子应用的样式可以完完全全的切分开来。

但是绝大部分情况下,你还是不能无脑的开启严格样式隔离的。原因之前的同学也已经提到过一些了,比如说你在使用一些弹窗组件的时候(弹窗很多情况下都是默认添加到了 document.body )这个时候它就跳过了阴影边界,跑到了主应用里面,样式就丢了。又比方说你子应用使用的是 React 技术栈,而 React 事件代理其实是挂在 document 上的,它也会出一些问题。所以实践里当你开启 Shadow DOM 之后,当你的子应用可能会遇到一些奇怪的错误,这些错误需要你一一的去手动修复,这是比较累的一个过程。

我们提供了 Shadow DOM 这么一种样式隔离方式。但是实际使用中还是工程化的手段最为可靠、最为简单。当然在允许的情况下,大家还是可以去尝试开启严格样式隔离,毕竟这才是真正的隔离。

应用接入_应用宝sdk接入_应用接入

样式隔离 RFC:runtime css transformer

qiankun 还收到过一个提案,让我们可以动态运行时地去改变 css。

如图,比如说原来子应用里面的样式是这个样子,那转换之后,我们可以把它前面加一个限定,我们规定只有在 data-qiankun-app1 这个子应用里面(这个 div 就是包裹子应用的最外层的容器), .main 这条样式才能真正的生效。通过这样一个运行时的转换,我们也能够达到样式隔离的目的。当然这个方案的问题在于这个转换不是那么好做,首先我们增加了一定的运行时开销,其次对于一些媒体查询,对于一些动画很可能会出一些意料之外的情况。这个 RFC 至今至今没有合并,不知道大家对此有什么看法?欢迎来到 PR 下发表你的见解。

应用接入_应用接入_应用宝sdk接入

在我们通过 JS 沙箱解决 JS 之间的副作用;通过不管用工程化手段也好,动态样式也好,或者说开启 Shadow DOM,解决掉 CSS 冲突之后,我们就要考虑应用之间的通信问题了。

应用通信:基于 URL

其实有一种最朴素的通讯方案,就是基于 URL。前端有一种设计叫做 URL 中心设计,就是说你的 URL 完全决定了你页面展示出来是什么样子。

假如我的应用里有一个列表,有一个分页,当你点下一页的时候,是不是就产生了一个在第二页的 query 参数?你可能会把这个参数同步到路由上,这样你把这个链接分享给别人的时候,别人就能看到跟你一样的页面。

我们其实完全可以把这种路由翻译成看作是一个函数调用,比如说这里的路由 b/function-log,query 参数 data 是 aaa ,我们可以把这个路由 URL 理解为我在调用 B 应用的 log 函数,这就像一次函数调用一样。当我们从应用 A 跳去应用 B,对应路由发生变化的时候,就是触发了一次函数调用,触发了一次通信。

所以路由实际上也有通信的功能。这种通信方式是完全解耦的,但是缺点就是比较弱。

应用宝sdk接入_应用接入_应用接入

(编辑:拼字网 - 核心网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!