飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • UEditor使用文档
  • AngularJS教程
  • ThinkPHP5.0教程

微前端框架 之 qiankun 从入门到源码分析

时间:2022-02-10  作者:liyongning  
微前端框架 之 qiankun 从入门到源码分析 从 single-spa 的缺陷讲起 -> qiankun 是如何从框架层面解决 single-spa 存在的问题 -> qiankun 源码解读,带你全方位刨析 qiankun 框架

封面

简介

从 single-spa 的缺陷讲起 -> qiankun 是如何从框架层面解决 single-spa 存在的问题 -> qiankun 源码解读,带你全方位刨析 qiankun 框架。

介绍

qiankun 是基于 single-spa 做了二次封装的微前端框架,通过解决了 single-spa 的一些弊端和不足,来帮助大家实现更简单、无痛的构建一个生产可用的微前端架构系统。

微前端框架 之 single-spa 从入门到精通 通过从 基本使用 -> 部署 -> 框架源码分析 -> 手写框架,带你全方位刨析 single-spa 框架。

因为 qiankun 是基于 single-spa 做的二次封装,主要解决了 single-spa 的一些痛点和不足,所以最好对 single-spa 有一个全面的了解和认识,明白其原理、了解它的不足和缺陷,然后带着问题和目的去阅读 qiankun 源码,可以达到事半功倍的效果,整个阅读过程的思路也会更加清晰明了。

为什么不是 single-spa

如果你很了解 single-spa 或者阅读过 微前端框架 之 single-spa 从入门到精通 ,你会发现 single-spa 就做了两件事,加载微应用(加载方法还是用户自己提供的)、维护微应用状态(初始化、挂载、卸载)。了解多了会发现 single-spa 虽好,但是却存在一些比较严重的问题

  1. 对微应用的侵入性太强

    single-spa 采用 JS Entry 的方式接入微应用。微应用改造一般分为三步:

    • 微应用路由改造,添加一个特定的前缀
    • 微应用入口改造,挂载点变更和生命周期函数导出
    • 打包工具配置更改

    侵入型强其实说的就是第三点,更改打包工具的配置,使用 single-spa 接入微应用需要将微应用整个打包成一个 JS 文件,发布到静态资源服务器,然后在主应用中配置该 JS 文件的地址告诉 single-spa 去这个地址加载微应用。

    不说其它的,就现在这个改动就存在很大的问题,将整个微应用打包成一个 JS 文件,常见的打包优化基本上都没了,比如:按需加载、首屏资源加载优化、css 独立打包等优化措施。

    项目发布以后出现了 bug ,修复之后需要更新上线,为了清除浏览器缓存带来的影响,一般文件名会带上 chunkcontent,微应用发布之后文件名都会发生变化,这时候还需要更新主应用中微应用配置,然后重新编译主应用然后发布,这套操作简直是不能忍受的,这也是 微前端框架 之 single-spa 从入门到精通 这篇文章中示例项目中微应用发布时的环境配置选择 development 的原因。

  2. 样式隔离问题

    single-spa 没有做这部分的工作。一个大型的系统会有很的微应用组成,怎么保证这些微应用之间的样式互不影响?微应用和主应用之间的样式互不影响?这时只能通过约定命名规范来实现,比如应用样式以自己的应用名称开头,以应用名构造一个独立的命名空间,这个方式新系统还好说,如果是一个已有的系统,这个改造工作量可不小。

  3. JS 隔离

    这部分工作 single-spa 也没有做。 JS 全局对象污染是一个很常见的现象,比如:微应用 A 在全局对象上添加了一个自己特有的属性,window.A,这时候切换到微应用 B,这时候如何保证 window 对象是干净的呢?

  4. 资源预加载

    这部分的工作 single-spa 更没做了,毕竟将微应用整个打包成一个 js 文件。现在有个需求,比如为了提高系统的用户体验,在第一个微应用挂载完成后,需要让浏览器在后台悄悄的加载其它微应用的静态资源,这个怎么实现呢?

  5. 应用间通信

    这部分工作 single-spa 没做,它只在注册微应用时给微应用注入一些状态信息,后续就不管了,没有任何通信的手段,只能用户自己去实现

以上 5 个问题中第 2、3、5 还好说,可以通过一些方式来解决,比如采用命名空间的方式解决样式隔离问题, 通过备份全局对象,每次微应用切换时初始化全局对象的方式来解决 JS 隔离的问题,通信问题可以通过传递一些通信方法,这点依赖了 JS 对象本身的特性(传递的是引用)来实现;但是第一个和第四个就不好解决了,这是 JS Entry 方式带来的问题,要解决这个问题,难度相对就会大很多,工作量也会更大。况且这些通用的脏活累活就不应该由用户(框架使用者)来解决,而是由框架来解决。

为什么是 qiankun

上面说到,通用的脏活累活应该在框架层面去做,qiankun 基于 single-spa 做了二次封装,很好的解决了上面提到的几个问题。

  1. HTML Entry

    qiankun 通过 HTML Entry 的方式来解决 JS Entry 带来的问题,让你接入微应用像使用 iframe 一样简单。

  2. 样式隔离

    qiankun 实现了两种样式隔离

    • 严格的样式隔离模式,为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响
    • 实验性的方式,通过动态改写 css 选择器来实现,可以理解为 css scoped 的方式
  3. **运行时沙箱 **

    qiankun 的运行时沙箱分为 JS 沙箱和 样式沙箱

    JS 沙箱 为每个微应用生成单独的 window proxy 对象,配合 HTML Entry 提供的 JS 脚本执行器 (execScripts) 来实现 JS 隔离;

    样式沙箱 通过重写 DOM 操作方法,来劫持动态样式和 JS 脚本的添加,让样式和脚本添加到正确的地方,即主应用的插入到主应用模版内,微应用的插入到微应用模版,并且为劫持的动态样式做了 scoped css 的处理,为劫持的脚本做了 JS 隔离的处理,更加具体的内容可继续往下阅读或者直接阅读 微前端专栏 中的 qiankun 2.x 运行时沙箱 源码分析

  4. 资源预加载

    qiankun 实现预加载的思路有两种,一种是当主应用执行 start 方法启动 qiankun 以后立即去预加载微应用的静态资源,另一种是在第一个微应用挂载以后预加载其它微应用的静态资源,这个是利用 single-spa 提供的 single-spa:first-mount 事件来实现的

  5. 应用间通信

    qiankun 通过发布订阅模式来实现应用间通信,状态由框架来统一维护,每个应用在初始化时由框架生成一套通信方法,应用通过这些方法来更改全局状态和注册回调函数,全局状态发生改变时触发各个应用注册的回调函数执行,将新旧状态传递到所有应用

