微前端

  • 微前端
    • 基本知识
    • 主要的微前端框架
      • iframe
        • 优点:
        • 缺点:
      • single-spa
        • 示例
          • 主应用
            • spa-root-config.js
            • microfrontend-layout.html
            • index.ejs
          • 子应用
            • spa-react-app2.js
            • root.component.js
          • 修改路由
            • spa-demo/microfrontend-layout.html
            • spa-demo/react-app1/webpack.config.js
            • spa-demo/react-app2/webpack.config.js
            • spa-demo/react-app1/src/root.component.js
          • 两个应用连接,将两个应用展示在同一个页面上
            • spa-demo/microfrontend-layout.html
          • 实现样式隔离
            • 创建styles文件,并启动服务
            • 子应用中写入
            • 主应用的css策略也要改
          • JS隔离
            • 子应用中
      • qiankun
        • 使用
          • 安装依赖
          • 启动服务
        • ShadowDom 影子DOM
          • 示例:
          • 使用shadowDom
        • proxySandbox VS snapshotSandbox
        • 全局弹窗
        • 路由
        • 预加载
        • 通信方式
  • EMP - 同架构应用的微前端

  • 微前端基础知识介绍
  • 常见框架
  • 对比总结

微前端

基本知识

微前端,Micro-Frontends,多个前端组成的前端
后端比较早使用到微前端,和后端微服务结合
对于 Web服务 来讲,后端是一个又一个的接口
前端对接口之前的关联是不在意的
微服务,多人协作问题,版本迭代问题,服务整体稳定性问题
前端请求时候,通过在请求头中增加东西,后端识别后,将请求引导到新的服务上去
后端通过微服务将每个服务单独部署,给单个服务增加数量或者使用容器化的技术,将服务容量提升
将多个各自独立的单体应用服务共同打包聚合成一个单独应用的方式

一个web前端应用中,直接运行其他web应用

特点:

  • 技术栈无关:子应用是react,vue,vue3的都是没有关系的,各个应用之间是完全独立(逻辑上)的
  • 独立开发、部署、仓库都独立
  • 增量升级:复杂的站点,看起来是一个页面,但是其中分了很多的模块,各个模块之前可能是完全不同部门来负责的,发布的应用可能导致整个页面挂掉,这肯定是不允许的,因此需要将不同部分负责的页面尽量是独立的。
  • 状态隔离:运行时的数据状态是独立的,不包括子应用,主副应用通信这种情况
  • 环境隔离:应用,css之间是隔离的
  • 消息通信:应用之间通信降低沟通成本,有整体体验
  • 依赖复用:应用是不同部门的,但是依赖是同一个版本的,是可以复用的。一般是使用 webpack 的 external 来进行复用的

主要的微前端框架

iframe

优点:

浏览器层面就直接支持,不需要搞其他任何开发就可以直接使用
不同的iframe之间是完全隔离的,各个iframe之间是完全不影响的,有自己的js线程,有自己的dom树,css样式树
浏览器对iframe本身有很多的安全限制,防止出现安全问题

缺点:
  • URL 是不同步的
    在浏览器的主应用中改变hashTak,各个iframe之间拿不到主应用的基本信息,页面刷新以下,各个iframe中的url就会自动回到副应用设置的初始值去
  • UI不同步
    弹窗,抽屉只能在当前iframe中去实现,没有办法做整个页面的抽屉和弹窗
  • 完全隔离
    各个iframe共享资源是不可能的,比如,主应用登录后,子应用想要免登录也是不可能的,禁止读取cookie等
  • 速度
    各个应用都要跑完一遍整个生命周期,域名解析,资源加载,loading,数据请求等全部都是自己独立的,无法实现资源复用,体验差

single-spa

用生命周期的概念实现各个子应用之间的加载,卸载,状态管理等等
子应用的调度,url的变化,各种事件的传递,各种函数的处理全都是根据生命周期来的

注册应用 -> url变化 -> app active,app激活状态,一个url对应着一个或多个子应用 -> 找到子应用 -> 执行子应用的生命周期钩子,life cycle

生命周期钩子:
bootstrap 状态初始化
mount 加载
unmount 卸载

应用分类:

  • root-config 主应用:负责注册管理所有的子应用,还有整体的node管理
  • app-parcel 子应用
示例

执行:

pnpx create-single-spa --moduleType root-config

在这里插入图片描述

将版本自动升级的符号去掉后安装,并且改下 webpack-config-single-spa-react
在这里插入图片描述

pnpm i

cd spa-demo

创建子应用1:

pnpx create-single-spa --moduleType app-parcel

也是一样将版本升级符号去掉

在这里插入图片描述
创建子应用2:react-app2,步骤和上面一样

在这里插入图片描述

重新强制安装

pnpm install --force --ignore-scripts

主应用

主应用启动的时候是可以配置预加载的,可以将子应用加载起来,但是不展示它,当要展示的时候,就会特别快

主应用目录:
在这里插入图片描述

spa-root-config.js

主应用启动的流程

import { registerApplication, start } from "single-spa";
import {constructApplications,constructRoutes,constructLayoutEngine,
} from "single-spa-layout";
import microfrontendLayout from "./microfrontend-layout.html";// 定义程序
const routes = constructRoutes(microfrontendLayout);
// 定义主应用程序
const applications = constructApplications({routes, //路由列表// 加载app应用程序loadApp({ name }) {return import(/* webpackIgnore: true */ name);},
});
const layoutEngine = constructLayoutEngine({ routes, applications });applications.forEach(registerApplication);
layoutEngine.activate();
start();
microfrontend-layout.html

