Skip to content
霞露小伙 — HfWang
On this page

qiankun 原理学习

qiankun 的特点

  • 📦 基于 single-spa 封装,提供了更加开箱即用的 API。
  • 📱 技术栈无关,任意技术栈的应用均可 使用/接入。
  • 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  • 🛡​ 样式隔离,确保微应用之间样式互相不干扰。
  • 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  • ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。

qiankun 和 single-spa 的区别

qiankun 是基于 single-spa 封装的

WARNING

  • single-spa 的思想: single-spa 认为任何一个 web 前端应用都可以被打包成一个 js 模块,这样的话对于一个巨石应用来说,就可以拆分成一个主应用 + 若干个子应用(js 模块),在主应用通过 jsonp 的方式请求对应的 js 模块(子应用), 并将其插入到主应用即可实现巨石应用的拆分

  • qiankun 的思想:qiankun 是基于 single-spa 实现的,底层的 api 用的都是 single-spa 的 api,但是针对 single-spa 的缺点进行了优化。

    1. qiankun 认为打包后的入口文件不应该是 js 模块,而是一个 html 文件
    2. 针对 single-spa 没有 js 隔离和 css 隔离的缺陷,为我们提供了 3 种 js 沙箱和 2 种 css 沙箱用于隔离 js 和 css
    3. 简化了主应用和子应用的接入成本,为我们提供更简便的 api
    4. 为我们提供了子应用预加载和子应用缓存的功能
注册子应用的 api请求的资源支持子应用预加载支持子应用缓存
single-sparegisterApplicationjs不支持不支持
qiankunregisterMicoApphtml支持支持

registerMicoApp 是对 registerApplication 进行了一次封装

qiankun registerMicoApp

首先就是 registerMicoApp 的使用

js
registerMicroApps([
  {
    name: 'app1',
    entry: '//localhost:8080',
    container: '#container',
    activeRule: '/react',
    props: {
      a: 1,
    },
  },
])
ts
let microApps: Array<RegistrableApp<Record<string, unknown>>> = []

export function registerMicroApps<T extends ObjectType>(
  apps: Array<RegistrableApp<T>>,
  lifeCycles?: FrameworkLifeCycles<T>,
) {
  // 获取还没有注册过的 子应用
  // 每个应用只需要注册一次,避免多次调用 registerMicroApps 函数同时注册了相同的应用
  const unregisteredApps = apps.filter(
    (app) => !microApps.some((registeredApp) => registeredApp.name === app.name), //
  )

  microApps = [...microApps, ...unregisteredApps]

  unregisteredApps.forEach((app) => {
    const { name, activeRule, loader = noop, props, ...appConfig } = app

    registerApplication({
      name, // 子应用名称,必须唯一
      app: async () => {
        // 1. 加载和解析`html`资源,获取页面、样式资源链接、脚本资源链接三种数据
        // 2. 对外部样式资源(css)进行加载处理,生成`render`函数
        // 3. 生成 `js`沙箱
        // 4. 加载外部脚本资源(js),且进行包装、执行
        // 5. 从入口文件里获取生命周期钩子函数:`bootstrap`、`mount`、`unmount`,
        // 然后当作registerApplication的app形参中的返回数据,如下所示

        loader(true)
        await frameworkStartedDefer.promise

        const { mount, ...otherMicroAppConfigs } = (
          await loadApp(
            { name, props, ...appConfig },
            frameworkConfiguration,
            lifeCycles, //
          )
        )()

        return {
          mount: [
            async () => loader(true),
            ...toArray(mount), //
            async () => loader(false),
          ],
          ...otherMicroAppConfigs,
        }
      },
      // 子应用激活路径
      activeWhen: activeRule,
      // 需要传递给子应用的数据
      customProps: props,
    })
  })
}

快速学习乾坤实现流程

qiankun 的整个流程

加载解析 html

qiankun 使用了 import-html-entry 这个库去处理 html 资源,并通过正则将 html 解析为下面几个模块

  • template
  • scripts
  • entry
  • styles

css 外部样式处理

比如入口的 html 文件中引入了 main.css, 在 html 解析的时候,对应的 css 文件引入代码在 template 里面会先被注释掉并打上标记

等第一步解析完成之后,再去请求 styles 里面的资源信息,在成功获取后在插入到 template 中标记的地方

成功合并之后生成新的 template,在根据新的 template 生成响应的 render 函数

render 在把我们的子应用挂载到父应用上对应的节点上

js 沙箱处理

js 沙箱 用于隔离应用间的 window 环境,防止子应用在执行代码期间影响了其他应用设置在 window 上的属性

qiankun 为我们提供了 3 种 js 沙箱

名称原理简述
SnapshotSandbox快照沙箱保存快照,diff
LegacySandbox单例沙箱window 变化监听 + diff
ProxySandbox多例沙箱proxy 代理和 window 相同属性方法的纯对象

具体差别可以看下文的 沙箱实现思路 部分

ProxySandbox:

  1. window 上的全部属性复制到一个纯对象里面
  2. 使用 proxy 代理这个纯对象
    • 子应用如果访问的属性是在设置的白名单(比如 window 上原本就有的属性、方法)里面的话,那会先把 window 上原先的值复制一份,子应用可以直接访问/修改 window 上对应的属性,子应用销毁时再把备份的数据重新给 window 上对的对应属性赋值
    • 如果不在白名单里面,访问 / 修改的就是纯对象里的数据,window 上的属性不会被访问 / 改变

