【微前端-Single-SPA、qiankun的基本原理和使用】

news2024/10/7 10:12:50

背景

在实际项目中,随着日常跌倒导致的必然墒增,项目会越来越冗余不好维护,而且有时候一个项目会使用的其他团队的功能,这种跨团队不好维护和管理等等问题,所以基于解决这些问题,出现了微前端的解决方案。微前端具有拆分和集成的特点,本文主要讲解主流的single-spa以及qiankun的基本原理和使用。

微前端

定义:微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略,就是将多个应用集合到一起,通过动态匹配的方式展示。
特点: 与技术无关、渐进式增量升级和迁移、模块隔离、运行时子应用状态不共享
解决问题:解决随着项目迭代会导致必然墒增,导致项目越来越庞大冗余,不好维护的问题,微前端将一个大应用拆分为可以独立运行的子应用,然后集合子应用。
image.png

Single-SPA

组合式路由分发:由主容器监听路由变化,动态进行子应用的挂载/卸载

特点

网页打开主应用会执行registerApplication注册子应用,然后会监控路由变化,当路由匹配activeWhen时,会先坚持子应用状态先通过在子应用注入的生命周期卸载上一个子应用,然后挂载匹配路由的子应用。函数流程大致是:registerApplication -> start -> bootstrap -> mount-> unmount
image.png

1、主应用注册子应用

singleSpa.registerApplication({
 'app1',
  () => import('src/app1/main.js'),
  (location) => location.pathname.startsWith('/app1'),
  customProps: { authToken: "d83jD63UdZ6RS6f70D0" }
});
singleSpa.registerApplication({
  'app2',
  () => import('src/app2/main.js'),
  (location) => location.pathname.startsWith('/app2'),
  customProps: (name, location) => {
    return { authToken: "d83jD63UdZ6RS6f70D0" };
  }
});

2、子应用注入主应用控制的生命周期

bootstrap: 初始化

应用挂载之前的初始化,这个生命周期函数会在应用第一次挂载前执行一次

mount:挂载

每当应用的activity function返回真值(registerApplication的第三个参数),但该应用处于未挂载状态时,挂载的生命周期函数就会被调用。调用时,函数会根据URL来确定当前被激活的路由,然后加载对应的js文件,创建DOM元素、监听DOM事件等以向用户呈现渲染的内容。任何子路由的改变(如hashchange或popstate等)不会再次触发mount,需要各应用自行处理。

unmount:卸载

每当应用的activity function返回假值,但该应用已挂载时,卸载的生命周期函数就会被调用。卸载函数被调用时,会清理在挂载应用时被创建的DOM元素、事件监听、内存、全局变量和消息订阅等。


let domEl;
export function bootstrap(props) {
    return Promise
        .resolve()
        .then(() => {
            domEl = document.createElement('div');
            domEl.id = 'app1';
            document.body.appendChild(domEl);
        });
}
export function mount(props) {
    return Promise
        .resolve()
        .then(() => {
            // 在这里通常使用框架将ui组件挂载到dom。请参阅https://single-spa.js.org/docs/ecosystem.html。
            domEl.textContent = 'App 1 is mounted!'
        });
}
export function unmount(props) {
    return Promise
        .resolve()
        .then(() => {
            // 在这里通常是通知框架把ui组件从dom中卸载。参见https://single-spa.js.org/docs/ecosystem.html
            domEl.textContent = '';
        })
}
unload:移除

可选的生命周期,只有当主应用调用unloadApplication时,才会触发子应用中的unload函数,一旦应用被移除,它的状态将会变成NOT_LOADED,下次激活时会被重新初始化bootstrap阶段。

timeout:超时设置

主应用可以针对每个阶段进行超时设置,超过设置时间还没完成,则会抛出异常。
全局配置:

import { setBootstrapMaxTime, setMountMaxTime, setUnmountMaxTime, setUnloadMaxTime } from 'single-spa';

// 设置 bootstrap 阶段的最大超时时间为 5 秒
setBootstrapMaxTime(5000, '应用 bootstrap 超时');