主应用路由定义文件

<single-spa-router><!--This is the single-spa Layout Definition for your microfrontends.See https://single-spa.js.org/docs/layout-definition/ for more information.--><!-- Example layouts you might find helpful:<nav><application name="@org/navbar"></application></nav><route path="settings"><application name="@org/settings"></application></route>--><main><!-- 默认路由,也就是 / 这个路由 --><route default><!-- 执行 @single-spa/welcome 子应用 --><application name="@single-spa/welcome"></application></route></main>
</single-spa-router>
index.ejs

html的模板文档

将后续的子应用启动地址填入这里:
在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Root Config</title><meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';"><meta name="importmap-type" use-injector /><!-- If you wish to turn off import-map-overrides for specific environments (prod), uncomment the line below --><!-- More info at https://github.com/single-spa/import-map-overrides/blob/main/docs/configuration.md#domain-list --><!-- <meta name="import-map-overrides-domains" content="denylist:prod.example.com" /> --><!-- Shared dependencies go into this import map --><!-- 默认注册这个 --><script type="injector-importmap">{"imports": {"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.3/lib/es2015/esm/single-spa.min.js"}}</script><link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@6.0.3/lib/es2015/esm/single-spa.min.js" as="module"><!-- Add your organization's prod import map URL to this script's src  --><!-- <script type="injector-importmap" src="/importmap.json"></script> --><!-- 如果是本地的话,还要注册这个 --><!-- 子应用在这里注册 --><% if (isLocal) { %><script type="injector-importmap">{"imports": {"@spa/root-config": "//localhost:9000/spa-root-config.js","@spa/react-app1": "http://localhost:8081/spa-react-app1.js","@spa/react-app2": "http://localhost:8080/spa-react-app2.js","@single-spa/welcome": "https://cdn.jsdelivr.net/npm/single-spa-welcome/dist/single-spa-welcome.min.js"}}</script><% } %><script src="https://cdn.jsdelivr.net/npm/import-map-overrides@5.1.1/dist/import-map-overrides.js"></script><script src="https://cdn.jsdelivr.net/npm/@single-spa/import-map-injector@2.0.1/lib/import-map-injector.js"></script>
</head>
<body><noscript>You need to enable JavaScript to run this app.</noscript><main></main><script>window.importMapInjector.initPromise.then(() => {import('@spa/root-config');});</script><import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>

启动主应用:
在这里插入图片描述
在这里插入图片描述
显示的是 默认路由:@single-spa/welcome 这个页面

添加开发工具:
在控制台执行:
localStorage.setItem(‘devtools’,true)
在这里插入图片描述
在这里插入图片描述

子应用

子应用是不去自己启动自己的,只是告诉主应用有 bootstrap,mount,unmount 三个钩子,让主应用在合适的时机触发这三个钩子就行了

子应用代码:
在这里插入图片描述

spa-react-app2.js

向主应用提供的钩子

import React from "react";
import ReactDOMClient from "react-dom/client";
import singleSpaReact from "single-spa-react";
import Root from "./root.component";// singleSpaReact 创建整个应用
const lifecycles = singleSpaReact({React,ReactDOMClient,rootComponent: Root, //根组件// 捕获react错误errorBoundary(err, info, props) {// Customize the root error boundary for your microfrontend here.return null;},
});// 子应用入口导出这三个生命周期钩子,主应用调用mount方法,则会进入到rootComponent
export const { bootstrap, mount, unmount } = lifecycles;

子应用往外导出是导出 根组件 还是 这三个生命周期钩子?
single-spa 子应用来说,不会去主动启动自己的,只会告诉主应用是子应用有这三个生命周期钩子,你在合适的时机,触发这三个生命周期钩子就行了

主应用启动的时候,是可以配置预加载的,可以将子应用加载起来,但是不展示它,预加载的时候,会执行 bootstrap 方法
如果当前页面url变化了,发现 react-app1处于激活状态,那么主应用就会调用mount方法,将当前页面的控制权交给你
当前页面url又变化了,react-app1已经从激活状态变到非激活状态了,会留一个清理现场的时间,则会调用 unmount,清理js,全局变量等

root.component.js

实际业务代码

// 指定了一个组件
export default function Root(props) {return <section>{props.name} is mounted!</section>;
}

启动子应用:
pnpm start:standalone 主应用可不启动,想要单独启动子应用,自己做调试开发时候可以使用这个命令

正常启动

pnpm start

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

修改路由
spa-demo/microfrontend-layout.html

在这里插入图片描述

<single-spa-router><!--This is the single-spa Layout Definition for your microfrontends.See https://single-spa.js.org/docs/layout-definition/ for more information.--><!-- Example layouts you might find helpful:<nav><application name="@org/navbar"></application></nav><route path="settings"><application name="@org/settings"></application></route>--><main><route default><!-- <application name="@single-spa/welcome"></application> --><div>main app</div></route><route path="react-app1"><application name="@spa/react-app1"></application></route><route path="react-app2"><application name="@spa/react-app1"></application></route></main>
</single-spa-router>
spa-demo/react-app1/webpack.config.js

在这里插入图片描述

const { merge } = require("webpack-merge");
const singleSpaDefaults = require("webpack-config-single-spa-react");module.exports = (webpackConfigEnv, argv) => {const defaultConfig = singleSpaDefaults({orgName: "spa",projectName: "react-app1",webpackConfigEnv,argv,outputSystemJS: false,});// 去掉共享依赖delete defaultConfig.externals;return merge(defaultConfig, {// modify the webpack config however you'd like to by adding to this object});
};