ProxySandbox 缺点:这种沙箱只能隔离 Window 的一级属性。因为 Proxy 只会捕获到一级属性的增删改,不能捕获到二级以上属性的变动

加载外部 js 资源

使用上一步的沙箱对请求到的 js 资源进行包装

例如我们要请求一个 http://xxxx:xxx/main.bunlde.xxx.js 文件

js
// main.bunlde.xxx.js
function a() {
  // ...
  b()
}
function b() {
  // ...
}
a()

使用沙箱封装

js
;(function(window, self, globalThis){
	// with 语句的主要作用是将代码的作用域设置到一个特定的对象中,简化多次编写同一个对象的工作
  with(window){
		function a() {
			// ...
			b()
		}
		function b() {
			// ...
		}
		a()
  }
).bind(window.proxy)(
	window.proxy,
	window.proxy,
	window.proxy
);

从入口文件获取生命周期钩子函数

qiankun js 沙箱具体实现思路

qiankun 沙箱发展时间线

qiankun 为我们提供了 3 种 js 沙箱

SnapshotSandbox --> 优化 --> LegacySandbox --> es6 的 proxy 普及,再次优化 --> ProxySandbox

SnapshotSandbox 快照沙箱

  • 将原本的 window 上的属性复制出来一份另外保存
  • 子应用修改 window 上的属性
  • 子应用卸载,diff window 和之前另外保存的那份数据,找出 diff 结果,保存 diff 结果
  • 使用备份数据恢复原先 window 上的属性
  • 子应用再次挂载时,将之前保存的 diff 结果重新赋值到 window 上

缺点:如果给 window 上自定义了上千个属性,那么每次子应用挂载、卸载每次 diff 都要花费较大的性能开销,而且同一时间内只能有存在一个子应用,若页面需要同时出现两个子应用则会出现异常,简单概括会污染全局 window

LegacySandbox 单例沙箱

针对上面每次都需要大量 diff 操作带来的性能消耗,qiankun 提供了另一种沙箱实现方式,就是监听 window 上的修改来记录 diff 内容

  • 子应用修改 window 属性
    • 修改的是原本就有的属性
      • 分别记录新的值和旧的值
    • 新增的属性
      • 记录下来
  • 子应用卸载时
    • 直接从 window 上把前面新增的属于移除
    • 对于原本就有的属性,使用保存的旧值直接替换

缺点:和快照沙箱一样,同样是会 污染全局 window 且只能存在单例子应用

ProxySandbox 多例沙箱

为了解决只能使用单例的问题,qiankun 又提出了另一种沙箱形式——代理沙箱

为每个子应用单独代理一个 window 对象,大致实现可以看前面 [js 沙箱处理] 部分

js 沙箱总结

  • SnapshotSandbox:记录 window 对象,每次 unmount 都要和微应用的环境进行 Diff
  • LegacySandbox:在微应用修改 window.xxx 时直接记录 Diff,将其用于环境恢复
  • ProxySandbox:为每个微应用分配一个 fakeWindow,当微应用操作 window 时,其实是在 fakeWindow 上操作

qiankun css 沙箱具体实现思路

qiankun 的沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。

shadow Dom 沙箱

strictStyleIsolation: true

shadow Dom

  • 封装

封装是面向对象编程的基本特性,它使程序员能够限制对某些对象组件的未授权访问。

在此定义下,对象以公共访问方法的形式提供接口作为与其数据交互的方式。这样对象的内部表示不能直接被对象的外部访问。

  • Shadow DOM

Shadow DOM 将此概念引入 HTML,它允许你将隐藏的,分离的 DOM 链接到元素,这意味着你可以使用 HTML 和 CSS 的本地范围。

现在可以用更通用的 CSS 选择器而不必担心命名冲突,并且样式不再泄漏或被应用于不恰当的元素。

通常我们将正常的 dom 节点叫做 light dom,用于区分正常 dom 和 shadow dom

缺点:

  • 组件库问题,比如常见的弹窗、提示条等,组件库一般都是挂载到 body 下面的,但是 shadow 会隔离 css,导致子应用里对这些挂载在 body 下的组件失去样式控制
  • 事件代理问题,如 react 是通过自己的合成事件机制处理事件的,即所有事件都要冒泡到 body 元素来进行处理,但 shadow dom 包裹的元素其对应的事件不会冒泡到 body

scoped Css 沙箱

experimentalStyleIsolation: true

qiankun 会改写子应用所添加的样式,为所有样式规则增加一个特殊的选择器规则来限定其影响范围

类似如下:

css
/* 我们自己写的 css 类名 */
.app-main {
  font-size: 14px;
}

/* 转化之后的 css 类名 */
div[data-qiankun-子应用名称] .app-main {
  font-size: 14px;
}

这个就有点想 vue 的 css scoped 了

缺点:

  • @keyframes , @font-face , @import , @page 不被支持
  • 子应用 css 中的 var 变量会丢失
  • 同 shadow dom,组件库那些挂载在 body 下的组件样式丢失

qiankun css 解决方案

  • vue:可以使用自己的 css scoped 减少样式冲突,针对少部分异常样式可以针对性修改
  • react: 使用 css module 代替,如 xxx.module.less / xxx.module.scss / xxx.module.css
  • 使用 tailwind / windicss / unocss 等原子化的 css 框架

本站中引用到的其他资料,如有侵权,请联系本人删除