说明

文章基于 qiankun 域名 版本做了完整的源码分析,目前网上好像还没有 qiankun 2.x 版本的完整源码分析,简单搜了下好像都是 1.x 版本的

由于框架代码比较多的,博客有字数限制,所以将全部内容拆成了三篇文章,每一篇都可独立阅读:

  • 微前端框架 之 qiankun 从入门到精通

    ,文章由以下三部分组成

    • 为什么不是 single-spa,详细介绍了 single-spa 存在的问题
    • 为什么是 qiankun,详细介绍了 qiankun 是怎么从框架层面解决 single-spa 存在的问题的
    • 源码解读,完整解读了 qiankun 2.x 版本的源码
  • qiankun 2.x 运行时沙箱 源码分析,详细解读了 qiankun 2.x 版本的沙箱实现

  • HTML Entry 源码分析,详细解读了 HTML Entry 的原理以及在 qiankun 中的应用

源码解读

这里没有单独编写示例代码,因为 qiankun 源码中提供了完整的示例项目,这也是 qiankun 做的很好的一个地方,提供完整的示例,避免大家在使用时重复踩坑。

微前端实现和改造时面临的第一个困难就是主应用的设置、微应用的接入,single-spa 官方没有提供一个很好的示例项目,所以大家在使用 single-spa 接入微应用时还是需要踩不少坑的,甚至有些问题需要去阅读源码才能解决

框架目录结构

从 github 克隆项目以后,执行一下命令:

  • 安装 qiankun 框架所需的包

    yarn install
    
  • 安装示例项目的包

    yarn examples:install
    

以上命令执行结束以后:

image-20220202220056482

有料的 域名

  • npm-run-all

    一个 CLI 工具,用于并行或顺序执行多个 npm 脚本

  • father-build

    基于 rollup 的库构建工具,father 更加强大

  • 多项目的目录组织以及 scripts 部分的编写

  • main 和 module 字段

    标识组件库的入口,当两者同时存在时,module 字段的优先级高于 main

示例项目中的主应用

这里需要更改一下示例项目中主应用的 webpack 配置

{
  ...
  devServer: {
    // 从 域名 中可以看出,启动示例项目时,主应用执行了两条命令,其实就是启动了两个主应用,但是却只配置了一个端口,浏览器打开 localhost:7099 和你预想的有一些出入,这时显示的是 loadMicroApp(手动加载微应用) 方式的主应用,基于路由配置的主应用没起来,因为端口被占用了
    // port: \'7099\'
		// 这样配置,手动加载微应用的主应用在 7099 端口,基于路由配置的主应用在 7088 端口
    port: 域名 === \'multiple\' ? \'7099\' : \'7088\'
  }
  ...
}

启动示例项目

yarn examples:start

命令执行结束以后,访问 localhost:7099localhost:7088 两个地址,可以看到如下内容:

image-20220202220258551

image-20220202220401608

到这一步,就证明项目正式跑起来了,所有准备工作就绪

示例项目

官方为我们准备了两种主应用的实现方式,五种微应用的接入示例,覆盖面可以说是比较广了,足以满足大家的普遍需要了

主应用

主应用在 examples/main 目录下,提供了两种实现方式,基于路由配置的 registerMicroApps 和 手动加载微应用的 loadMicroApp。主应用很简单,就是一个从 0 通过 webpack 配置的一个同时支持 react 和 vue 的项目,至于为什么同时支持 react 和 vue,继续往下看

域名

就是一个普通的 webpack 配置,配置了一个开发服务器 devServer、两个 loader (babel-loader、css loader)、一个插件 HtmlWebpackPlugin (告诉 webpack html 模版文件是哪个)

通过 webpack 配置文件的 entry 字段得知入口文件分别为 域名域名

基于路由配置

通用将微应用关联到一些 url 规则的方式,实现当浏览器 url 发生变化时,自动加载相应的微应用的功能

域名
// qiankun api 引入
import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from \'../../es\';
// 全局样式
import \'./域名\';

// 专门针对 angular 微应用引入的一个库
import \'域名\';

/**
 * 主应用可以使用任何技术栈,这里提供了 react 和 vue 两种,可以随意切换
 * 最终都导出了一个 render 函数,负责渲染主应用
 */
// import render from \'./render/ReactRender\';
import render from \'./render/VueRender\';

// 初始化主应用,其实就是渲染主应用
render({ loading: true });

// 定义 loader 函数,切换微应用时由 qiankun 框架负责调用显示一个 loading 状态
const loader = loading => render({ loading });