同理,在 react-app2 的 webpack.config.js 中 也加上 删除共享依赖这句:

spa-demo/react-app2/webpack.config.js
const { merge } = require("webpack-merge");
const singleSpaDefaults = require("webpack-config-single-spa-react");module.exports = (webpackConfigEnv, argv) => {const defaultConfig = singleSpaDefaults({orgName: "spa",projectName: "react-app2",webpackConfigEnv,argv,outputSystemJS: false,});// 去掉共享依赖delete defaultConfig.externals;return merge(defaultConfig, {// modify the webpack config however you'd like to by adding to this object});
};
spa-demo/react-app1/src/root.component.js
export default function Root(props) {return <section>hello,{props.name} is mounted!</section>;
}

重启 react-app1,react-app2:

在这里插入图片描述
在这里插入图片描述

两个应用连接,将两个应用展示在同一个页面上
spa-demo/microfrontend-layout.html

在这里插入图片描述

<single-spa-router><!--This is the single-spa Layout Definition for your microfrontends.See https://single-spa.js.org/docs/layout-definition/ for more information.--><!-- Example layouts you might find helpful:<nav><application name="@org/navbar"></application></nav><route path="settings"><application name="@org/settings"></application></route>--><main><div style="display:flex;gap:20px;margin:10px 20px"><a href="/">main app</a><a href="/react-app1">react-app1</a><a href="/react-app2">react-app2</a><a href="/all">All MicroFrontends Apps</a></div><route default><!-- <application name="@single-spa/welcome"></application> --><div>main app</div></route><route path="react-app1"><application name="@spa/react-app1"></application></route><route path="react-app2"><application name="@spa/react-app1"></application></route><route path="all"><div style="border: 1px solid green;"><application name="@spa/react-app1"></application></div><div style="border: 1px solid cyan;margin-top: 50px;"><application name="@spa/react-app2"></application></div></route></main>
</single-spa-router>

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

实现样式隔离

添加依赖 single-spa-css,这个依赖能够帮助我们管理css

在react-app1中安装:

pnpm add single-spa-css

在这里插入图片描述

创建styles文件,并启动服务

在这里插入图片描述

body {background-color: #aaa;
}

http-server启动一个服务
在这里插入图片描述
当前目录下所有静态资源的访问:
在这里插入图片描述
在这里插入图片描述

子应用中写入

spa-demo/react-app1/src/spa-react-app1.js:
在入口中,一般是:
挂载的时候做某个事情
卸载的时候做某个事情

先启动子应用,主应用,再启动样式服务,免得子应用的8080端口被占用然后报错
启动8888端口服务
在这里插入图片描述
在这里插入图片描述