// 设置 mount 阶段的最大超时时间为 10 秒
setMountMaxTime(10000, '应用 mount 超时');

// 设置 unmount 阶段的最大超时时间为 5 秒
setUnmountMaxTime(5000, '应用 unmount 超时');

// 设置 unload 阶段的最大超时时间为 5 秒
setUnloadMaxTime(5000, '应用 unload 超时');

给特定的子应用配置:

import { registerApplication } from 'single-spa';

registerApplication({
  name: 'my-app',
  app: () => System.import('my-app'),
  activeWhen: ['/my-app'],
  customProps: {
    bootstrapTimeout: 5000, // bootstrap 阶段的超时时间
    mountTimeout: 10000,    // mount 阶段的超时时间
    unmountTimeout: 5000,   // unmount 阶段的超时时间
    unloadTimeout: 5000,    // unload 阶段的超时时间
  }
});

超时处理的默认行为:
当 single-spa 检测到应用在指定阶段的操作超过了配置的超时时间时,默认的行为是:

  1. Bootstrap 阶段超时: 应用将无法进入 MOUNTED 状态,single-spa 会抛出错误。
  2. Mount 阶段超时: 应用将无法完全加载和显示,single-spa 会抛出错误。
  3. Unmount 阶段超时: 应用无法正确卸载,可能导致残留的状态或资源占用。
  4. Unload 阶段超时: 应用无法被卸载,可能导致内存泄漏或其他问题。

3、主应用启动/卸载子应用

卸载有两种:

  • 子应用切换:当子应用A切换B时,会先卸载当前子应用A,然后挂载匹配的子应用B,这时候A的资源还是在内存中,以便切回A时能快速响应,如果切回A,则不会进入bootstrap初始化周期。
  • 卸载子应用:当主应用彻底卸妆子应用A时,会调用A中的unload生命周期,并且会将子应用状态设置为NOT_LOADED,然后将A从内存中移除,再次启动A时,会重新进入bootstrap初始化周期。

// 启动 single-spa
singleSpa.start();

// 卸载子应用
unloadApplication('my-app').then(() => {
  console.log('子应用已经卸载');
  
  // 模拟用户导航到子应用的路径,触发重新加载
  navigateToUrl('/container');
});

4、生命周期执行流程

  1. 加载阶段:
    • 子应用第一次被加载时,single-spa 会依次执行子应用的 bootstrap 和 mount 钩子。
  2. 挂载阶段:
    • 子应用在页面上呈现时,single-spa 会执行子应用的 mount 钩子。
  3. 卸载阶段:
    • 当子应用不再需要显示时,single-spa 会调用 unmount 钩子,将子应用从页面上移除。
    • 这时,子应用的状态和资源可能仍然保留在内存中,以便快速重新加载。
  4. 完全卸载阶段:
    • 调用 unloadApplication 时,single-spa 会执行子应用的 unload 钩子,并将子应用的资源从内存中释放。
    • 下一次匹配到这个子应用时,会重新执行 bootstrap 钩子。

5、存在的问题

  • 手动加载资源,会根据子应用打包方式改变

single-spa采用的是Js Entry的方式来加载子应用。需要手动加载js、css等资源,如果子应用打包方式改变(比如生成的资源路径变化),主应用中也要变化。

import { registerApplication } from 'single-spa';
registerApplication({
    name: 'app',
    () => import('src/app1/main.js'), // 手动加载Js资源
    (location) => location.pathname.startsWith('/app1'),
})

singleSpa.start()
  • 代码冲突

single-spa主要通过Js Entry将多个子应用引入到主应用,多个Js运行在主应用的全局上下文中,可能存在命名冲突、样式冲突、Js冲突等问题。需要手动做JS、CSS的隔离。

  • 子应用通信

Single-SPA只是将多个子应用挂载在主应用,而子应用之间互相通信,没有内置功能,需要自己手动实现。

qiankun

