腾讯蓝鲸基座实现原理(基于amd,iframe的微前端方案)
theme: vue-pro
前言
过去一段时间中,我曾经研究过一段时间的微前端方案,但我更像是直接站在巨人的肩膀上去看世界,上来就是从无界乾坤这些比较火热的方案入手,因此我也不禁好奇以前实现微前端都是怎么做的呢?
正好我接触过腾讯的蓝鲸项目,其使用的便是以前的微前端实现方案,以此为入口去研究下 以前是如何实现微前端及和现在的方案有什么差别
。
各位大佬阅读此文的时候最好拉一下腾讯蓝鲸的代码,结合代码阅读,不然会比较生涩难懂。
知识储备
蓝鲸CI的代码开源链接:https://github.com/TencentBlueKing/bk-ci
蓝鲸基座共支持两种接入方式: iframe
及 amd
iframe
:需要提供子项目对应的链接
amd
:需要将子项目打包成 amd格式的js文件并提供相关的css文件
下面的内容将基于如何实现 iframe
及 amd
两种接入方式展开:
基座原理
众所周知我们在请求一个页面的时候,往往我们会先请求下来对应的页面模板。
同样在请求微前端项目的时候也会去获取页面模板,但和一般页面不同,微前端项目会先把基座的模板请求下来。
基座模板做了什么?
(1)
模版代码:https://github.com/TencentBlueKing/bk-ci/blob/master/src/frontend/devops-nav/src/index.html
结合代码,我们可以看到蓝鲸模板的代码中包含了很多js,比如说定义js,css加载函数及cookie相关操作的代码
因此 在基座模板代码中会先定义一下基础的函数。
(2)
因为基座本身也是一个系统,他并不知道会嵌入什么子系统,因此接着就会在基座的基础上去调用接口。
下面就是基座相关接口调用的地方。
会去调用getdata函数获取当前基座所有注册的项目信息。
(3)
完成基座注册系统的数据获取之后,就会基于内容执行init函数,这一步的作用是对注册系统的数据进行分类。
可以看到这里先调用了一个getServiceObject函数,下面是代码实现:
实际上就是对我们基座注册系统数据 根据amd 及 iframe两种类型 进行了分类,补全amd类型所需的js,css链接,同时也生成iframe路由集合。
紧接着就是根据地址和分类后的对象,去设置当前项目信息
假如当前项目是amd模式的话就会直接加载这段js和css 那么第一个子项目就被完成挂载了。
总结
到这里基座的模板需要完成的工作就基本结束了,总结下做了什么:
- 定义一些基础函数包括js加载css加载等
- 请求当前模板涉及的系统信息
- 调用init函数基于请求结果做过滤,得到所有服务基础信息对象,比如有什么服务,服务的类型及服务的资源链接等,还会在这一步生成iframe路由集合
在基座模板中实际上是做了一些初始化的工作。
基座如何加载子应用?
上一部分解释了基座在页面模版中做了什么准备工作,那基座是如何完成子应用的加载呢?
这里我们来介绍下基座如何加载子应用。
基座也是一个系统,蓝鲸为例其基座是一个vue2系统,也有自己的路由等信息, 所以在基座进行路由跳转等操作的时候一样是跳转到对应的路由,不同于一般项目的是 其中子路由可能指向的是 嵌入的子系统
(1)
这里来看看基座的路由文件
路由: https://github.com/TencentBlueKing/bk-ci/blob/master/src/frontend/devops-nav/src/router/index.ts
我们来研究下代码,也非常容易明白
蓝鲸基座这里和一般的vue项目一样,也有自己的路由文件,也导入了入口页等信息,值得注意的是:
这里基于window.serviceObject.iframeRoutes 和 iframe文件去批量生产了 iframeRoutes 最后并入了 routes 对象中(即第一第二个红框内的代码)
(2)
这一步实际上就已经完成了iframe类型的子系统的注册,后面如果进行路由跳转的时候就可以跳转到 iframe类型的子系统 中,会指向IFrame.vue 文件
同时 生成的 iframeRoutes 里面实际上是包含了各个注册的ifrema子系统的相关信息,包括请求链接等,当进入IFrame.vue 就可以基于当前路由挂载的内容得到iframe的具体链接,这样实际上就完成了iframe类型子系统的挂载
不妨看看iframe.vue 的代码长什么样:
iframe.vue: https://github.com/TencentBlueKing/bk-ci/blob/master/src/frontend/devops-nav/src/views/IFrame.vue
其中核心代码是里面的init函数:
这里会基于路由挂载的内容生成 src,这个src就是iframe 对应的链接。
因此可以看到蓝鲸基座如何接入iframe类型的子系统呢?实际上就是把生成的iframesRoutes注入到基座的路由中, 当基座路由跳转到iframe类型的子路由时就目标文件指向IFrame.vue文件
,因为当前子系统的相关信息都注册到了路由上, IFrame.vue文件就可以基于自身生命周期将目标系统的路径生成,并挂到IFrame.vue文件的iframe标签上,
这样就完成了iframe类型子系统的挂载
但是显然这样子是很简陋的,路由同步,基座通信都没有,这些后面会说。
(3)
上面介绍了基座如何挂载基础的iframe子项目,那基座是怎样挂载amd类型的子项目呢?
在介绍这个之前我们需要先简单了解什么是amd:
AMD(Asynchronous Module Definition)模式是一种用于在JavaScript中定义模块的规范。它允许开发者异步加载模块,并在模块加载完成后执行相应的回调函数。
在AMD模式中,每个模块都被定义为一个独立的文件,使用define函数来定义模块。define函数接受两个参数:模块的依赖数组和一个回调函数。依赖数组指定了当前模块所依赖的其他模块,而回调函数则定义了模块的功能和行为。
AMD模式的一个主要特点是支持异步加载模块。这意味着在应用程序运行时,可以根据需要动态地加载模块,而不是一次性加载所有模块。这种方式可以提高应用程序的性能和加载速度。
一个常见的AMD模块示例:
define(['dependency1', 'dependency2'], function(dep1, dep2) {
// 模块的功能代码
// ...
return moduleExports; // 返回模块的输出
});
在上面的示例中,模块依赖于 `dependency1` 和 `dependency2` 两个模块。当这两个模块加载完成后,回调函数将被执行,并传入相应的依赖模块作为参数。模块的功能代码可以在回调函数中定义,并通过 `return` 语句返回模块的输出。
总而言之,AMD模式是一种用于定义和加载JavaScript模块的规范,它支持异步加载和依赖管理,可以提高应用程序的性能和可维护性。
对于这种类型,会要求将子项目打包成amd格式的js包,当然umd也行,只是不同的模块化规范罢了。
所以当我们在浏览器中加载了对应格式的amd格式的js包,一般来说就已经加载了js包对应的子系统。
因此 蓝鲸基座中对于amd格式的加载实际上就是将对应的js和css通过标签的方式挂载到页面,这样就可以将对应的子系统加载下来
那基座如何实现amd的挂载呢?不妨看看基座的路由生命周期:
显而易见,通过即将跳转的路由名称,去serviceMap中进行映射,找到对应的子系统信息,通过解析的内容找到子系统对应的js和css文件,并完成加载。
这样子amd格式的子系统就已经完成加载了。
这里有两个小坑:
(1)一次加载可以加载一个子系统,那我如果后面希望跳转到其他页面再跳转回来,是否需要重新加载js和css呢?
(2)怎么把子系统挂到页面上?
显而易见并不是的,因为主包已经加载下来了,后续反复跳转如何快速定位到注册的子系统呢?最好的方式就是把子系统直接注册到基座上面即可和iframeRoutes一样
这就是为什么会有这段代码的存在?
这里就是为了将加载下来的子系统都挂载到基座路由中,后续子系统的路由切换都将在基座路由中进行,这样的好处是还同步将子系统和基座的路由做了同步。
这里会有同学问为什么,window.pages会有子系统相关的路由信息?
实际上这就是子系统接入规范要求了,如果希望子系统以amd的形式注册到基座中,那么 子系统的入口文件中必须执行下面代码,手动把子系统的路由等信息挂载到window上。
(注:实际上这样会有一个隐患,子系统的名称是映射的key,而且这个key同时维护在基座和子系统中了,假如一方不小心改动就会导致难以排查的异常)
总结:
对于amd模式的子系统接入,相对来说比较简单,基座跳转的时候会拿到目标地址,基于目标地址去serviceObject中找对应的项目,假如找到了,那就解析出里面的js和css,并动态挂载到页面上。
并且在接入过程中会约束amd类型子项目要将自己的routes和store等信息注册到window上,这样js加载完之后基座就可以拿到子项目到路由等内容并将其注册到自身的路由中,后续的路由跳转就相当于在基座自身的路由中进行跳转,只是里面有一些vue文件是通过js动态加载下来的。
缺陷: 这里amd的实现思路相当于把子项目直接加载到基座中进行运行,变成基座的子路由的思想。
- 没有做css隔离,也没有做js隔离,意味着子应用完全可以把service等重要信息直接修改或删除,有严重的漏洞存在。
- 路由合并的过程中是统一合并到一个数组下的,假如出现相同名称的路由,难以保证是否会出现异常。
- 这类名称做路由key的情况,不应该同时维护在基座和子项目中万一哪头突然改了就会导致难以发觉的异常。
- 代码中没看到自定义依赖的注入,需要提供自定义依赖注入机制。
- 技术栈锁死了,因为amd相当于是把子系统路由变成基座的子路由,因此技术栈必须和基座一致。
总结
这里介绍了基座是如何完成iframe和amd类型的子系统的加载。
核心是通过基座的路由完成的, 对于iframe类型的子系统会将注册的iframeRoute合并到基座的路由中,后续如果要进行跳转到iframe类型的子系统的话,会并进入IFrame.vue,通过路由自带的数据和IFrame.vue的生命周期函数去生成iframe的链接,并将这个链接挂载到IFrame.vue的iframe标签上。
对于amd类型的子系统, 会通过基座路由钩子获取当前目标子系统的key,基于key去到serviceObject中映射对应的子系统,并解析出其中的js和css,动态挂载到基座的页面上。
通过接入规范,约束子系统将子系统的routes和stores等信息,通过key挂载到window上,当js和css动态挂载结束之后,就将子系统的routes合并到基座的路由中,这样子系统就转化成基座的子页面了,后续的跳转就相当于基座自身的路由跳转。
如何实现iframe类型的子系统路由同步及与基座间的交互?
上面的内容介绍了蓝鲸的基座做了什么准备工作以及如何实现amd和iframe两类基座的接入。
对于amd类型来说比较简单直接转化成基座的子路由,但是相对来说隔离性就比较差。
那iframe呢?iframe拥有极好的隔离性,那iframe模式会有什么问题吗?
对于iframe而言,如何实现路由的同步是一个大问题,因为iframe相当于中父页面中嵌入了一个小页面,二者的路由是不同的。
因此在蓝鲸基座这里设置了相关的交互机制实现二者的路由一致。
(1)
基座提供了iframeUtils 方法里面注册了message的监听函数,并创建了相关的交互事件,当触发message之后就会去匹配是否触发了对应key的函数。
iframeUtils: https://github.com/TencentBlueKing/bk-ci/blob/master/src/frontend/devops-nav/src/utils/iframeUtil.ts
下面我们来看看 iframeUtils 中一些重要的实现:
- 接收路由对象,导出iframeUtils的创建函数
- 注册iframe监听
- 声明路由更新函数
因此通过这些函数基座实现了监听iframe的postMessage能力,所以当iframe子系统可以通过触发postMessage去通知基座更新路由。
(2)
上面介绍了基座提供了iframe子系统通知基座触发指定行为的机制,假如基座改变了路由,又该如何通知到子应用更新路由呢?
很简单, 基座直接监听路由即可,路由一旦发生改变,就执行init函数,重新生成src赋值给iframe标签即可,但是有一个问题就是基座无法触发子系统的路由跳转,哪怕跳转的路由在子系统中存在,这就导致如果基座希望更新子系统路由就必须重新走一遍iframe的加载流程,对于系统的加载速度,网络资源的损耗都是不利的。
(3)
(1)中我们介绍了基座实现了监听iframe的postMessage能力,通过iframe的postMessage可以让基座去同步路由,那我们iframe类型的子系统如何接入这个能力呢?
同样的基座也提供了相关的js文件,当我们加载这个文件之后就会自动为我们的工程补充上和基座沟通的渠道。
devopsUtil:https://github.com/TencentBlueKing/bk-ci/blob/master/src/frontend/devops-nav/src/assets/static/devops-utils.js
devopsUtil文件会加载一个立即执行函数,执行之后会对基座支持的所️交互行为进行注册挂载,这样子系统就可以通过挂载到vue上的函数进行和基座间的交互了。
稍微看下 devopsUtil 的代码:
当devopsUtil的代码执行结束之后,vue的原型上就会得到上述注册的事件,当iframe子系统主动去触发这些事件的时候,就会通过postMessage通知到基座,这样就可以完成基座的路由同步和基座子应用的通信了。
vue的原型上就有注册的事件:
到这里iframe类型的子系统挂载就完成了。
总结
这里介绍了iframe类型的子系统和基座如何实现路由同步及基座子应用的通信。
第一步基座会提供一个 iframeUtils 的文件, 里面注册了message的监听函数,并创建了相关的交互事件,当触发message之后就会去匹配是否触发了对应key的函数,基座就得到了和接收子应用触发事件的能力。
第二步为了 让基座去同步子应用的路由,当基座的链接发生改变就直接执行IFrame.vue文件的init方法,重新设置iframe标签的src。
第三步为了提供子系统触发基座事件的能力, 基座会提供一个devopsUtil的js文件,子系统执行之后会将所有的触发事件都挂载到子系统的vue的原型上,里面封装了各类触发事件和postmessage方法,子系统就得到了触发事件的能力。
因此 如果要同步路由的话 就会变得很简单, 只需要监听子系统的路由,每当路由发生变更的时候执行挂载到原型的$syncUrl函数通知基座更新路由即可。
总结
到这里蓝鲸基座实现微前端的原理解析就完成了。
大概可以分成下面几步:
- 请求接口获取所有注册的子系统
- 过滤请求数据生成serviceObject,将所有的子系统信息都挂载到基座的window上
- 将iframeRoutes挂载到基座的路由上,当跳转到iframe类型的子系统时指向IFrame.vue文件,在IFrame.vue的生命周期中去读取当前路由的系统信息生成iframe的src链接,并赋值到IFrame.vue标签中。
- 当跳转到amd类型的子系统时,通过基座路由的钩子拿到跳转目标系统的key,通过key和serviceObject映射到目标子系统的信息,执行子系统的js和css,这样amd类型的子系统就会被加载到基座上, 为了使用子系统,因此需要约束amd类型的子系统必须要将路由和stores等信息挂载到window上,接着基座路由钩子就会把路由直接注入到基座的路由中,这样就相当于把子系统合并到基座上,后续的跳转就相当于基座自身的路由跳转。
- 为了实现iframe类型子系统和基座的通信,基座会提供一个 devopsUtil文件,这个是一个立即执行函数里面封装了postmessage事件和基座支持的触发事件,执行之后这些函数都会被挂载到子系统的原型上,子系统就可以通过调用原型方法触发基座支持的事件,包括同步基座路由也可以通过监听子系统的路由,每当路由发生变更的时候执行挂载到原型的$syncUrl函数通知基座更新路由来实现。
后面会继续补充这种实现方式的优缺点及和目前流行的微前端方案相比有什么可以优化的点,未完待续。