import React from "react";
import ReactDOMClient from "react-dom/client";
import singleSpaReact from "single-spa-react";
import Root from "./root.component";
import singleSpaCss from 'single-spa-css';const lifecycles = singleSpaReact({React,ReactDOMClient,rootComponent: Root,errorBoundary(err, info, props) {// Customize the root error boundary for your microfrontend here.return null;},
});const styleLifeCycles = singleSpaCss({// 刚刚启动的样式服务地址cssUrls: ["http://192.168.10.4:8888/style.css"],// 是否在webpack打包中去除掉css文件webpackExtractedCss: false,// 子应用切换的时候就看不到样式了shouldUnmount: true
});// export const { bootstrap, mount, unmount } = lifecycles;// 数组的顺序就是在主应用调用的时候执行的顺序
// 先启动样式的bootstrap,再启动应用的bootstrap
export const bootstrap = [styleLifeCycles.bootstrap,lifecycles.bootstrap,
];export const mount = [styleLifeCycles.mount,lifecycles.mount,
];// 卸载的时候要先卸载主应用,再卸载样式
export const unmount = [lifecycles.unmount,styleLifeCycles.unmount,
];
主应用的css策略也要改

spa-demo/src/index.ejs:
在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Root Config</title><meta http-equiv="Content-Security-Policy" content="content-src * ws: wss:;"><meta name="importmap-type" use-injector /><!-- If you wish to turn off import-map-overrides for specific environments (prod), uncomment the line below --><!-- More info at https://github.com/single-spa/import-map-overrides/blob/main/docs/configuration.md#domain-list --><!-- <meta name="import-map-overrides-domains" content="denylist:prod.example.com" /> --><!-- Shared dependencies go into this import map --><script type="injector-importmap">{"imports": {"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.3/lib/es2015/esm/single-spa.min.js"}}</script><link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@6.0.3/lib/es2015/esm/single-spa.min.js" as="module"><!-- Add your organization's prod import map URL to this script's src  --><!-- <script type="injector-importmap" src="/importmap.json"></script> --><% if (isLocal) { %><script type="injector-importmap">{"imports": {"@spa/root-config": "//localhost:9000/spa-root-config.js","@spa/react-app1": "http://localhost:8080/spa-react-app1.js","@spa/react-app2": "http://localhost:8081/spa-react-app2.js","@single-spa/welcome": "https://cdn.jsdelivr.net/npm/single-spa-welcome/dist/single-spa-welcome.min.js"}}</script><% } %><script src="https://cdn.jsdelivr.net/npm/import-map-overrides@5.1.1/dist/import-map-overrides.js"></script><script src="https://cdn.jsdelivr.net/npm/@single-spa/import-map-injector@2.0.1/lib/import-map-injector.js"></script>
</head>
<body><noscript>You need to enable JavaScript to run this app.</noscript><main></main><script>window.importMapInjector.initPromise.then(() => {import('@spa/root-config');});</script><import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>

这样就修改了子应用的样式
在这里插入图片描述
在这里插入图片描述
添加了两个link,
preload:只是想要资源加载一下,但是不想展示它,或者不想应用它的时候,就使用preload,一般是在性能优化的时候会用到这个东西
没有preload修饰的:会直接将元素渲染出来

单独的 react-app2 是没有样式的:
在这里插入图片描述
react-app1,react-app2应用都在的情况:
在这里插入图片描述
样式也会被渲染,因为是用到了body上

样式隔离还有

  1. scopedCss,在样式上加一些特定的名字,确保css只影响应用中的方式
  2. shadowDOM

例子中这两种都没有用到,只是用了全局的css来演示了一下

JS隔离

做两个事情:

single-spa没有提供JS隔离,只是提供了一个扩展,让应用在卸载的时候可以将全局的,变量的影响给它移除掉
qiankun中会使用 sandbox 的方式,让每个应用之间,有自己完全隔离的全局变量/window变量,每个子应用都是自己的window,自己怎么改都不会影响到主应用的,但是在代码实践上其实和single-spa实现方式是一样的。

qiankun中的 sandbox 分两种:

  1. ProxySandbox
  2. LegacySandbox
子应用中
  1. 安装依赖

pnpm add single-spa-leaked-globals

在这里插入图片描述

  1. react-app1中应用
    在这里插入图片描述

spa-demo/react-app1/src/spa-react-app1.js

import React from "react";
import ReactDOMClient from "react-dom/client";
import singleSpaReact from "single-spa-react";
import Root from "./root.component";
import singleSpaCss from 'single-spa-css';
import singleSpaLeakedGlobals from 'single-spa-leaked-globals';// getLinkedGlobals:深层次对比前后两个对象是否发生变化
// restoreGlobals:应用启动之前,缓存的全局变量全部恢复过来
const {getLinkedGlobals,restoreGlobals} = singleSpaLeakedGlobals();const lifecycles = singleSpaReact({React,ReactDOMClient,rootComponent: Root,errorBoundary(err, info, props) {// Customize the root error boundary for your microfrontend here.return null;},
});const styleLifeCycles = singleSpaCss({// 刚刚启动的样式服务地址cssUrls: ["http://192.168.10.4:8888/style.css"],// 是否在webpack打包中去除掉css文件webpackExtractedCss: false,// 子应用切换的时候就看不到样式了shouldUnmount: true
});const jsLifeCycles = singleSpaLeakedGlobals({globalVariableNames: ['_','$','jQuery','mode','type']
})// export const { bootstrap, mount, unmount } = lifecycles;// 数组的顺序就是在主应用调用的时候执行的顺序
// 先启动样式的bootstrap,再启动应用的bootstrap
export const bootstrap = [styleLifeCycles.bootstrap,jsLifeCycles.bootstrap,lifecycles.bootstrap,
];export const mount = [styleLifeCycles.mount,jsLifeCycles.mount,lifecycles.mount,
];// 卸载的时候要先卸载主应用,再卸载样式
export const unmount = [lifecycles.unmount,jsLifeCycles.unmount,styleLifeCycles.unmount,
];

qiankun

qiankun github

  1. 在 single-spa 基础上去做的

  2. qiankun主要是运用原生的 fetch 方法,像刚刚spa中js,css应用的还是浏览器的能力,但是qiankun不一样,运用原生的 fetch 方法,来请求微应用的各种资源,将返回的内容再转换成对于的各种各样的字符串

  3. single-spa 与HTML文档是没有关系的,是直接导出的React组件去做这个事情,通过生命周期钩子来完成的
    qiankun会获取html文档,解析了后再做一个子应用

  • fetch:获取资源文件
  • processTpl:解析html做子应用,将所有的js文件,css文件,内敛的css文件全部解析出来
  • style 收集的所有的styles,通过 fetch方法
  • script 收集的所有的script对象,url,也会通过fetch方法拉回来
  • requestIdleCallback 浏览器在空闲时候会执行的回调,并且还会告诉剩余时间,然后做各种各样的事情,一般情况,只有低优先级的时候才会使用这个方法,因为有可能这个方法不执行,在高优先级的时候,使用 requestAnimationFrame 方法,这个方法是必定会执行的。
  • 匿名自执行函数:包裹住所有的JS,使用闭包来对JS做了限制,通过evl做了执行,定了上下文,通过传入的proxy来改变window的指向,沙箱机制
  • 渲染子应用

基于Single-spa封装的,也支持各种框架,通过解析html的方式兼容一整套的,类似 iframe,qiankun在iframe思路上实现功能的。
样式,使用 shadowDOM 做样式隔离的
js,通过 沙箱机制,将js代码拉回来后,通过evl/自执行函数,执行JS,限制上行文,包括window的访问的操作等,通过 proxy 代理
资源预加载,通过解析html搞定的

使用
安装依赖
  1. 安装qiankun主依赖

    pnpm i

    在这里插入图片描述

  2. 安装主应用main依赖

    pnpm i

    在这里插入图片描述

  3. 安装子应用react16依赖
    在这里插入图片描述

  4. 安装子应用vue依赖
    在这里插入图片描述

启动服务
  1. 启动vue服务

    pnpm start

  2. 启动react16服务

    pnpm start

    报错:
    在这里插入图片描述
    openssl 不支持
    使用cross-env加个环境变量将它忽略掉

    pnpm add --save-dev cross-env

    在这里插入图片描述
    修改启动代码:
    qiankun/examples/react16/package.json:

    cross-env NODE_OPTIONS=–openssl-legacy-provider rescripts start

    在这里插入图片描述

  3. 构建qiankun

    pnpm build

    报错: 在这里插入图片描述

isConstDestructAssignmentSupported 类型推导有问题
这里改一下:
在这里插入图片描述
成功了:
在这里插入图片描述

  1. 启动主应用

    pnpm start

    在这里插入图片描述

react16-home页面:
在这里插入图片描述
react16-about页面:
在这里插入图片描述
react16-弹窗:
在这里插入图片描述
Vue3的页面:
在这里插入图片描述
vue用的组件是elementUI,react用的是组件AntD

ShadowDom 影子DOM

qiankun的样式使用shadowDom来做隔离的,shadowDom不是真实的能看到的dom

示例:
  1. 创建html页面
    video.html:
<htmkl><body><video width="300" height="100" controls></video></body>
</htmkl>
  1. 启动一个httpServer端
    在这里插入图片描述
    在这里插入图片描述

只写了一个video标签,但是video上面有很多可操作的东西
在这里插入图片描述
可以看到这里就是shadowDom
shadowDom里面全部都是这样一个个dom元素,像上面各种控制的组件在里面都能找得到,本身在dom树里是没有单独存在的,它是在某一个元素上有一个完整的dom树这样
shadowDom对当前组件的dom和css都提供了封装,实际上在浏览器预览文档的时候就会指定dom写好这个元素,外部的配置不影响内部,内部的配置也不影响外部

在查看源代码的时候还是看不到shadowDom
shadowDOM有两种模式:1. open 2. close
就比如,这样:
在这里插入图片描述

open的时候,外部就可以访问shadowDom里面的元素,如果是close,那么对外部则是完全不可见的

使用shadowDom
<htmkl><body><!-- <video width="300" height="100" controls></video> --><script>// 给div挂载一个shadowDomconst ele = document.createElement("div");const shadow = ele.attachShadow({mode: "open",});shadow.innerHTML = "<div>hello world</div>";document.body.appendChild(ele);</script></body>
</htmkl>

在这里插入图片描述

proxySandbox VS snapshotSandbox
  • qiankun
    • src
      • sandbox
        • proxySandbox.ts
        • snapshotSandbox.ts 两个文件
  1. proxySandbox 使用代理的方式来为每一个子应用创建一个完全独立的环境,与window全局变量是没有任何关系的
  2. snapshotSandbox 通过single-spa-link-global的方式来做的,解决兼容问题,用于不支持proxy的低版本浏览器

在这里插入图片描述
两个都是基于SandBox基础上实现

snapshotSandbox.ts

/*** @author Hydrogen* @since 2020-3-8*/
import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces';function iter(obj: typeof window | Record<any, any>, callbackFn: (prop: any) => void) {// eslint-disable-next-line guard-for-in, no-restricted-syntaxfor (const prop in obj) {// patch for clearInterval for compatible reason, see #1490if (obj.hasOwnProperty(prop) || prop === 'clearInterval') {callbackFn(prop);}}
}/*** 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器*/
export default class SnapshotSandbox implements SandBox {proxy: WindowProxy;name: string;type: SandBoxType;sandboxRunning = true;private windowSnapshot!: Window;private modifyPropsMap: Record<any, any> = {};private deletePropsSet: Set<any> = new Set();constructor(name: string) {this.name = name; //名称this.proxy = window; //proxy就是windowthis.type = SandBoxType.Snapshot; //类型是Snapshot}// 激活的时候active() {// 记录当前快照this.windowSnapshot = {} as Window;// 迭代器对window进行迭代iter(window, (prop) => {// 将window上的属性给到windowSnapshot快照上this.windowSnapshot[prop] = window[prop];});// 恢复之前的变更Object.keys(this.modifyPropsMap).forEach((p: any) => {window[p] = this.modifyPropsMap[p];});// 删除之前删除的属性this.deletePropsSet.forEach((p: any) => {delete window[p];});this.sandboxRunning = true;}inactive() {this.modifyPropsMap = {};// 清理这个环境this.deletePropsSet.clear();// 然后迭代当前的windowiter(window, (prop) => {if (window[prop] !== this.windowSnapshot[prop]) {// 记录变更,恢复环境this.modifyPropsMap[prop] = window[prop];window[prop] = this.windowSnapshot[prop];}});iter(this.windowSnapshot, (prop) => {if (!window.hasOwnProperty(prop)) {// 记录被删除的属性,恢复环境this.deletePropsSet.add(prop);window[prop] = this.windowSnapshot[prop];}});if (process.env.NODE_ENV === 'development') {console.info(`[qiankun:sandbox] ${this.name} origin window restore...`,Object.keys(this.modifyPropsMap),this.deletePropsSet.keys(),);}this.sandboxRunning = false;}patchDocument(): void {}
}

proxySandbox.ts

/*** 基于 Proxy 实现的沙箱*/
export default class ProxySandbox implements SandBox {/** window 值变更记录 */private updatedValueSet = new Set<PropertyKey>();private document = document;name: string;type: SandBoxType;proxy: WindowProxy;sandboxRunning = true;latestSetProp: PropertyKey | null = null;active() {// proxy的sandbox是可以有多个实例的,也就是说,一个子应用是可以有多个sandbox的// 而snapshot的sandbox只有一个实例,每个子应用都只有一个上下文if (!this.sandboxRunning) activeSandboxCount++;this.sandboxRunning = true;}inactive() {if (process.env.NODE_ENV === 'development') {console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [...this.updatedValueSet.keys(),]);}// 恢复现场的// 如果在测试环境或者 activeSandboxCount === 0 时,需要恢复现场if (inTest || --activeSandboxCount === 0) {// reset the global value to the prev value// 遍历 globalWhitelistPrevDescriptor这个对象Object.keys(this.globalWhitelistPrevDescriptor).forEach((p) => {const descriptor = this.globalWhitelistPrevDescriptor[p];if (descriptor) {// 恢复了globalContext的代理Object.defineProperty(this.globalContext, p, descriptor);} else {// @ts-ignore// 清理环境delete this.globalContext[p];}});}this.sandboxRunning = false;}......constructor(name: string, globalContext = window, opts?: { speedy: boolean }) {......// 生成proxy,给fakeWindow添加getter/setterconst proxy = new Proxy(fakeWindow, {// 更改fakeWindow变量的值,而所有的更改都是改到globalWhitelistPrevDescriptor上去了,而不是改到fakeWindow上去set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {if (this.sandboxRunning) {this.registerRunningApp(name, proxy);// sync the property to globalContext 同步到全局上下文// 定义了globalWhitelistPrevDescriptor对象if (typeof p === 'string' && globalVariableWhiteList.indexOf(p) !== -1) {this.globalWhitelistPrevDescriptor[p] = Object.getOwnPropertyDescriptor(globalContext, p);// @ts-ignoreglobalContext[p] = value;} else {......}......}},// get返回的是rebindTarget2Fn函数的返回值// 而rebindTarget2Fn(boundTarget, value)的传参boundTarget有可能是nativeGlobal(原始global)和globalContext(虚拟的proxy代理的上下文)get: (target: FakeWindow, p: PropertyKey): any => {.......const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;return rebindTarget2Fn(boundTarget, value);},});
.......}
.......
}

这两个整体操作是类似的,只不过一个操作的是真的window对象,一个操作的是代理过的window对象,而且proxy支持不同激活的实例是单独的对象环境

全局弹窗

微前端在处理全局弹窗的时候,一般都是在body下面挂一个新的dom来处理的,就会有个问题,这里每个子应用都有自己的dom元素,而新的这个dom是不受子元素控制的,本身样式隔离,就比如qiankun,qiankun是用shadowDOM的方式将子应用样式做隔离的,而现在在子元素shadowDOM之外,也就是body下面创建一个新的全局弹窗,而这时全局弹窗的样式要怎么办呢?
那么这时候需要将样式能够达到实现一个全局的效果

qiankun是做了特殊处理的,可以将一些特定的样式挂载到主应用上去 qiankun的每个子应用是有一个单独的节点的
无界的shadowDOM本身就是挂载在主应用上的,因此它这时的全局弹窗就是真正的全局弹窗 无界直接将这个应用挂载到主应用上去了

路由

qiankun中,如果定义一个路径是给到子应用的,在页面刷新的开始时候需要一下子就进入到子应用的话,并且如果这个子应用根本就没有加载完成的话,那么就可能会匹配到主应用的404的问题。
因此,qiankun这里会有一个特殊的变动,与single-spa类似的,将url嵌入直接劫持掉了,只是qiankun的劫持稍微有点不同,这里可以参考源代码

预加载

qiankun里有一个预加载,比如

  • qiankun
    • examples
      • main
        • index.js

在这里插入图片描述
当启动主应用的时候,不管是否打开了react16,它都会加载react16所有的资源

通信方式

参考 globalState.ts的代码

  • qiankun
    • src
      • globalState.ts
/*** @author dbkillerf6* @since 2020-04-10*/import { cloneDeep } from 'lodash';
import type { OnGlobalStateChangeCallback, MicroAppStateActions } from './interfaces';let globalState: Record<string, any> = {};const deps: Record<string, OnGlobalStateChangeCallback> = {};// 触发全局监听
function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {Object.keys(deps).forEach((id: string) => {if (deps[id] instanceof Function) {deps[id](cloneDeep(state), cloneDeep(prevState));}});
}export function initGlobalState(state: Record<string, any> = {}) {if (process.env.NODE_ENV === 'development') {console.warn(`[qiankun] globalState tools will be removed in 3.0, pls don't use it!`);}if (state === globalState) {console.warn('[qiankun] state has not changed!');} else {const prevGlobalState = cloneDeep(globalState);globalState = cloneDeep(state);emitGlobal(globalState, prevGlobalState);}return getMicroAppStateActions(`global-${+new Date()}`, true);
}export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {return {/*** onGlobalStateChange 全局依赖监听** 收集 setState 时所需要触发的依赖** 限制条件:每个子应用只有一个激活状态的全局监听,新监听覆盖旧监听,若只是监听部分属性,请使用 onGlobalStateChange** 这么设计是为了减少全局监听滥用导致的内存爆炸** 依赖数据结构为:* {*   {id}: callback* }** @param callback* @param fireImmediately*/onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {if (!(callback instanceof Function)) {console.error('[qiankun] callback must be function!');return;}if (deps[id]) {console.warn(`[qiankun] '${id}' global listener already exists before this, new listener will overwrite it.`);}deps[id] = callback;if (fireImmediately) {const cloneState = cloneDeep(globalState);callback(cloneState, cloneState);}},/*** setGlobalState 更新 store 数据** 1. 对输入 state 的第一层属性做校验,只有初始化时声明过的第一层(bucket)属性才会被更改* 2. 修改 store 并触发全局监听** @param state*/setGlobalState(state: Record<string, any> = {}) {if (state === globalState) {console.warn('[qiankun] state has not changed!');return false;}const changeKeys: string[] = [];const prevGlobalState = cloneDeep(globalState);globalState = cloneDeep(Object.keys(state).reduce((_globalState, changeKey) => {if (isMaster || _globalState.hasOwnProperty(changeKey)) {changeKeys.push(changeKey);return Object.assign(_globalState, { [changeKey]: state[changeKey] });}console.warn(`[qiankun] '${changeKey}' not declared when init state!`);return _globalState;}, globalState),);if (changeKeys.length === 0) {console.warn('[qiankun] state has not changed!');return false;}emitGlobal(globalState, prevGlobalState);return true;},// 注销该应用下的依赖offGlobalStateChange() {delete deps[id];return true;},};
}

EMP - 同架构应用的微前端

EMP也是一个微组件的解决方案,借助的是webpack Module Federation,webpack的模块邦联
类似external,但是具体实现是不一样的
它可以将一个组件分成两种,一种叫remote,一种叫host
host应用是可以引用remote的应用的

将应用分成remote和host两种,但是我们每个应用是如果是由别人继承的话,就作为remote的状态,而真正的host应用是可以引用我,大家都在一个webpack里,但是都是独立的,因为打包都是独立的,每个子应用都作为remote的方式,每个remote之间都是独立的,打包,发布,部署也全是独立的

除了微前端之外还有一个微组件
微组件在业内不是很流行,但是在很多公司有很多的实践
module federation对于大部分公司来说是比较鸡肋的,由于都是同一套webpack配置,无需再分过多
module federation 针对一些应用比较复杂,或者说负责某一应用的部门特别多,做的东西被很多部门使用才会考虑这种模式
而做的东西被很多部门使用为什么不用npm包,是由于npm包是有一些问题的,因为它的版本号是锁定的,只有npm包的使用方才能决定用哪个版本,而发布方是没有办法控制别人用哪个包的,但是通过module federation就可以由开发方自己来控制

module federation使用场景有限,因为导出的是什么,别人就要用什么
而微前端是在一个应用中集成不同框架的东西,不管是vue还是react,最大特点是这个,但是module federation只能是一个框架里的,如果组件是react,那么使用方也得是react
而且module federation这个导出来的东西,导出的是一个组件,不是真正的单体应用。

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MemcacheRedis--缓存服务器理论

Memcached/redis是高性能的分布式内存缓存服务器,通过缓存数据库查询结果&#xff0c;减少数据库访问次数&#xff0c;以提高动态Web等应用的速度、 提高可扩展性。 缓存服务器作用: 加快访问速度 ,缓解数据库压力 1. memcached&#xff08;单节点在用&#xff09; 1.1 特点 1…

【stm32】标准库学习——I2C

目录 一、I2C 1.I2C简介 2.MPU6050参数 3.I2C时序基本单元 二、I2C外设 1.I2C外设简介 2.配置I2C基本结构 3.初始化函数模板 4.常用函数 一、I2C 1.I2C简介 本节课使用的是MPU6050硬件外设 2.MPU6050参数 3.I2C时序基本单元 这里发送应答是指主机发送&#xff0c;即…

HSA22HSA29美光固态芯片D8BJVC8BJW

HSA22HSA29美光固态芯片D8BJVC8BJW 美光固态芯片D8BJVC8BJW系列&#xff1a;技术革新与行业应用深度解析 一、技术解析&#xff1a;核心架构与创新突破 美光D8BJVC8BJW系列固态芯片&#xff08;如MT29F8T08EQLEHL5-QAES:E、MT29F512G08CUCABH3-12Q等&#xff09;的技术竞争力…

【Linux网络与网络编程】06.应用层协议HTTP

前言 虽然应用层协议是我们程序猿自己定的&#xff0c;但实际上已经有大佬们定义了一些现成的又非常好用的应用层协议供我们直接参考使用&#xff0c;HTTP(超文本传输协议)就是其中之一。 在互联网世界中&#xff0c;HTTP&#xff08;HyperText Transfer Protocol&#xff0c…

磁悬浮轴承的“生死线”:磁轴承气隙与保护轴承气隙的可靠性设计

在高速旋转机械的尖端领域&#xff0c;磁悬浮轴承&#xff08;AMB&#xff09;凭借无摩擦、超高转速、免润滑等优势傲视群雄。然而&#xff0c;其核心参数——气隙的设置&#xff0c;尤其是额定工作气隙与保护轴承&#xff08;辅助轴承&#xff09;气隙之间的大小关系与具体数值…

QT 学习笔记摘要(一)

第一节 QT介绍 1. QT概述 简单来说&#xff0c;QT就是一个跨平台的客户端技术&#xff0c;HTML画网页一样&#xff0c;而QT就是画客户端的&#xff0c;它不仅可以绘制界面而且可以做单机应用开发&#xff0c;还可以做网络程序的客户端界面开发 更专业的说法是&#xff1a;Qt 是…

QCustomPlot 叠加对象(Overlay Items)

QCustomPlot 提供了一系列可以在图表上叠加显示的对象&#xff08;items&#xff09;&#xff0c;这些对象不属于数据本身&#xff0c;而是用于标注、辅助线等用途。以下是主要叠加对象的详细说明和使用方法。 1. QCPItemStraightLine (无限直线) 特性&#xff1a; 无限延伸的…

Flink源码阅读环境准备全攻略:搭建高效探索的基石

想要深入探索Flink的底层原理&#xff0c;搭建一套完整且适配的源码阅读环境是必经之路。这不仅能让我们更清晰地剖析代码逻辑&#xff0c;还能在调试过程中精准定位关键环节。接下来&#xff0c;结合有道云笔记内容&#xff0c;从开发工具安装、源码获取导入到调试配置&#x…

Dify,FastGPT,RagFlow有啥区别,在智能问答方面有啥区别

Dify、FastGPT、RagFlow都是当前非常流行的低代码AI应用开发平台&#xff0c;它们都专注于让用户能够快速构建AI应用&#xff0c;但在设计理念、功能特色和适用场景上有明显区别。 让我详细对比一下这三个平台&#xff1a; 1. 平台定位对比 Dify 定位&#xff1a;全栈AI应用…

基站定位接口如何如何用PHP实现调用?

随着“新基建”战略的推进&#xff0c;我国移动通信基站数量快速增长。截至2024年底&#xff0c;全国基站总数已达1265万个&#xff0c;其中5G基站超425万个&#xff0c;构建起全球规模最大、技术领先的通信网络。 在这一基础上&#xff0c;基站查询API通过整合三大运营商数据…

Day 4:Shell流程控制——从“直线思维“到“智能决策“的进化

目录 一、if语句&#xff1a;你以为简单其实暗藏杀机1. 基础语法&#xff08;看似简单却容易踩坑&#xff09;2. 安全写法规范&#xff08;企业级建议&#xff09; 二、条件测试的六大门派1. 文件测试&#xff08;运维最爱&#xff09;2. 字符串比较&#xff08;引号是灵魂&…

分布式训练中的随机种子策略:深入理解与实践指南

分布式训练中的随机种子策略&#xff1a;深入理解与实践指南 引言&#xff1a;一个容易被忽视的关键细节 在深度学习的分布式训练中&#xff0c;你是否见过这样的代码&#xff1f; torch.manual_seed(process_seed) # PyTorch操作 random.seed(process_seed) # Pyth…

金山办公发布WPS智慧教育平台,发力教育AI意义何在?

钛媒体消息&#xff0c;金山办公发布面向教育用户的 WPS 智慧教育平台&#xff0c;面对着金山办公的发力&#xff0c;我们该怎么分析呢&#xff1f; 首先&#xff0c;从市场拓展与用户需求响应角度看&#xff0c;金山办公此次推出WPS智慧教育平台&#xff0c;直接瞄准了教育领…

无人机航电系统之语音通信技术篇

无人机航电系统的语音通信技术是确保无人机与地面控制站、其他无人机或相关人员之间实现高效、稳定语音交互的关键技术&#xff0c;在军事侦察、应急救援、物流运输、航拍测绘等众多领域发挥着至关重要的作用。 一、技术原理 无人机航电系统的语音通信技术主要基于无线通信原理…

element plus 的树形控件,如何根据后台返回的节点key数组,获取节点key对应的node节点

在使用 Element Plus 的 el-tree 组件时&#xff0c;如果后端返回的节点 key 数组中包含了部分选中的父级节点的 key&#xff0c;可能会导致该父级节点下的所有子节点也被默认选中。这是因为 el-tree 的默认行为是&#xff1a;如果一个父节点被选中&#xff0c;那么其所有子节点…

什么是Sentinel

什么是 Sentinel? 在分布式系统中,服务间的依赖关系错综复杂。一个服务的故障,很可能像多米诺骨牌一样,迅速蔓延并导致整个系统崩溃,这就是我们常说的“雪崩效应”。为了避免这种灾难性后果,我们需要一种强大的机制来保护我们的系统,而 Sentinel 正是为此而生。 Senti…

AWS 使用图形化界面创建 EKS 集群(零基础教程)

无需命令行&#xff01;通过 AWS 控制台图形化操作&#xff0c;轻松创建 Kubernetes 集群。 文章目录 文章简介 一、准备工作&#xff1a;登录 AWS 控制台 二、创建 EKS 集群&#xff08;控制面&#xff09; 1. 创建 EKS 集群 2. 设置集群基本信息 3. 配置网络设置 三、添加…

[C#] Winform - 进程间通信(SendMessage篇)

一、发送端 // 窗体&#xff1a;发送端 public partial class SendForm : Form {public SendForm(){InitializeComponent();}// 按钮&#xff1a;发送private void btnSend_Click(object sender, System.EventArgs e){IntPtr hwnd User32Helper.FindWindow(null, "接收端…

锂电池保护板测试仪:守护电池安全的幕后保障

在电动汽车、储能电站和便携式电子设备中&#xff0c;锂电池作为核心动力源&#xff0c;其能量密度与安全性始终是行业关注的焦点。锂电池在过充、过放、短路等异常情况下可能引发起火甚至爆炸&#xff0c;而锂电池保护板正是解决这一问题的关键组件。为确保保护板性能可靠&…

Qemu搭建RISC-V,运行opensbi+u-boot+img

✨1.先下载一个Ubuntu镜像 https://cdimage.ubuntu.com/releases/24.04/release/ 我这里下载的是这个RISC-V的 &#x1f31f;1.2.解压镜像 xz -d ubuntu-24.04.2-preinstalled-server-riscv64.img.xz ✨2.准备RISC-V的QEMU 打开qemu官网https://www.qemu.org/&#xff0c…