qiankun 是一个基于 single-spa 的微前端实现库,解决了single-spa的一些问题,就是一个更完善的single-spa库。主要是解决了single-spa的一些问题,并做了丰富。使用思路和single-spa大致一样,在主应用中注册子应用,然后在子应用中写入主应用的生命周期等待主应用的调用。

  • 需要手动写加载资源路径,如果子应用打包方式改变,主应用资源加载路径就要调整
  • 使用Js Entry导致多应用间Js、Css代码冲突问题
  • 子应用间通信

特点

  • 沙箱隔离: 嵌入主容器的不同子应用的Js、Css是相互隔离的,1、通过scope的方式,在dom节点和样式都加上前缀,避免冲突。2、Proxy代理对象的方式,子应用访问的是这个代理对象,CSS 的隔离就是使用 shadow dom ,这是浏览器支持的特性,shadow root 下的 dom 的样式是不会影响其他 dom 的。

解决问题一

通过Html Entry的方式,在主应用配置子应用的html即可,qiankun会自动加载并解析html文件,并将script脚本部分解析拆离出来单独加载,其他html部分添加到主应用到dom中。以此来解决需要用户手动写资源url路径的问题(这个功能的实现放在 import-html-entry 这个包里)

registerMicroApps([
  {
    name: 'react app', // app name registered
    entry: '//localhost:7100', // 会自动加载html文件
    container: '#yourContainer',
    activeRule: '/yourActiveRule',
  },
  {
    name: 'vue app',
    entry: { scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);

子应用的html文件:
image.png
挂载在主应用的dom上:
image.png

解决问题二: JS、CSS隔离

JS隔离

主要就是全局window的隔离
qiankun 实现 window 隔离有三种沙箱:

  • 快照,加载子应用前记录下 window 的属性,卸载之后恢复到之前的快照
  • diff,加载子应用之后记录对 window 属性的增删改,卸载之后恢复回去
  • Proxy,创建一个代理对象,每个子应用访问到的都是这个代理对象

image.png
从源码中能看出来,当浏览器支持Proxy的时候,会根据useLooseSandbox来判断使用LegacySandbox还是ProxySandbox。这两个都是基于Proxy代理的,但是LegacySandbox的隔离度不高,对子应用进行基本隔离,主要是兼容较老的一些浏览器。ProxySandbox是拦截子应用的所有全局变量和函数的访问和修改,是强隔离的。当浏览器不支持Proxy时,会降级使用快照来隔离。

由于前两种都是先记录window的更改,当卸载之后再恢复之前的状态,所以不能存在多个子应用,否则会发生冲突,一般还是使用Proxy代理沙箱。

Proxy代理沙箱

Proxy代理对象,子应用访问的是这个代理对象,Proxy对代理对象进行了get、set拦截操作,会隔离不同子应用,使其只会访问自身上下文,不会影响全局对象。

createFakeWindow

通过createFakeWindow创建ProxyWindow代理:通过createFakeWindow传入全局window,会对其进行代理,返回一个代理代理window(fakeWindow)和一个保存拥有自定义getter属性的对象(propertiesWithGetter)
为什么需要propertiesWithGetter?
ProxySandbox 通过 Proxy 来代理对全局对象(如 window)的访问。但是,有些属性具有自定义的 getter 方法,这些 getter 方法在属性被访问时会执行特定的逻辑。这些属性不能简单地通过 Proxy 来直接代理,因为访问时需要正确触发这些 getter 方法。

	function createFakeWindow(globalContext, speedy) {
	  var propertiesWithGetter = new Map();
	  var fakeWindow = {};
	  Object.getOwnPropertyNames(globalContext).filter(function (p) {
	    var descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
	    return !(descriptor === null || descriptor === void 0 ? void 0 : descriptor.configurable);
	  }).forEach(function (p) {
	    var descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
	    if (descriptor) {
	      var hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
	      if (p === 'top' || p === 'parent' || p === 'self' || p === 'window' ||
	      p === 'document' && speedy || inTest ) {
	        descriptor.configurable = true;
	        if (!hasGetter) {
	          descriptor.writable = true;
	        }
	      }
	      if (hasGetter) propertiesWithGetter.set(p, true);
	    }
	  });
	  return {
	    fakeWindow: fakeWindow,
	    propertiesWithGetter: propertiesWithGetter
	  };
	}
通过Proxy代理window

基于代理fakeWindow对象,对子应用的操作进行拦截
image.png
以get为例:

get: function get(target, p) {
	        _this.registerRunningApp(name, proxy);
	        if (p === Symbol.unscopables) return unscopables;
	        if (p === 'window' || p === 'self') {
	          return proxy;
	        }
	        if (p === 'globalThis' || inTest ) {
	          return proxy;
	        }
	        if (p === 'top' || p === 'parent' || inTest ) {
	          if (globalContext === globalContext.parent) {
	            return proxy;
	          }
	          return globalContext[p];
	        }
	        if (p === 'hasOwnProperty') {
	          return hasOwnProperty;
	        }
	        if (p === 'document') {
	          return _this.document;
	        }
	        if (p === 'eval') {
	          return eval;
	        }
	        if (p === 'string' && globalVariableWhiteList.indexOf(p) !== -1) {
	          return globalContext[p];
	        }
	        var actualTarget = propertiesWithGetter.has(p) ? globalContext : p in target ? target : globalContext;
	        var value = actualTarget[p];
	        if (isPropertyFrozen(actualTarget, p)) {
	          return value;
	        }
	        if (!isNativeGlobalProp(p) && !useNativeWindowForBindingsProps.has(p)) {
	          return value;
	        }
	        var boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;
	        return rebindTarget2Fn(boundTarget, value);
	      },

其中globalVariableWhiteList用来保存一个允许全局访问的变量或属性的白名单。这个白名单中的变量或属性在沙箱的代理过程中不会被隔离或修改,允许子应用直接访问和操作这些全局变量,qiankun不会对白名单中的属性进行修改。

const globalVariableWhiteList = [
  'window',
  'self',
  'globalThis',
  'document',
  'navigator',
  'location',
  'localStorage',
  'sessionStorage',
  'fetch',
  'XMLHttpRequest',
  'console',
  'setTimeout',
  'setInterval',
  'clearTimeout',
  'clearInterval',
  'requestAnimationFrame',
  'cancelAnimationFrame'
];

Css隔离

1、Shadow DOM(影子DOM)

允许创建隔离的 DOM 子树,通过给真实DOM绑定一个Shadow DOM,然后将我们的样式通过添加到Shadow DOM树的方式添加,Shadow DOM树中的DOM天生不会影响真实DOM。shadow dom 是 web components 技术的一部分,其实就一个 attachShadow 的 api(原生)。web components 添加内容的时候,不直接 appendChild,而是通过attachshadow创建一个影子DOM,然后再在下面 appendChild。shadow dom 就是原生自带的CSS沙箱。内部样式影响不了外界,外界样式也影响不了 shadow dom 内的元素。但是使用组件的Modal弹窗这些默认挂载在body中的,样式就会有问题。

image.png

2、Scope CSS隔离

Scope CSS(作用域 CSS),通过给子应用的根节点添加一个前缀来进行区分,结合样式重写来让样式只会影响本应用的DOM(添加子应用唯一ID - 前缀),一般用文件路径和项目package.name来组成hash为前缀。
以Vue的scope为例:
image.png
Vue: vue的scoped样式其实也有问题,它是通过.vue文件在项目中的相对路径path+文件名进行计算hash值的,当主子应用中同时存在一个path和文件名相同时的.vue文件,它的data-v-xxxxx算出来就是一样的,此时样式还是会冲突。可以加上package.name来区分
React使用的是CSS Module来实现CSS隔离,在编译时会进行hash换算,所以在代码中要使用style[class_name]的方式来添加样式类

解决问题三: 应用间通信

single-spa需要自己实现应用间的状态共享,而qiankun提供的api:initGlobalState在主容器中初始化需要共享的状态,然后在挂载在子应用中的mount生命周期通过props.onGlobalStateChange来获取共享数据

initGlobalState 是 qiankun 提供的一个 API,用于创建一个全局的共享状态对象,并允许不同的微前端应用进行读写和监听这个共享状态的变化。这个共享状态管理系统与沙箱机制是分开的,不依赖window,而是由qiankun自身维护的一个状态管理系统。qiankun会监控这个状态,当状态发生变化时会通知所有订阅了这个状态变化的应用。

import { initGlobalState, MicroAppStateActions } from 'qiankun';

// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
// 设置状态
actions.setGlobalState(state);
// 监控状态变化
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
// 取消监听
actions.offGlobalStateChange();
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });

  props.setGlobalState(state);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1868292.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FFmpeg教程-二-代码实现录音

目录 一,实现步骤 二,具体实现 1,注册设备 2,获取输入格式对象 3,打开设备 4,采集数据 一,实现步骤 二,具体实现 1,注册设备 // 初始化libavdevice并注册所有输入…

Java版本Spring Cloud+SpringBoot b2b2c:Java商城实现一件代发设置及多商家直播带货商城搭建

一、产品简介 我们的JAVA版多商家入驻直播带货商城系统是一款全*面的电子商务平台,它允许商家和消费者在一个集成的环境中进行互动。系统采用先进的JAVA语言开发,提供多商家入驻、直播带货、B2B2C等多种功能,帮助用户实现线上线下的无缝对接…

M Farm RPG Assets Pack(农场RPG资源包)

🌟塞尔达的开场动画:风鱼之歌风格!🌟 像素参考:20*20 字体和声音不包括在内 资产包括: 1名身体部位分离的玩家和4个方向动画: 闲逛|散步|跑步|持有物品|使用工具|拉起|浇水 6个带有4个方向动画的工具 斧头|镐|喙|锄头|水壶|篮子 4个NPC,有4个方向动画: 闲逛|散步 �…

理解IP地址与域名:访问网站的基石

在互联网的世界里,每一次点击、每一次浏览都伴随着一个神秘的数字串——IP地址,以及一个易于记忆的字符串——域名。对于普通用户而言,这两者可能只是浏览网页时的“幕后英雄”,但实际上,它们构成了我们访问网站的基石…

人工神经网络是什么,其应用有哪些?

人工神经网络是什么,其应用有哪些? 当你阅读这篇文章时,你身体的哪个器官正在考虑它?当然是大脑!但是你知道大脑是如何工作的吗?嗯,它有神经元或神经细胞,它们是大脑和神经系统的主要…

STM32HAL库 -- RS485 开发板通信(速记版)

在本章中, 我们将使用 STM32F429的串口 2 来实现两块开发板之间的 485 通信(半双工)。 RS485 简介 485(一般称作 RS485/EIA-485)隶属于 OSI 模型物理层,是串行通讯的一种。电气特性规定为 2 线,半双工,多…

java-数据结构与算法-02-数据结构-02-链表

文章目录 1. 概述2. 单向链表3. 单向链表(带哨兵)4. 双向链表(带哨兵)5. 环形链表(带哨兵)6. 习题E01. 反转单向链表-Leetcode 206E02. 根据值删除节点-Leetcode 203E03. 两数相加-Leetcode 2E04. 删除倒数…

匠心铸就服务品质,全视通技术服务获盘锦市中医医院高度认可

一声表扬,万分肯定 寥寥数语,情意深重 承载着荣誉 道出了心声 传达了谢意 倾注了期盼 字里行间的内容 是对全视通技术服务的高度认可 记录了全视通与盘锦市中医医院之间的双向奔赴 盘锦市中医医院表扬信是对全视通技术服务团队工作的高度认可&am…

JAVA进阶学习09

文章目录 一、双列集合Map1.1 双列集合介绍1.2 双列集合Map常见API1.3 Map集合遍历方式1.3.1 通过集合的全部键来遍历集合1.3.2 Map集合遍历方式21.3.3 Map集合遍历方式3 二、Map集合的实现类2.1 HashMap类2.2 LinkedHashMap2.3 TreeMap 三、可变参数四、Collections类五、集合…

【数据分享】《中国文化及相关产业统计年鉴》2013-2022

而今天要免费分享的数据就是2013-2022年间出版的《中国文化及相关产业统计年鉴》并以多格式提供免费下载。(无需分享朋友圈即可获取) 数据介绍 在过去的十年里,中国的文化及文化产业经历了翻天覆地的变化。随着《中国文化及相关产业统计年鉴…

ETO外汇:日元技术分析,美元/日元、欧元/日元、英镑/日元未来走势如何?

摘要: 根据近期的市场分析,美元/日元、欧元/日元和英镑/日元这三组货币对在未来的走势将受到多方面因素的影响。本文将从技术角度对每一组货币对进行详细分析,帮助投资者了解可能的支撑和阻力位,以及未来的走势预期。通过对关键技…

Python高精度浮点运算库之mpmath使用详解

概要 在科学计算和工程应用中,精确的数学计算至关重要。Python 作为一种灵活而强大的编程语言,提供了多种数学库,其中 mpmath 库因其高精度浮点运算和丰富的数学函数支持而备受关注。mpmath 库不仅适用于基本的高精度计算,还支持复数运算、矩阵运算和特殊函数计算,广泛应…

iptables(12)实际应用举例:策略路由、iptables转发、TPROXY

简介 前面的文章中我们已经介绍过iptables的基本原理,表、链的基本操作,匹配条件、扩展模块、自定义链以及网络防火墙、NAT等基本配置及原理。 这篇文章将以实际应用出发,列举一个iptables的综合配置使用案例,将我们前面所涉及到的功能集合起来,形成一个完整的配置范例。…

前端vue自定义鼠标指针的图标并且单击鼠标时点击的地方会出现烟花特效

//实现在某一个div内鼠标的指针形状呈自定义的图标 。&#xff08;也可以全局定义&#xff0c;当前时在某一个div内的实例&#xff09; //DOM <div class"firework-container" click"createFirework"></div> //js部分 const createFirework …

lodash.js 工具库

lodash 是什么? Lodash是一个流行的JavaScript实用工具库,提供了许多高效、高兼容性的工具函数,能够方便地处理集合、字符串、数值、函数等多种数据类型,大大提高工作效率。 lodash官网 文档参见:Lodash Documentation lodash 在Vue中怎么使用? 1、首先安装 lodash np…

Fizz Buzz 经典问题 - 蓝桥杯

基础知识要求&#xff1a; Java&#xff1a;方法、if else语句、算术运算符、逻辑运算符、Scanner类 Python&#xff1a; 方法、if else语句、算术运算符、逻辑运算符、input() 题目&#xff1a; 思路解析&#xff1a; 读取输入&#xff1a; 从标准输入或其他方式读取一个整数…

京津冀一体化十年辉煌|雄安宣武医院携手唯迈医疗大放异彩

京津冀一体化协同发展战略自提出以来&#xff0c;已走过辉煌的十年历程。在这十年间&#xff0c;三地共同推动区域经济发展&#xff0c;优化资源配置&#xff0c;不仅经济深度融合&#xff0c;更在医疗、教育、文化等多个领域实现了资源共享、优势互补&#xff0c;取得了举世瞩…

RabbitMQ的Fanout交换机

Fanout交换机 Fanout&#xff0c;英文翻译是扇出&#xff0c;我觉得在MQ中叫广播更合适。 在广播模式下&#xff0c;消息发送流程是这样的&#xff1a; 1&#xff09; 可以有多个队列2&#xff09; 每个队列都要绑定到Exchange&#xff08;交换机&#xff09;3&#xff09; …

vrrp与mstp

简单实验步骤

Redis缓存问题二、缓存雪崩

缓存雪崩 缓存雪崩&#xff1a;是指在同一时段大量的缓存key同时失效或者Redis服务宕机&#xff0c;导致大量请求到达数据库&#xff0c;带来巨大压力。 缓存雪崩的解决方案&#xff1a; 给不同的Key的TTL添加随机值利用Redis集群提高服务的可用性给缓存业务添加降级限流策略…