// 注册微应用
registerMicroApps(
  // 微应用配置列表
  [
    {
      // 应用名称
      name: \'react16\',
      // 应用的入口地址
      entry: \'//localhost:7100\',
      // 应用的挂载点,这个挂载点在上面渲染函数中的模版里面提供的
      container: \'#subapp-viewport\',
      // 微应用切换时调用的方法,显示一个 loading 状态
      loader,
      // 当路由前缀为 /react16 时激活当前应用
      activeRule: \'/react16\',
    },
    {
      name: \'react15\',
      entry: \'//localhost:7102\',
      container: \'#subapp-viewport\',
      loader,
      activeRule: \'/react15\',
    },
    {
      name: \'vue\',
      entry: \'//localhost:7101\',
      container: \'#subapp-viewport\',
      loader,
      activeRule: \'/vue\',
    },
    {
      name: \'angular9\',
      entry: \'//localhost:7103\',
      container: \'#subapp-viewport\',
      loader,
      activeRule: \'/angular9\',
    },
    {
      name: \'purehtml\',
      entry: \'//localhost:7104\',
      container: \'#subapp-viewport\',
      loader,
      activeRule: \'/purehtml\',
    },
  ],
  // 全局生命周期钩子,切换微应用时框架负责调用
  {
    beforeLoad: [
      app => {
        // 这个打印日志的方法可以学习一下,第三个参数会替换掉第一个参数中的 %c%s,并且第三个参数的颜色由第二个参数决定
        域名(\'[LifeCycle] before load %c%s\', \'color: green;\', 域名);
      },
    ],
    beforeMount: [
      app => {
        域名(\'[LifeCycle] before mount %c%s\', \'color: green;\', 域名);
      },
    ],
    afterUnmount: [
      app => {
        域名(\'[LifeCycle] after unmount %c%s\', \'color: green;\', 域名);
      },
    ],
  },
);

// 定义全局状态,并返回两个通信方法
const { onGlobalStateChange, setGlobalState } = initGlobalState({
  user: \'qiankun\',
});

// 监听全局状态的更改,当状态发生改变时执行回调函数
onGlobalStateChange((value, prev) => 域名(\'[onGlobalStateChange - master]:\', value, prev));

// 设置新的全局状态,只能设置一级属性,微应用只能修改已存在的一级属性
setGlobalState({
  ignore: \'master\',
  user: {
    name: \'master\',
  },
});

// 设置默认进入的子应用,当主应用启动以后默认进入指定微应用
setDefaultMountApp(\'/react16\');

// 启动应用
start();

// 当第一个微应用挂载以后,执行回调函数,在这里可以做一些特殊的事情,比如开启一监控或者买点脚本
runAfterFirstMounted(() => {
  域名(\'[MainApp] first app mounted\');
});
域名
/**
 * 导出一个由 vue 实现的渲染函数,渲染了一个模版,模版里面包含一个 loading 状态节点和微应用容器节点
 */
import Vue from \'vue/dist/域名\';

// 返回一个 vue 实例
function vueRender({ loading }) {
  return new Vue({
    template: `
      <div id="subapp-container">
        <h4 v-if="loading" class="subapp-loading">Loading...</h4>
        <div id="subapp-viewport"></div>
      </div>
    `,
    el: \'#subapp-container\',
    data() {
      return {
        loading,
      };
    },
  });
}

// vue 实例
let app = null;

// 渲染函数
export default function render({ loading }) {
  // 单例,如果 vue 实例不存在则实例化主应用,存在则说明主应用已经渲染,需要更新主营应用的 loading 状态
  if (!app) {
    app = vueRender({ loading });
  } else {
    域名ing = loading;
  }
}
域名
/**
 * 同 vue 实现的渲染函数,这里通过 react 实现了一个一样的渲染函数
 */
import React from \'react\';
import ReactDOM from \'react-dom\';

// 渲染主应用
function Render(props) {
  const { loading } = props;

  return (
    <>
      {loading && <h4 className="subapp-loading">Loading...</h4>}
      <div id="subapp-viewport" />
    </>
  );
}

// 将主应用渲染到指定节点下
export default function render({ loading }) {
  const container = 域名lementById(\'subapp-container\');
  域名er(<Render loading={loading} />, container);
}
手动加载微应用

通常这种场景下的微应用是一个不带路由的可独立运行的业务组件,这种使用方式的情况比较少见

域名
/**
 * 调用 loadMicroApp 方法注册了两个微应用
 */
import { loadMicroApp } from \'../../es\';

const app1 = loadMicroApp(
  // 应用配置,名称、入口地址、容器节点
  { name: \'react15\', entry: \'//localhost:7102\', container: \'#react15\' },
  // 可以添加一些其它的配置,比如:沙箱、样式隔离等
  {
    sandbox: {
      // strictStyleIsolation: true,
    },
  },
);

const app2 = loadMicroApp(
  { name: \'vue\', entry: \'//localhost:7101\', container: \'#vue\' },
  {
    sandbox: {
      // strictStyleIsolation: true,
    },
  },
);

vue

vue 微应用在 examples/vue 目录下,就是一个通过 vue-cli 创建的 vue demo 应用,然后对 域名域名 做了一些更改

域名

一个普通的 webpack 配置,需要注意的地方就三点

{
  ...
  // publicPath 没在这里设置,是通过 webpack 提供的全局变量 __webpack_public_path__ 来即时设置的,域名/guides/public-path/
  devServer: {
    ...
    // 设置跨域,因为主应用需要通过 fetch 去获取微应用引入的静态资源的,所以必须要求这些静态资源支持跨域
    headers: {
      \'Access-Control-Allow-Origin\': \'*\',
    },
  },
  output: {
    // 把子应用打包成 umd 库格式
    library: `${name}-[name]`,	// 库名称,唯一
    libraryTarget: \'umd\',
    jsonpFunction: `webpackJsonp_${name}`,
  }
  ...
}
域名
// 动态设置 __webpack_public_path__
import \'./public-path\';
import ElementUI from \'element-ui\';
import \'element-ui/lib/theme-chalk/域名\';
import Vue from \'vue\';
import VueRouter from \'vue-router\';
import App from \'./域名\';
// 路由配置
import routes from \'./router\';
import store from \'./store\';

域名uctionTip = false;

域名(ElementUI);

let router = null;
let instance = null;

// 应用渲染函数
function render(props = {}) {
  const { container } = props;
  // 实例化 router,根据应用运行环境设置路由前缀
  router = new VueRouter({
    // 作为微应用运行,则设置 /vue 为前缀,否则设置 /
    base: 域名WERED_BY_QIANKUN__ ? \'/vue\' : \'/\',
    mode: \'history\',
    routes,
  });

  // 实例化 vue 实例
  instance = new Vue({
    router,
    store,
    render: h => h(App),
  }).$mount(container ? 域名ySelector(\'#app\') : \'#app\');
}

// 支持应用独立运行
if (!域名WERED_BY_QIANKUN__) {
  render();
}

/**
 * 从 props 中获取通信方法,监听全局状态的更改和设置全局状态,只能操作一级属性
 * @param {*} props 
 */
function storeTest(props) {
  域名obalStateChange &&
    域名obalStateChange(
      (value, prev) => 域名(`[onGlobalStateChange - ${域名}]:`, value, prev),
      true,
    );
  域名lobalState &&
    域名lobalState({
      ignore: 域名,
      user: {
        name: 域名,
      },
    });
}

/**
 * 导出的三个生命周期函数
 */
// 初始化
export async function bootstrap() {
  域名(\'[vue] vue app bootstraped\');
}

// 挂载微应用
export async function mount(props) {
  域名(\'[vue] props from main framework\', props);
  storeTest(props);
  render(props);
}

// 卸载、销毁微应用
export async function unmount() {
  instance.$destroy();
  instance.$域名rHTML = \'\';
  instance = null;
  router = null;
}
public-域名
/**
 * 在入口文件中使用 ES6 模块导入,则在导入后对 __webpack_public_path__ 进行赋值。
 * 在这种情况下,必须将公共路径(public path)赋值移至专属模块,然后将其在最前面导入
 */

// qiankun 设置的全局变量,表示应用作为微应用在运行
if (域名WERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = 域名JECTED_PUBLIC_PATH_BY_QIANKUN__;
}

jQuery

这是一个使用了 jQuery 的项目,在 examples/purehtml 目录下,展示了如何接入使用 jQuery 开发的应用

域名

为了达到演示效果,使用 http-server 在起了一个本地服务器,并且支持跨域

{
  ...
  "scripts": {
    "start": "cross-env PORT=7104 http-server . --cors",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...
}
域名
// 渲染函数
const render = $ => {
  $(\'#purehtml-container\').html(\'Hello, render with jQuery\');
  return 域名lve();
};

// 在全局对象上导出三个生命周期函数
(global => {
  global[\'purehtml\'] = {
    bootstrap: () => {
      域名(\'purehtml bootstrap\');
      return 域名lve();
    },
    mount: () => {
      域名(\'purehtml mount\');
      // 调用渲染函数
      return render($);
    },
    unmount: () => {
      域名(\'purehtml unmount\');
      return 域名lve();
    },
  };
})(window);
域名
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Purehtml Example</title>
  <script src="//域名/jquery/3.4.1/域名">
  </script>
</head>
<body>
  <div style="display: flex; justify-content: center; align-items: center; height: 200px;">
    Purehtml Example
  </div>
  <div id="purehtml-container" style="text-align:center"></div>
  <!-- 引入 域名,相当于 vue 项目的 publicPath 配置 -->
  <script src="//localhost:7104/域名" entry></script>
</body>
</html>

angular 9、react 15、react 16

这三个实例项目就不一一分析了,和 vue 项目类似,都是配置打包工具将微应用打包成一个 umd 格式,然后配置应用入口文件 和 路由前缀

小结

好了,读到这里,系统改造(可以开始干活了)基本上就已经可以顺利进行了,从主应用的开发到微应用接入,应该是不会有什么问题了。

当然如果你想继续深入了解,比如:

  • 上面用到那些 API 的原理是什么?
  • qiankun 是怎么解决我们之前提到的 single-spa 未解决的问题的?
  • ...

接下来就带着我们的疑问和目的去全面深入的了解 qiankun 框架的内部实现

框架源码

整个框架的源码目录是 src,入口文件是 src/域名

入口 src/域名

/**
 * 在示例或者官网提到的所有 API 都在这里统一导出
 */
// 最关键的三个,手动加载微应用、基于路由配置、启动 qiankun
export { loadMicroApp, registerMicroApps, start } from \'./apis\';
// 全局状态
export { initGlobalState } from \'./globalState\';
// 全局的未捕获异常处理器
export * from \'./errorHandler\';
// setDefaultMountApp 设置主应用启动后默认进入哪个微应用、runAfterFirstMounted 设置当第一个微应用挂载以后需要调用的一些方法
export * from \'./effects\';
// 类型定义
export * from \'./interfaces\';
// prefetch
export { prefetchImmediately as prefetchApps } from \'./prefetch\';

registerMicroApps

/**
 * 注册微应用,基于路由配置
 * @param apps = [
 *  {
 *    name: \'react16\',
 *    entry: \'//localhost:7100\',
 *    container: \'#subapp-viewport\',
 *    loader,
 *    activeRule: \'/react16\'
 *  },
 *  ...
 * ]
 * @param lifeCycles = { ...各个生命周期方法对象 }
 */
export function registerMicroApps<T extends object = {}>(
  apps: Array<RegistrableApp<T>>,
  lifeCycles?: FrameworkLifeCycles<T>,
) {
  // 防止微应用重复注册,得到所有没有被注册的微应用列表
  const unregisteredApps = 域名er(app => !域名(registeredApp => 域名 === 域名));

  // 所有的微应用 = 已注册 + 未注册的(将要被注册的)
  microApps = [...microApps, ...unregisteredApps];

  // 注册每一个微应用
  域名ach(app => {
    // 注册时提供的微应用基本信息
    const { name, activeRule, loader = noop, props, ...appConfig } = app;

    // 调用 single-spa 的 registerApplication 方法注册微应用
    registerApplication({
      // 微应用名称
      name,
      // 微应用的加载方法,Promise<生命周期方法组成的对象>
      app: async () => {
        // 加载微应用时主应用显示 loading 状态
        loader(true);
        // 这句可以忽略,目的是在 single-spa 执行这个加载方法时让出线程,让其它微应用的加载方法都开始执行
        await 域名ise;

        // 核心、精髓、难点所在,负责加载微应用,然后一大堆处理,返回 bootstrap、mount、unmount、update 这个几个生命周期
        const { mount, ...otherMicroAppConfigs } = await loadApp(
          // 微应用的配置信息
          { name, props, ...appConfig },
          // start 方法执行时设置的配置对象
          frameworkConfiguration,
          // 注册微应用时提供的全局生命周期对象
          lifeCycles,
        );

        return {
          mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
          ...otherMicroAppConfigs,
        };
      },
      // 微应用的激活条件
      activeWhen: activeRule,
      // 传递给微应用的 props
      customProps: props,
    });
  });
}

start

/**
 * 启动 qiankun
 * @param opts start 方法的配置对象 
 */
export function start(opts: FrameworkConfiguration = {}) {
  // qiankun 框架默认开启预加载、单例模式、样式沙箱
  frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts };
  // 从这里可以看出 start 方法支持的参数不止官网文档说的那些,比如 urlRerouteOnly,这个是 single-spa 的 start 方法支持的
  const { prefetch, sandbox, singular, urlRerouteOnly, ...importEntryOpts } = frameworkConfiguration;

  // 预加载
  if (prefetch) {
    // 执行预加载策略,参数分别为微应用列表、预加载策略、{ fetch、getPublicPath、getTemplate }
    doPrefetchStrategy(microApps, prefetch, importEntryOpts);
  }

  // 样式沙箱
  if (sandbox) {
    if (!域名y) {
      域名(\'[qiankun] Miss 域名y, proxySandbox will degenerate into snapshotSandbox\');
      // 快照沙箱不支持非 singular 模式
      if (!singular) {
        域名r(\'[qiankun] singular is forced to be true when sandbox enable but proxySandbox unavailable\');
        // 如果开启沙箱,会强制使用单例模式
        域名ular = true;
      }
    }
  }

  // 执行 single-spa 的 start 方法,启动 single-spa
  startSingleSpa({ urlRerouteOnly });

  域名lve();
}

预加载 - doPrefetchStrategy

/**
 * 执行预加载策略,qiankun 支持四种
 * @param apps 所有的微应用 
 * @param prefetchStrategy 预加载策略,四种 =》 
 *  1、true,第一个微应用挂载以后加载其它微应用的静态资源,利用的是 single-spa 提供的 single-spa:first-mount 事件来实现的
 *  2、string[],微应用名称数组,在第一个微应用挂载以后加载指定的微应用的静态资源
 *  3、all,主应用执行 start 以后就直接开始预加载所有微应用的静态资源
 *  4、自定义函数,返回两个微应用组成的数组,一个是关键微应用组成的数组,需要马上就执行预加载的微应用,一个是普通的微应用组成的数组,在第一个微应用挂载以后预加载这些微应用的静态资源
 * @param importEntryOpts = { fetch, getPublicPath, getTemplate }
 */
export function doPrefetchStrategy(
  apps: AppMetadata[],
  prefetchStrategy: PrefetchStrategy,
  importEntryOpts?: ImportEntryOpts,
) {
  // 定义函数,函数接收一个微应用名称组成的数组,然后从微应用列表中返回这些名称所对应的微应用,最后得到一个数组[{name, entry}, ...]
  const appsName2Apps = (names: string[]): AppMetadata[] => 域名er(app => 域名udes(域名));

  if (域名ray(prefetchStrategy)) {
    // 说明加载策略是一个数组,当第一个微应用挂载之后开始加载数组内由用户指定的微应用资源,数组内的每一项表示一个微应用的名称
    prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts);
  } else if (isFunction(prefetchStrategy)) {
    // 加载策略是一个自定义的函数,可完全自定义应用资源的加载时机(首屏应用、次屏应用)
    (async () => {
      // critical rendering apps would be prefetch as earlier as possible,关键的应用程序应该尽可能早的预取
      // 执行加载策略函数,函数会返回两个数组,一个关键的应用程序数组,会立即执行预加载动作,另一个是在第一个微应用挂载以后执行微应用静态资源的预加载
      const { criticalAppNames = [], minorAppsName = [] } = await prefetchStrategy(apps);
      // 立即预加载这些关键微应用程序的静态资源
      prefetchImmediately(appsName2Apps(criticalAppNames), importEntryOpts);
      // 当第一个微应用挂载以后预加载这些微应用的静态资源
      prefetchAfterFirstMounted(appsName2Apps(minorAppsName), importEntryOpts);
    })();
  } else {
    // 加载策略是默认的 true 或者 all
    switch (prefetchStrategy) {
      case true:
        // 第一个微应用挂载之后开始加载其它微应用的静态资源
        prefetchAfterFirstMounted(apps, importEntryOpts);
        break;

      case \'all\':
        // 在主应用执行 start 以后就开始加载所有微应用的静态资源
        prefetchImmediately(apps, importEntryOpts);
        break;

      default:
        break;
    }
  }
}

// 判断是否为弱网环境
const isSlowNetwork = 域名ection
  ? 域名Data ||
    (域名 !== \'wifi\' &&
      域名 !== \'ethernet\' &&
      /(2|3)g/.test(域名ctiveType))
  : false;

/**
 * prefetch assets, do nothing while in mobile network
 * 预加载静态资源,在移动网络下什么都不做
 * @param entry
 * @param opts
 */
function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
  // 弱网环境下不执行预加载
  if (!域名ne || isSlowNetwork) {
    // Don\'t prefetch if in a slow network or offline
    return;
  }

  // 通过时间切片的方式去加载静态资源,在浏览器空闲时去执行回调函数,避免浏览器卡顿
  requestIdleCallback(async () => {
    // 得到加载静态资源的函数
    const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
    // 样式
    requestIdleCallback(getExternalStyleSheets);
    // js 脚本
    requestIdleCallback(getExternalScripts);
  });
}

/**
 * 在第一个微应用挂载之后开始加载 apps 中指定的微应用的静态资源
 * 通过监听 single-spa 提供的 single-spa:first-mount 事件来实现,该事件在第一个微应用挂载以后会被触发
 * @param apps 需要被预加载静态资源的微应用列表,[{ name, entry }, ...]
 * @param opts = { fetch , getPublicPath, getTemplate }
 */
function prefetchAfterFirstMounted(apps: AppMetadata[], opts?: ImportEntryOpts): void {
  // 监听 single-spa:first-mount 事件
  域名ventListener(\'single-spa:first-mount\', function listener() {
    // 已挂载的微应用
    const mountedApps = getMountedApps();
    // 从预加载的微应用列表中过滤出未挂载的微应用
    const notMountedApps = 域名er(app => 域名xOf(域名) === -1);

    // 开发环境打印日志,已挂载的微应用和未挂载的微应用分别有哪些
    if (域名_ENV === \'development\') {
      域名(`[qiankun] prefetch starting after ${mountedApps} mounted...`, notMountedApps);
    }

    // 循环加载微应用的静态资源
    域名ach(({ entry }) => prefetch(entry, opts));

    // 移除 single-spa:first-mount 事件
    域名veEventListener(\'single-spa:first-mount\', listener);
  });
}

/**
 * 在执行 start 启动 qiankun 之后立即预加载所有微应用的静态资源
 * @param apps 需要被预加载静态资源的微应用列表,[{ name, entry }, ...]
 * @param opts = { fetch , getPublicPath, getTemplate }
 */
export function prefetchImmediately(apps: AppMetadata[], opts?: ImportEntryOpts): void {
  // 开发环境打印日志
  if (域名_ENV === \'development\') {
    域名(\'[qiankun] prefetch starting for apps...\', apps);
  }

  // 加载所有微应用的静态资源
  域名ach(({ entry }) => prefetch(entry, opts));
}

应用间通信 initGlobalState

// 触发全局监听,执行所有应用注册的回调函数
function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {
  // 循环遍历,执行所有应用注册的回调函数
  域名(deps).forEach((id: string) => {
    if (deps[id] instanceof Function) {
      deps[id](cloneDeep(state), cloneDeep(prevState));
    }
  });
}

/**
 * 定义全局状态,并返回通信方法,一般由主应用调用,微应用通过 props 获取通信方法。 
 * @param state 全局状态,{ key: value }
 */
export function initGlobalState(state: Record<string, any> = {}) {
  if (state === globalState) {
    域名(\'[qiankun] state has not changed!\');
  } else {
    // 方法有可能被重复调用,将已有的全局状态克隆一份,为空则是第一次调用 initGlobalState 方法,不为空则非第一次次调用
    const prevGlobalState = cloneDeep(globalState);
    // 将传递的状态克隆一份赋值为 globalState
    globalState = cloneDeep(state);
    // 触发全局监听,当然在这个位置调用,正常情况下没啥反应,因为现在还没有应用注册回调函数
    emitGlobal(globalState, prevGlobalState);
  }
  // 返回通信方法,参数表示应用 id,true 表示自己是主应用调用
  return getMicroAppStateActions(`global-${+new Date()}`, true);
}

/**
 * 返回通信方法 
 * @param id 应用 id
 * @param isMaster 表明调用的应用是否为主应用,在主应用初始化全局状态时,initGlobalState 内部调用该方法时会传递 true,其它都为 false
 */
export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {
  return {
    /**
     * 全局依赖监听,为指定应用(id = 应用id)注册回调函数
     * 依赖数据结构为:
     * {
     *   {id}: callback
     * }
     *
     * @param callback 注册的回调函数
     * @param fireImmediately 是否立即执行回调
     */
    onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
      // 回调函数必须为 function
      if (!(callback instanceof Function)) {
        域名r(\'[qiankun] callback must be function!\');
        return;
      }
      // 如果回调函数已经存在,重复注册时给出覆盖提示信息
      if (deps[id]) {
        域名(`[qiankun] \'${id}\' global listener already exists before this, new listener will overwrite it.`);
      }
      // id 为一个应用 id,一个应用对应一个回调
      deps[id] = callback;
      // 克隆全局状态
      const cloneState = cloneDeep(globalState);
      // 如果需要,立即出发回调执行
      if (fireImmediately) {
        callback(cloneState, cloneState);
      }
    },

    /**
     * setGlobalState 更新 store 数据
     *
     * 1. 对新输入 state 的第一层属性做校验,如果是主应用则可以添加新的一级属性进来,也可以更新已存在的一级属性,
     *    如果是微应用,则只能更新已存在的一级属性,不可以新增一级属性
     * 2. 触发全局监听,执行所有应用注册的回调函数,以达到应用间通信的目的
     *
     * @param state 新的全局状态
     */
    setGlobalState(state: Record<string, any> = {}) {
      if (state === globalState) {
        域名(\'[qiankun] state has not changed!\');
        return false;
      }

      // 记录旧的全局状态中被改变的 key
      const changeKeys: string[] = [];
      // 旧的全局状态
      const prevGlobalState = cloneDeep(globalState);
      globalState = cloneDeep(
        // 循环遍历新状态中的所有 key
        域名(state).reduce((_globalState, changeKey) => {
          if (isMaster || 域名wnProperty(changeKey)) {
            // 主应用 或者 旧的全局状态存在该 key 时才进来,说明只有主应用才可以新增属性,微应用只可以更新已存在的属性值,且不论主应用微应用只能更新一级属性
            // 记录被改变的key
            域名(changeKey);
            // 更新旧状态中对应的 key value
            return 域名gn(_globalState, { [changeKey]: state[changeKey] });
          }
          域名(`[qiankun] \'${changeKey}\' not declared when init state!`);
          return _globalState;
        }, globalState),
      );
      if (域名th === 0) {
        域名(\'[qiankun] state has not changed!\');
        return false;
      }
      // 触发全局监听
      emitGlobal(globalState, prevGlobalState);
      return true;
    },

    // 注销该应用下的依赖
    offGlobalStateChange() {
      delete deps[id];
      return true;
    },
  };
}

全局未捕获异常处理器

/**
 * 整个文件的逻辑一眼明了,整个框架提供了两种全局异常捕获,一个是 single-spa 提供的,另一个是 qiankun 自己的,你只需提供相应的回调函数即可
 */

// single-spa 的异常捕获
export { addErrorHandler, removeErrorHandler } from \'single-spa\';

// qiankun 的异常捕获
// 监听了 error 和 unhandlerejection 事件
export function addGlobalUncaughtErrorHandler(errorHandler: OnErrorEventHandlerNonNull): void {
  域名ventListener(\'error\', errorHandler);
  域名ventListener(\'unhandledrejection\', errorHandler);
}

// 移除 error 和 unhandlerejection 事件监听
export function removeGlobalUncaughtErrorHandler(errorHandler: (...args: any[]) => any) {
  域名veEventListener(\'error\', errorHandler);
  域名veEventListener(\'unhandledrejection\', errorHandler);
}

setDefaultMountApp

/**
 * 设置主应用启动后默认进入的微应用,其实是规定了第一个微应用挂载完成后决定默认进入哪个微应用
 * 利用的是 single-spa 的 single-spa:no-app-change 事件,该事件在所有微应用状态改变结束后(即发生路由切换且新的微应用已经被挂载完成)触发
 * @param defaultAppLink 微应用的链接,比如 /react16
 */
export function setDefaultMountApp(defaultAppLink: string) {
  // 当事件触发时就说明微应用已经挂载完成,但这里只监听了一次,因为事件被触发以后就移除了监听,所以说是主应用启动后默认进入的微应用,且只执行了一次的原因
  域名ventListener(\'single-spa:no-app-change\', function listener() {
    // 说明微应用已经挂载完成,获取挂载的微应用列表,再次确认确实有微应用挂载了,其实这个确认没啥必要
    const mountedApps = getMountedApps();
    if (!域名th) {
      // 这个是 single-spa 提供的一个 api,通过触发 域名 或者 pushState 更改路由,切换微应用
      navigateToUrl(defaultAppLink);
    }

    // 触发一次以后,就移除该事件的监听函数,后续的路由切换(事件触发)时就不再响应
    域名veEventListener(\'single-spa:no-app-change\', listener);
  });
}

// 这个 api 和 setDefaultMountApp 作用一致,官网也提到,兼容老版本的一个 api
export function runDefaultMountEffects(defaultAppLink: string) {
  域名(
    \'[qiankun] runDefaultMountEffects will be removed in next version, please use setDefaultMountApp instead\',
  );
  setDefaultMountApp(defaultAppLink);
}

runAfterFirstMounted

/**
 * 第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本
 * 同样利用的 single-spa 的 single-spa:first-mount 事件,当第一个微应用挂载以后会触发
 * @param effect 回调函数,当第一个微应用挂载以后要做的事情
 */
export function runAfterFirstMounted(effect: () => void) {
  // can not use addEventListener once option for ie support
  域名ventListener(\'single-spa:first-mount\', function listener() {
    if (域名_ENV === \'development\') {
      域名End(firstMountLogLabel);
    }

    effect();

    // 这里不移除也没事,因为这个事件后续不会再被触发了
    域名veEventListener(\'single-spa:first-mount\', listener);
  });
}

手动加载微应用 loadMicroApp

/**
 * 手动加载一个微应用,是通过 single-spa 的 mountRootParcel api 实现的,返回微应用实例
 * @param app = { name, entry, container, props }
 * @param configuration 配置对象
 * @param lifeCycles 还支持一个全局生命周期配置对象,这个参数官方文档没提到
 */
export function loadMicroApp<T extends object = {}>(
  app: LoadableApp<T>,
  configuration?: FrameworkConfiguration,
  lifeCycles?: FrameworkLifeCycles<T>,
): MicroApp {
  const { props } = app;
  // single-spa 的 mountRootParcel api
  return mountRootParcel(() => loadApp(app, configuration ?? frameworkConfiguration, lifeCycles), {
    domElement: 域名teElement(\'div\'),
    ...props,
  });
}

qiankun 的核心 loadApp

接下来介绍 loadApp 方法,个人认为 qiankun 的核心代码可以说大部分都在这里,当然这也是整个框架的精髓和难点所在

/**
 * 完成了以下几件事:
 *  1、通过 HTML Entry 的方式远程加载微应用,得到微应用的 html 模版(首屏内容)、JS 脚本执行器、静态经资源路径
 *  2、样式隔离,shadow DOM 或者 scoped css 两种方式
 *  3、渲染微应用
 *  4、运行时沙箱,JS 沙箱、样式沙箱
 *  5、合并沙箱传递出来的 生命周期方法、用户传递的生命周期方法、框架内置的生命周期方法,将这些生命周期方法统一整理,导出一个生命周期对象,
 * 供 single-spa 的 registerApplication 方法使用,这个对象就相当于使用 single-spa 时你的微应用导出的那些生命周期方法,只不过 qiankun
 * 额外填了一些生命周期方法,做了一些事情
 *  6、给微应用注册通信方法并返回通信方法,然后会将通信方法通过 props 注入到微应用
 * @param app 微应用配置对象
 * @param configuration start 方法执行时设置的配置对象 
 * @param lifeCycles 注册微应用时提供的全局生命周期对象
 */
export async function loadApp<T extends object>(
  app: LoadableApp<T>,
  configuration: FrameworkConfiguration = {},
  lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObject> {
  // 微应用的入口和名称
  const { entry, name: appName } = app;
  // 实例 id
  const appInstanceId = `${appName}_${+new Date()}_${域名r(域名om() * 1000)}`;

  // 下面这个不用管,就是生成一个标记名称,然后使用该名称在浏览器性能缓冲器中设置一个时间戳,可以用来度量程序的执行时间,域名、域名ure
  const markName = `[qiankun] App ${appInstanceId} Loading`;
  if (域名_ENV === \'development\') {
    performanceMark(markName);
  }

  // 配置信息
  const { singular = false, sandbox = true, excludeAssetFilter, ...importEntryOpts } = configuration;

  /**
   * 获取微应用的入口 html 内容和脚本执行器
   * template 是 link 替换为 style 后的 template
   * execScript 是 让 JS 代码(scripts)在指定 上下文 中运行
   * assetPublicPath 是静态资源地址
   */
  const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);

  // single-spa 的限制,加载、初始化和卸载不能同时进行,必须等卸载完成以后才可以进行加载,这个 promise 会在微应用卸载完成后被 resolve,在后面可以看到
  if (await validateSingularMode(singular, app)) {
    await (prevAppUnmountedDeferred && 域名ise);
  }

  // --------------- 样式隔离 ---------------
  // 是否严格样式隔离
  const strictStyleIsolation = typeof sandbox === \'object\' && !!域名ctStyleIsolation;
  // 实验性的样式隔离,后面就叫 scoped css,和严格样式隔离不能同时开启,如果开启了严格样式隔离,则 scoped css 就为 false,强制关闭
  const enableScopedCSS = isEnableScopedCSS(configuration);

  // 用一个容器元素包裹微应用入口 html 模版, appContent = `<div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div>`
  const appContent = getDefaultTplWrapper(appInstanceId, appName)(template);
  // 将 appContent 有字符串模版转换为 html dom 元素,如果需要开启样式严格隔离,则将 appContent 的子元素即微应用入口模版用 shadow dom 包裹起来,以达到样式严格隔离的目的
  let element: HTMLElement | null = createElement(appContent, strictStyleIsolation);
  // 通过 scoped css 的方式隔离样式,从这里也就能看出官方为什么说:
  // 在目前的阶段,该功能还不支持动态的、使用 <link />标签来插入外联的样式,但考虑在未来支持这部分场景
  // 在现阶段只处理 style 这种内联标签的情况 
  if (element && isEnableScopedCSS(configuration)) {
    const styleNodes = 域名ySelectorAll(\'style\') || [];
    forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
      域名ess(element!, stylesheetElement, appName);
    });
  }

  // --------------- 渲染微应用 ---------------
  // 主应用装载微应用的容器节点
  const container = \'container\' in app ? 域名ainer : undefined;
  // 这个是 1.x 版本遗留下来的实现,如果提供了 render 函数,当微应用需要被激活时就执行 render 函数渲染微应用,新版本用的 container,弃了 render
  // 而且 legacyRender 和 strictStyleIsolation、scoped css 不兼容
  const legacyRender = \'render\' in app ? 域名er : undefined;

  // 返回一个 render 函数,这个 render 函数要不使用用户传递的 render 函数,要不将 element 插入到 container
  const render = getRender(appName, appContent, container, legacyRender);

  // 渲染微应用到容器节点,并显示 loading 状态
  render({ element, loading: true }, \'loading\');

  // 得到一个 getter 函数,通过该函数可以获取 <div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div>
  const containerGetter = getAppWrapperGetter(
    appName,
    appInstanceId,
    !!legacyRender,
    strictStyleIsolation,
    enableScopedCSS,
    () => element,
  );

  // --------------- 运行时沙箱 ---------------
  // 保证每一个微应用运行在一个干净的环境中(JS 执行上下文独立、应用间不会发生样式污染)
  let global = window;
  let mountSandbox = () => 域名lve();
  let unmountSandbox = () => 域名lve();
  if (sandbox) {
    /**
     * 生成运行时沙箱,这个沙箱其实由两部分组成 => JS 沙箱(执行上下文)、样式沙箱
     * 
     * 沙箱返回 window 的代理对象 proxy 和 mount、unmount 两个方法
     * unmount 方法会让微应用失活,恢复被增强的原生方法,并记录一堆 rebuild 函数,这个函数是微应用卸载时希望自己被重新挂载时要做的一些事情,比如动态样式表重建(卸载时会缓存)
     * mount 方法会执行一些一些 patch 动作,恢复原生方法的增强功能,并执行 rebuild 函数,将微应用恢复到卸载时的状态,当然从初始化状态进入挂载状态就没有恢复一说了
     */
    const sandboxInstance = createSandbox(
      appName,
      containerGetter,
      Boolean(singular),
      enableScopedCSS,
      excludeAssetFilter,
    );
    // 用沙箱的代理对象作为接下来使用的全局对象
    global = 域名y as typeof window;
    mountSandbox = 域名t;
    unmountSandbox = 域名unt;
  }

  // 合并用户传递的生命周期对象和 qiankun 框架内置的生命周期对象
  const { beforeUnmount = [], afterUnmount = [], afterMount = [], beforeMount = [], beforeLoad = [] } = mergeWith(
    {},
    // 返回内置生命周期对象,域名WERED_BY_QIANKUN__ 和 域名JECTED_PUBLIC_PATH_BY_QIANKUN__ 的设置就是在内置的生命周期对象中设置的
    getAddOns(global, assetPublicPath),
    lifeCycles,
    (v1, v2) => concat(v1 ?? [], v2 ?? []),
  );

  await execHooksChain(toArray(beforeLoad), app, global);

  // get the lifecycle hooks from module exports,获取微应用暴露出来的生命周期函数
  const scriptExports: any = await execScripts(global, !singular);
  const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(scriptExports, appName, global);

  // 给微应用注册通信方法并返回通信方法,然后会将通信方法通过 props 注入到微应用
  const {
    onGlobalStateChange,
    setGlobalState,
    offGlobalStateChange,
  }: Record<string, Function> = getMicroAppStateActions(appInstanceId);

  const parcelConfig: ParcelConfigObject = {
    name: appInstanceId,
    bootstrap,
    // 挂载阶段需要执行的一系列方法
    mount: [
      // 性能度量,不用管
      async () => {
        if (域名_ENV === \'development\') {
          const marks = 域名ntriesByName(markName, \'mark\');
          // mark length is zero means the app is remounting
          if (!域名th) {
            performanceMark(markName);
          }
        }
      },
      // 单例模式需要等微应用卸载完成以后才能执行挂载任务,promise 会在微应用卸载完以后 resolve
      async () => {
        if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
          return 域名ise;
        }

        return undefined;
      },
      // 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
      async () => {
        // element would be destroyed after unmounted, we need to recreate it if it not exist
        // unmount 阶段会置空,这里重新生成
        element = element || createElement(appContent, strictStyleIsolation);
        // 渲染微应用到容器节点,并显示 loading 状态
        render({ element, loading: true }, \'mounting\');
      },
      // 运行时沙箱导出的 mount
      mountSandbox,
      // exec the chain after rendering to keep the behavior with beforeLoad
      async () => execHooksChain(toArray(beforeMount), app, global),
      // 向微应用的 mount 生命周期函数传递参数,比如微应用中使用的 域名obalStateChange 方法
      async props => mount({ ...props, container: containerGetter(), setGlobalState, onGlobalStateChange }),
      // 应用 mount 完成后结束 loading
      async () => render({ element, loading: false }, \'mounted\'),
      async () => execHooksChain(toArray(afterMount), app, global),
      // initialize the unmount defer after app mounted and resolve the defer after it unmounted
      // 微应用挂载完成以后初始化这个 promise,并且在微应用卸载以后 resolve 这个 promise
      async () => {
        if (await validateSingularMode(singular, app)) {
          prevAppUnmountedDeferred = new Deferred<void>();
        }
      },
      // 性能度量,不用管
      async () => {
        if (域名_ENV === \'development\') {
          const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`;
          performanceMeasure(measureName, markName);
        }
      },
    ],
    // 卸载微应用
    unmount: [
      async () => execHooksChain(toArray(beforeUnmount), app, global),
      // 执行微应用的 unmount 生命周期函数
      async props => unmount({ ...props, container: containerGetter() }),
      // 沙箱导出的 unmount 方法
      unmountSandbox,
      async () => execHooksChain(toArray(afterUnmount), app, global),
      // 显示 loading 状态、移除微应用的状态监听、置空 element
      async () => {
        render({ element: null, loading: false }, \'unmounted\');
        offGlobalStateChange(appInstanceId);
        // for gc
        element = null;
      },
      // 微应用卸载以后 resolve 这个 promise,框架就可以进行后续的工作,比如加载或者挂载其它微应用
      async () => {
        if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
          域名lve();
        }
      },
    ],
  };

  // 微应用有可能定义 update 方法
  if (typeof update === \'function\') {
    域名te = update;
  }

  return parcelConfig;
}

样式隔离

qiankun 的样式隔离有两种方式,一种是严格样式隔离,通过 shadow dom 来实现,另一种是实验性的样式隔离,就是 scoped css,两种方式不可共存

严格样式隔离

qiankun 中的严格样式隔离,就是在这个 createElement 方法中做的,通过 shadow dom 来实现, shadow dom 是浏览器原生提供的一种能力,在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构。以一个有着默认播放控制按钮的 <video> 元素为例,实际上,在它的 Shadow DOM 中,包含来一系列的按钮和其他控制器。Shadow DOM 标准允许你为你自己的元素(custom element)维护一组 Shadow DOM。具体内容可查看 shadow DOM

/**
 * 做了两件事
 *  1、将 appContent 由字符串模版转换成 html dom 元素
 *  2、如果需要开启严格样式隔离,则将 appContent 的子元素即微应用的入口模版用 shadow dom 包裹起来,达到样式严格隔离的目的
 * @param appContent = `<div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div>`
 * @param strictStyleIsolation 是否开启严格样式隔离
 */
function createElement(appContent: string, strictStyleIsolation: boolean): HTMLElement {
  // 创建一个 div 元素
  const containerElement = 域名teElement(\'div\');
  // 将字符串模版 appContent 设置为 div 的子与阿苏
  域名rHTML = appContent;
  // appContent always wrapped with a singular div,appContent 由模版字符串变成了 DOM 元素
  const appElement = 域名tChild as HTMLElement;
  // 如果开启了严格的样式隔离,则将 appContent 的子元素(微应用的入口模版)用 shadow dom 包裹,以达到微应用之间样式严格隔离的目的
  if (strictStyleIsola                
标签:编程
湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。