Микроинтерфейсный фреймворк qiankun от входа до анализа исходного кода

Архитектура внешний интерфейс
Микроинтерфейсный фреймворк qiankun от входа до анализа исходного кода

Статьи были включены вgithub, Добро пожаловать в Watch and Star.

Введение

Начиная с дефектов single-spa -> как qiankun решает проблемы single-spa на уровне фреймворка -> интерпретация исходного кода qiankun, вы можете всесторонне проанализировать фреймворк qiankun.

вводить

qiankun — это фреймворк для микроинтерфейса с вторичной инкапсуляцией на основе single-spa.Решая некоторые недостатки и недостатки single-spa, он помогает каждому проще и безболезненнее построить готовую к производству систему архитектуры микроинтерфейса.

Односпальный микроинтерфейсный фреймворк от начального уровня до мастерстваПутем базового использования -> развертывания -> анализа исходного кода фреймворка -> написанного вручную фреймворка вам потребуется всесторонне проанализировать фреймворк single-spa.

Поскольку qiankun является вторичным пакетом, основанным на одинарном спа, он в основном устраняет некоторые болевые точки и недостатки одиночного спа, поэтому лучше иметь полное представление и понимание одиночного спа, понимать его принципы, а также понимать его недостатки и дефекты, а затем прочитайте исходный код qiankun с вопросами и целями, которые могут достичь удвоенного результата с половиной усилий, и мышление всего процесса чтения будет более ясным.

Почему не одноместный спа

Если вы хорошо знаете single spa или читалиОдноспальный микроинтерфейсный фреймворк от начального уровня до мастерства, вы обнаружите, что single-spa выполняет две функции: загружает микроприложение (метод загрузки предоставляется пользователем) и поддерживает состояние микроприложения (инициализация, монтирование и выгрузка). Если вы поймете больше, вы обнаружите, что одинарное спа — это хорошо, но есть некоторые серьезные проблемы.

  1. Слишком инвазивный для микроприложений

    single-spa использует JS Entry для доступа к микроприложениям. Преобразование микроприложений обычно делится на три этапа:

    • Преобразование маршрутизации микроприложений, добавление определенного префикса
    • Преобразование записи микроприложения, изменение точки монтирования и экспорт функции жизненного цикла
    • Изменения конфигурации инструмента упаковки

    Навязчиво сильный на самом деле означает третий пункт, изменение конфигурации инструмента упаковки, использование single-spa для доступа к микроприложению требует упаковки всего микроприложения в файл JS, публикации его на сервере статических ресурсов, а затем настройки JS в основное приложение Адрес файла указывает single-spa перейти по этому адресу для загрузки микроприложения.

    Не говоря уже о других вещах, с этим изменением сейчас большая проблема: все микро-приложение упаковано в JS-файл, а стандартные оптимизации упаковки практически убраны, такие как: загрузка по запросу, оптимизация загрузки ресурсов первого экрана. , CSS независимость упаковки и другие меры по оптимизации.

    Существует ошибка после выпуска проекта, и его необходимо обновить после исправления.Чтобы очистить влияние кеша браузера, общее имя файла будет иметь фрагментное содержимое, а имя файла изменится после того, как микроприложение Применять конфигурацию, потом перекомпилировать основное приложение и публиковать его просто невыносимо, что тожеОдноспальный микроинтерфейсный фреймворк от начального уровня до мастерстваПричина выбора разработки в качестве конфигурации среды при публикации микроприложения в примере проекта в этой статье.

  2. проблема изоляции стиля

    single-spa не выполняет эту часть работы. Масштабная система будет состоять из множества микроприложений, как сделать так, чтобы стили этих микроприложений не влияли друг на друга? Влияют ли стили между микроприложением и основным приложением друг на друга? В настоящее время это может быть достигнуто только с помощью соглашений об именах. Например, стиль приложения начинается с собственного имени приложения, а независимое пространство имен создается с именем приложения. Хорошо сказать, что новая система таким образом, если это существующая система, эта рабочая нагрузка по преобразованию не мала.

  3. JS-изоляция

    С этой частью работы single spa тоже не справился. Загрязнение глобального объекта JS — очень распространенное явление, например, микроприложение A добавляет уникальный атрибут глобальному объекту.window.A, переключиться на микроприложение B в это время, как гарантировать это времяwindowОбъект чистый?

  4. Предварительная загрузка ресурсов

    Эта часть работы не была сделана в одиночку, ведь все микроприложение упаковано в js-файл. Теперь есть требование.Например, для улучшения пользовательского опыта системы, после монтирования первого микро-приложения, браузер должен спокойно загружать статические ресурсы других микро-приложений в фоновом режиме.Как добиться это?

  5. связь между приложениями

    Эту часть работы сингл-спа не делает, он лишь вводит некоторую информацию о состоянии в микроприложение при регистрации микроприложения, и в дальнейшем это не имеет значения, средств связи нет, а пользователь может реализовать его только самостоятельно.

2-я, 3-я и 5-я из 5 вышеперечисленных проблем могут быть решены некоторыми способами, такими как использование пространства имен для решения проблемы изоляции стиля, резервное копирование глобального объекта и инициализация глобального объекта при каждом переключении микроприложения. .Проблема изоляции JS, проблема связи может быть решена путем передачи некоторых методов связи, что зависит от характеристик самого объекта JS (передается ссылка), но первое и четвертое решить непросто, это JS Вход Проблема, вызванная методом, чтобы решить эту проблему, будет намного сложнее, и рабочая нагрузка будет больше. Причем эту общую грязную работу должен решать не пользователь (пользователь фреймворка), а фреймворк.

почему цянькунь

Как упоминалось выше, общая грязная работа должна быть сделана на уровне фреймворка Qiankun сделал вторичную инкапсуляцию на основе single-spa, которая очень хорошо решает вышеупомянутые проблемы.

  1. HTML Entry

    qiankun решает проблемы, вызванные JS Entry, через HTML 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 2.0.26Версия сделала полный анализ исходного кода, и, кажется, нет онлайнqiankun 2.xПолный анализ исходного кода версии, простой поиск кажется версией 1.x

Из-за большого объема кода фреймворка и ограничения на количество слов в блоге весь контент разделен на три статьи, каждую из которых можно читать самостоятельно:

  • Структура микроинтерфейса цянькуня от начального уровня до мастерства

    , статья состоит из следующих трех частей

    • 为什么不是 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

Информативный package.json

  • npm-run-all

    Инструмент командной строки для выполнения нескольких сценариев npm параллельно или последовательно.

  • father-build

    Инструмент для создания библиотек на основе накопительного пакета, отец более мощный

  • Организация многопроектного каталога и написание раздела сценариев

  • основные и модульные поля

    Идентифицирует запись библиотеки компонентов. Когда оба существуют одновременно, поле модуля имеет приоритет над основным

Основное приложение в примере проекта

Здесь вам нужно изменить основное приложение в примере проектаwebpackнастроить

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

Запустите пример проекта

yarn examples:start

После выполнения команды доступlocalhost:7099иlocalhost:7088Два адреса, вы можете увидеть следующее:

image-20220202220258551

image-20220202220401608

На данный момент это доказывает, что проект официально запущен и все приготовления готовы.

пример проекта

Чиновник подготовил для нас два метода реализации для основного приложения и пять примеров доступа к микро-приложениям.Покрытие можно сказать относительно широкое, достаточное для удовлетворения общих потребностей каждого.

основное приложение

основное применение вexamples/mainВ каталоге представлены две реализации, одна из которых основана на конфигурации маршрутизации.registerMicroAppsи вручную загружать микроприложенияloadMicroApp. Основное приложение очень простое.Это проект, настроенный с нуля через веб-пакет, который поддерживает как реакцию, так и vue.Что касается того, почему реакции и vue поддерживаются одновременно, продолжайте читать

webpack.config.js

просто обычныйwebpackНастроить, настроить сервер разработкиdevServer,дваloader(babel-loader, css loader), плагинHtmlWebpackPlugin(укажите webpack, какой файл шаблона html)

пройти черезwebpackфайл конфигурацииentryПоле знает, что входные файлы соответственноindex.jsиmultiple.js

Конфигурация на основе маршрута

Generic связывает микроприложения с некоторымиurlПравила способ достижения, когда браузерurlКогда происходит изменение, функция автоматической загрузки соответствующего микроприложения

index.js
// qiankun api 引入
import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from '../../es';
// 全局样式
import './index.less';

// 专门针对 angular 微应用引入的一个库
import 'zone.js';

/**
 * 主应用可以使用任何技术栈,这里提供了 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,并且第三个参数的颜色由第二个参数决定
        console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
      },
    ],
    beforeMount: [
      app => {
        console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
      },
    ],
    afterUnmount: [
      app => {
        console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
      },
    ],
  },
);

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

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

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

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

// 启动应用
start();

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

// 返回一个 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 {
    app.loading = loading;
  }
}
ReactRender.js
/**
 * 同 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 = document.getElementById('subapp-container');
  ReactDOM.render(<Render loading={loading} />, container);
}
Вручную загружать микроприложения

Обычно микроприложение в этом сценарии представляет собой бизнес-компонент, который может работать независимо без маршрутизации, что бывает редко.

multiple.js
/**
 * 调用 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, созданное vue-cli, а затемvue.config.jsиmain.jsвнес некоторые изменения

vue.config.js

обычныйwebpackКонфигурация, есть три момента, на которые следует обратить внимание

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

Vue.config.productionTip = false;

Vue.use(ElementUI);

let router = null;
let instance = null;

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

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

// 支持应用独立运行
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

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

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

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

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

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

jQuery

Это проект с использованием jQuery, вexamples/purehtmlкаталог, показывающий, как получить доступ к приложениям, разработанным с помощью jQuery

package.json

В демонстрационных целях используйтеhttp-serverЗапустите локальный сервер и поддержите междоменный доступ

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

// 在全局对象上导出三个生命周期函数
(global => {
  global['purehtml'] = {
    bootstrap: () => {
      console.log('purehtml bootstrap');
      return Promise.resolve();
    },
    mount: () => {
      console.log('purehtml mount');
      // 调用渲染函数
      return render($);
    },
    unmount: () => {
      console.log('purehtml unmount');
      return Promise.resolve();
    },
  };
})(window);
index.html
<!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="//cdn.bootcss.com/jquery/3.4.1/jquery.min.js">
  </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>
  <!-- 引入 entry.js,相当于 vue 项目的 publicPath 配置 -->
  <script src="//localhost:7104/entry.js" entry></script>
</body>
</html>

угловой 9, реагировать 15, реагировать 16

Эти три примера проектов не будут анализироваться один за другим.Как и проект vue, все они настраивают инструмент упаковки для упаковки микроприложения в формат umd, а затем настраивают файл записи приложения и префикс маршрутизации.

резюме

Что ж, после прочтения этого трансформация системы (можно начинать работать) в принципе прошла гладко.От разработки основного приложения до доступа к микроприложению проблем быть не должно.

Конечно, если вы хотите продолжить узнавать больше, например:

  • Каково обоснование этих API, использованных выше?
  • Как qiankun решает нерешенную проблему с одним спа, о которой мы упоминали ранее?
  • ...

Далее мы возьмем наши вопросы и цели, чтобы иметь всестороннее и глубокое пониманиеqiankunВнутренняя реализация фреймворка

Исходный код фреймворка

Исходный каталог всего фреймворкаsrc, входной файлsrc/index.ts

запись src/index.ts

/**
 * 在示例或者官网提到的所有 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 = apps.filter(app => !microApps.some(registeredApp => registeredApp.name === app.name));

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

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

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

        // 核心、精髓、难点所在,负责加载微应用,然后一大堆处理,返回 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 (!window.Proxy) {
      console.warn('[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox');
      // 快照沙箱不支持非 singular 模式
      if (!singular) {
        console.error('[qiankun] singular is forced to be true when sandbox enable but proxySandbox unavailable');
        // 如果开启沙箱,会强制使用单例模式
        frameworkConfiguration.singular = true;
      }
    }
  }

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

  frameworkStartedDefer.resolve();
}

Предварительная загрузка — 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[] => apps.filter(app => names.includes(app.name));

  if (Array.isArray(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 = navigator.connection
  ? navigator.connection.saveData ||
    (navigator.connection.type !== 'wifi' &&
      navigator.connection.type !== 'ethernet' &&
      /(2|3)g/.test(navigator.connection.effectiveType))
  : false;

/**
 * prefetch assets, do nothing while in mobile network
 * 预加载静态资源,在移动网络下什么都不做
 * @param entry
 * @param opts
 */
function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
  // 弱网环境下不执行预加载
  if (!navigator.onLine || 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 事件
  window.addEventListener('single-spa:first-mount', function listener() {
    // 已挂载的微应用
    const mountedApps = getMountedApps();
    // 从预加载的微应用列表中过滤出未挂载的微应用
    const notMountedApps = apps.filter(app => mountedApps.indexOf(app.name) === -1);

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

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

    // 移除 single-spa:first-mount 事件
    window.removeEventListener('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 (process.env.NODE_ENV === 'development') {
    console.log('[qiankun] prefetch starting for apps...', apps);
  }

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

Связь между приложениями initGlobalState

// 触发全局监听,执行所有应用注册的回调函数
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));
    }
  });
}

/**
 * 定义全局状态,并返回通信方法,一般由主应用调用,微应用通过 props 获取通信方法。 
 * @param state 全局状态,{ key: value }
 */
export function initGlobalState(state: Record<string, any> = {}) {
  if (state === globalState) {
    console.warn('[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)) {
        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.`);
      }
      // 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) {
        console.warn('[qiankun] state has not changed!');
        return false;
      }

      // 记录旧的全局状态中被改变的 key
      const changeKeys: string[] = [];
      // 旧的全局状态
      const prevGlobalState = cloneDeep(globalState);
      globalState = cloneDeep(
        // 循环遍历新状态中的所有 key
        Object.keys(state).reduce((_globalState, changeKey) => {
          if (isMaster || _globalState.hasOwnProperty(changeKey)) {
            // 主应用 或者 旧的全局状态存在该 key 时才进来,说明只有主应用才可以新增属性,微应用只可以更新已存在的属性值,且不论主应用微应用只能更新一级属性
            // 记录被改变的key
            changeKeys.push(changeKey);
            // 更新旧状态中对应的 key value
            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;
    },
  };
}

глобальный обработчик необработанных исключений

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

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

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

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

setDefaultMountApp

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

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

// 这个 api 和 setDefaultMountApp 作用一致,官网也提到,兼容老版本的一个 api
export function runDefaultMountEffects(defaultAppLink: string) {
  console.warn(
    '[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
  window.addEventListener('single-spa:first-mount', function listener() {
    if (process.env.NODE_ENV === 'development') {
      console.timeEnd(firstMountLogLabel);
    }

    effect();

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

Загрузите MicroApp вручную 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: document.createElement('div'),
    ...props,
  });
}

Основное приложение загрузки qiankun

Следующий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()}_${Math.floor(Math.random() * 1000)}`;

  // 下面这个不用管,就是生成一个标记名称,然后使用该名称在浏览器性能缓冲器中设置一个时间戳,可以用来度量程序的执行时间,performance.mark、performance.measure
  const markName = `[qiankun] App ${appInstanceId} Loading`;
  if (process.env.NODE_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 && prevAppUnmountedDeferred.promise);
  }

  // --------------- 样式隔离 ---------------
  // 是否严格样式隔离
  const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;
  // 实验性的样式隔离,后面就叫 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 = element.querySelectorAll('style') || [];
    forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
      css.process(element!, stylesheetElement, appName);
    });
  }

  // --------------- 渲染微应用 ---------------
  // 主应用装载微应用的容器节点
  const container = 'container' in app ? app.container : undefined;
  // 这个是 1.x 版本遗留下来的实现,如果提供了 render 函数,当微应用需要被激活时就执行 render 函数渲染微应用,新版本用的 container,弃了 render
  // 而且 legacyRender 和 strictStyleIsolation、scoped css 不兼容
  const legacyRender = 'render' in app ? app.render : 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 = () => Promise.resolve();
  let unmountSandbox = () => Promise.resolve();
  if (sandbox) {
    /**
     * 生成运行时沙箱,这个沙箱其实由两部分组成 => JS 沙箱(执行上下文)、样式沙箱
     * 
     * 沙箱返回 window 的代理对象 proxy 和 mount、unmount 两个方法
     * unmount 方法会让微应用失活,恢复被增强的原生方法,并记录一堆 rebuild 函数,这个函数是微应用卸载时希望自己被重新挂载时要做的一些事情,比如动态样式表重建(卸载时会缓存)
     * mount 方法会执行一些一些 patch 动作,恢复原生方法的增强功能,并执行 rebuild 函数,将微应用恢复到卸载时的状态,当然从初始化状态进入挂载状态就没有恢复一说了
     */
    const sandboxInstance = createSandbox(
      appName,
      containerGetter,
      Boolean(singular),
      enableScopedCSS,
      excludeAssetFilter,
    );
    // 用沙箱的代理对象作为接下来使用的全局对象
    global = sandboxInstance.proxy as typeof window;
    mountSandbox = sandboxInstance.mount;
    unmountSandbox = sandboxInstance.unmount;
  }

  // 合并用户传递的生命周期对象和 qiankun 框架内置的生命周期对象
  const { beforeUnmount = [], afterUnmount = [], afterMount = [], beforeMount = [], beforeLoad = [] } = mergeWith(
    {},
    // 返回内置生命周期对象,global.__POWERED_BY_QIANKUN__ 和 global.__INJECTED_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 (process.env.NODE_ENV === 'development') {
          const marks = performance.getEntriesByName(markName, 'mark');
          // mark length is zero means the app is remounting
          if (!marks.length) {
            performanceMark(markName);
          }
        }
      },
      // 单例模式需要等微应用卸载完成以后才能执行挂载任务,promise 会在微应用卸载完以后 resolve
      async () => {
        if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
          return prevAppUnmountedDeferred.promise;
        }

        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 生命周期函数传递参数,比如微应用中使用的 props.onGlobalStateChange 方法
      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 (process.env.NODE_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) {
          prevAppUnmountedDeferred.resolve();
        }
      },
    ],
  };

  // 微应用有可能定义 update 方法
  if (typeof update === 'function') {
    parcelConfig.update = update;
  }

  return parcelConfig;
}

изоляция стиля

qiankunЕсть два способа изоляции стиля, один из них — строгая изоляция стиля,shadow domдостижения, другой — это изоляция экспериментального стиля, котораяscoped css, два пути не могут сосуществовать

Строгая изоляция стиля

существуетqiankunСтрогая изоляция стиля вcreateElementметод, поshadow domреализовать,shadow domЭто возможность, предоставляемая браузерами изначально, и долгое время в прошлом браузеры использовали ее для инкапсуляции внутренней структуры некоторых элементов. с кнопкой управления воспроизведением по умолчанию<video>Элемент, например, на самом деле в своем Shadow DOM содержит ряд кнопок и других контроллеров. Стандарт Shadow DOM позволяет вам поддерживать набор 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 = document.createElement('div');
  // 将字符串模版 appContent 设置为 div 的子与阿苏
  containerElement.innerHTML = appContent;
  // appContent always wrapped with a singular div,appContent 由模版字符串变成了 DOM 元素
  const appElement = containerElement.firstChild as HTMLElement;
  // 如果开启了严格的样式隔离,则将 appContent 的子元素(微应用的入口模版)用 shadow dom 包裹,以达到微应用之间样式严格隔离的目的
  if (strictStyleIsolation) {
    if (!supportShadowDOM) {
      console.warn(
        '[qiankun]: As current browser not support shadow dom, your strictStyleIsolation configuration will be ignored!',
      );
    } else {
      const { innerHTML } = appElement;
      appElement.innerHTML = '';
      let shadow: ShadowRoot;

      if (appElement.attachShadow) {
        shadow = appElement.attachShadow({ mode: 'open' });
      } else {
        // createShadowRoot was proposed in initial spec, which has then been deprecated
        shadow = (appElement as any).createShadowRoot();
      }
      shadow.innerHTML = innerHTML;
    }
  }

  return appElement;
}
Изоляция экспериментального стиля

Экспериментальный стиль изоляции на самом делеscoped css,qiankunбудет ограничено путем динамического переопределения специального ограничения селектораcss, примененный стиль будет переписан по следующему шаблону:

// 假设应用名是 react16
.app-main {
  font-size: 14px;
}
div[data-qiankun-react16] .app-main {
  font-size: 14px;
}
process
/**
 * 做了两件事:
 *  实例化 processor = new ScopedCss(),真正处理样式选择器的地方
 *  生成样式前缀 `div[data-qiankun]=${appName}`
 * @param appWrapper = <div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div>
 * @param stylesheetElement = <style>xx</style>
 * @param appName 微应用名称
 */
export const process = (
  appWrapper: HTMLElement,
  stylesheetElement: HTMLStyleElement | HTMLLinkElement,
  appName: string,
) => {
  // lazy singleton pattern,单例模式
  if (!processor) {
    processor = new ScopedCSS();
  }

  // 目前支持 style 标签
  if (stylesheetElement.tagName === 'LINK') {
    console.warn('Feature: sandbox.experimentalStyleIsolation is not support for link element yet.');
  }

  // 微应用模版
  const mountDOM = appWrapper;
  if (!mountDOM) {
    return;
  }

  // div
  const tag = (mountDOM.tagName || '').toLowerCase();

  if (tag && stylesheetElement.tagName === 'STYLE') {
    // 生成前缀 `div[data-qiankun]=${appName}`
    const prefix = `${tag}[${QiankunCSSRewriteAttr}="${appName}"]`;
     /**
     * 实际处理样式的地方
     * 拿到样式节点中的所有样式规则,然后重写样式选择器
     *  含有根元素选择器的情况:用前缀替换掉选择器中的根元素选择器部分,
     *  普通选择器:将前缀插到第一个选择器的后面
     */
    processor.process(stylesheetElement, prefix);
  }
}

export const QiankunCSSRewriteAttr = 'data-qiankun';
ScopedCSS
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
enum RuleType {
  // type: rule will be rewrote
  STYLE = 1,
  MEDIA = 4,
  SUPPORTS = 12,

  // type: value will be kept
  IMPORT = 3,
  FONT_FACE = 5,
  PAGE = 6,
  KEYFRAMES = 7,
  KEYFRAME = 8,
}

const arrayify = <T>(list: CSSRuleList | any[]) => {
  return [].slice.call(list, 0) as T[];
};

export class ScopedCSS {
  private static ModifiedTag = 'Symbol(style-modified-qiankun)';

  private sheet: StyleSheet;

  private swapNode: HTMLStyleElement;

  constructor() {
    const styleNode = document.createElement('style');
    document.body.appendChild(styleNode);

    this.swapNode = styleNode;
    this.sheet = styleNode.sheet!;
    this.sheet.disabled = true;
  }

  /**
   * 拿到样式节点中的所有样式规则,然后重写样式选择器
   *  含有根元素选择器的情况:用前缀替换掉选择器中的根元素选择器部分,
   *  普通选择器:将前缀插到第一个选择器的后面
   * 
   * 如果发现一个样式节点为空,则该节点的样式内容可能会被动态插入,qiankun 监控了该动态插入的样式,并做了同样的处理
   * 
   * @param styleNode 样式节点
   * @param prefix 前缀 `div[data-qiankun]=${appName}`
   */
  process(styleNode: HTMLStyleElement, prefix: string = '') {
    // 样式节点不为空,即 <style>xx</style>
    if (styleNode.textContent !== '') {
      // 创建一个文本节点,内容为 style 节点内的样式内容
      const textNode = document.createTextNode(styleNode.textContent || '');
      // swapNode 是 ScopedCss 类实例化时创建的一个空 style 节点,将样式内容添加到这个节点下
      this.swapNode.appendChild(textNode);
      /**
       * {
       *  cssRules: CSSRuleList {0: CSSStyleRule, 1: CSSStyleRule, 2: CSSStyleRule, 3: CSSStyleRule, length: 4}
       *  disabled: false
       *  href: null
       *  media: MediaList {length: 0, mediaText: ""}
       *  ownerNode: style
       *  ownerRule: null
       *  parentStyleSheet: null
       *  rules: CSSRuleList {0: CSSStyleRule, 1: CSSStyleRule, 2: CSSStyleRule, 3: CSSStyleRule, length: 4}
       *  title: null
       *  type: "text/css"
       * }
       */
      const sheet = this.swapNode.sheet as any; // type is missing
      /**
       * 得到所有的样式规则,比如
       * [
       *  {selectorText: "body", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "body { background: rgb(255, 255, 255); margin: 0px; }", …}
       *  {selectorText: "#oneGoogleBar", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "#oneGoogleBar { height: 56px; }", …}
       *  {selectorText: "#backgroundImage", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "#backgroundImage { border: none; height: 100%; poi…xed; top: 0px; visibility: hidden; width: 100%; }", …}
       *  {selectorText: "[show-background-image] #backgroundImage {xx}"
       * ]
       */
      const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
      /**
       * 重写样式选择器
       *  含有根元素选择器的情况:用前缀替换掉选择器中的根元素选择器部分,
       *  普通选择器:将前缀插到第一个选择器的后面
       */
      const css = this.rewrite(rules, prefix);
      // 用重写后的样式替换原来的样式
      // eslint-disable-next-line no-param-reassign
      styleNode.textContent = css;

      // cleanup
      this.swapNode.removeChild(textNode);
      return;
    }

    /**
     * 
     * 走到这里说明样式节点为空
     */

    // 创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用
    const mutator = new MutationObserver(mutations => {
      for (let i = 0; i < mutations.length; i += 1) {
        const mutation = mutations[i];

        // 表示该节点已经被 qiankun 处理过,后面就不会再被重复处理
        if (ScopedCSS.ModifiedTag in styleNode) {
          return;
        }

        // 如果是子节点列表发生变化
        if (mutation.type === 'childList') {
          // 拿到 styleNode 下的所有样式规则,并重写其样式选择器,然后用重写后的样式替换原有样式
          const sheet = styleNode.sheet as any;
          const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
          const css = this.rewrite(rules, prefix);

          // eslint-disable-next-line no-param-reassign
          styleNode.textContent = css;
          // 给 styleNode 添加一个 ScopedCss.ModifiedTag 属性,表示已经被 qiankun 处理过,后面就不会再被处理了
          // eslint-disable-next-line no-param-reassign
          (styleNode as any)[ScopedCSS.ModifiedTag] = true;
        }
      }
    });

    // since observer will be deleted when node be removed
    // we dont need create a cleanup function manually
    // see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/disconnect
    // 观察 styleNode 节点,当其子节点发生变化时调用 callback 即 实例化时传递的函数
    mutator.observe(styleNode, { childList: true });
  }

  /**
   * 重写样式选择器,都是在 ruleStyle 中处理的:
   *  含有根元素选择器的情况:用前缀替换掉选择器中的根元素选择器部分,
   *  普通选择器:将前缀插到第一个选择器的后面
   * 
   * @param rules 样式规则
   * @param prefix 前缀 `div[data-qiankun]=${appName}`
   */
  private rewrite(rules: CSSRule[], prefix: string = '') {
    let css = '';

    rules.forEach(rule => {
      // 几种类型的样式规则,所有类型查看 https://developer.mozilla.org/zh-CN/docs/Web/API/CSSRule#%E7%B1%BB%E5%9E%8B%E5%B8%B8%E9%87%8F
      switch (rule.type) {
        // 最常见的 selector { prop: val }
        case RuleType.STYLE:
          /**
           * 含有根元素选择器的情况:用前缀替换掉选择器中的根元素选择器部分,
           * 普通选择器:将前缀插到第一个选择器的后面
           */
          css += this.ruleStyle(rule as CSSStyleRule, prefix);
          break;
        // 媒体 @media screen and (max-width: 300px) { prop: val }
        case RuleType.MEDIA:
          // 拿到其中的具体样式规则,然后调用 rewrite 通过 ruleStyle 去处理
          css += this.ruleMedia(rule as CSSMediaRule, prefix);
          break;
        // @supports (display: grid) {}
        case RuleType.SUPPORTS:
          // 拿到其中的具体样式规则,然后调用 rewrite 通过 ruleStyle 去处理
          css += this.ruleSupport(rule as CSSSupportsRule, prefix);
          break;
        // 其它,直接返回样式内容
        default:
          css += `${rule.cssText}`;
          break;
      }
    });

    return css;
  }

  /**
   * 普通的根选择器用前缀代替
   * 根组合选择器置空,忽略非标准形式的兄弟选择器,比如 html + body {...}
   * 针对普通选择器则是在第一个选择器后面插入前缀,比如 .xx 变成 .xxprefix
   * 
   * 总结就是:
   *  含有根元素选择器的情况:用前缀替换掉选择器中的根元素选择器部分,
   *  普通选择器:将前缀插到第一个选择器的后面
   * 
   * handle case:
   * .app-main {}
   * html, body {}
   * 
   * @param rule 比如:.app-main {} 或者 html, body {}
   * @param prefix `div[data-qiankun]=${appName}`
   */
  // eslint-disable-next-line class-methods-use-this
  private ruleStyle(rule: CSSStyleRule, prefix: string) {
    // 根选择,比如 html、body、:root
    const rootSelectorRE = /((?:[^\w\-.#]|^)(body|html|:root))/gm;
    // 根组合选择器,比如 html body {...} 、 html > body {...}
    const rootCombinationRE = /(html[^\w{[]+)/gm;

    // 选择器
    const selector = rule.selectorText.trim();

    // 样式文本
    let { cssText } = rule;

    // 如果选择器为根选择器,则直接用前缀将根选择器替换掉
    // handle html { ... }
    // handle body { ... }
    // handle :root { ... }
    if (selector === 'html' || selector === 'body' || selector === ':root') {
      return cssText.replace(rootSelectorRE, prefix);
    }

    // 根组合选择器
    // handle html body { ... }
    // handle html > body { ... }
    if (rootCombinationRE.test(rule.selectorText)) {
      // 兄弟选择器 html + body,非标准选择器,无效,转换时忽略
      const siblingSelectorRE = /(html[^\w{]+)(\+|~)/gm;

      // since html + body is a non-standard rule for html
      // transformer will ignore it
      if (!siblingSelectorRE.test(rule.selectorText)) {
        // 说明时 html + body 这种非标准形式,则将根组合器置空
        cssText = cssText.replace(rootCombinationRE, '');
      }
    }

    // 其它一般选择器,比如 类选择器、id 选择器、元素选择器、组合选择器等
    // handle grouping selector, a,span,p,div { ... }
    cssText = cssText.replace(/^[\s\S]+{/, selectors =>
      // item 是匹配的字串,p 是第一个分组匹配的内容,s 是第二个分组匹配的内容
      selectors.replace(/(^|,\n?)([^,]+)/g, (item, p, s) => {
        // handle div,body,span { ... }
        if (rootSelectorRE.test(item)) {
          // 说明选择器中含有根元素选择器
          return item.replace(rootSelectorRE, m => {
            // do not discard valid previous character, such as body,html or *:not(:root)
            const whitePrevChars = [',', '('];

            // 将其中的根元素替换为前缀
            if (m && whitePrevChars.includes(m[0])) {
              return `${m[0]}${prefix}`;
            }

            // replace root selector with prefix
            return prefix;
          });
        }

        // selector1 selector2 =》 selector1prefix selector2
        return `${p}${prefix} ${s.replace(/^ */, '')}`;
      }),
    );

    return cssText;
  }

  // 拿到其中的具体样式规则,然后调用 rewrite 通过 ruleStyle 去处理
  // handle case:
  // @media screen and (max-width: 300px) {}
  private ruleMedia(rule: CSSMediaRule, prefix: string) {
    const css = this.rewrite(arrayify(rule.cssRules), prefix);
    return `@media ${rule.conditionText} {${css}}`;
  }

  // 拿到其中的具体样式规则,然后调用 rewrite 通过 ruleStyle 去处理
  // handle case:
  // @supports (display: grid) {}
  private ruleSupport(rule: CSSSupportsRule, prefix: string) {
    const css = this.rewrite(arrayify(rule.cssRules), prefix);
    return `@supports ${rule.conditionText} {${css}}`;
  }
}

Эпилог

Приведенное выше содержание представляет собой полную интерпретацию фреймворка qiankun. Я верю, что вы получите хороший урожай после прочтения этой статьи. Исходный код находится вgithub

Ощущение при чтении цянькунь书读百变其义自现, Реализация фреймворка qiankun в некоторых местах все еще сложна для понимания. Я полагаю, что каждый будет чувствовать себя так при чтении исходного кода. Затем прочитайте его несколько раз. Конечно, вы также можете зайти в область комментариев, чтобы пообщаться , учитесь и развивайтесь вместе! !

Ссылка на сайт

Всем спасибо:как,собиратьиКомментарий, увидимся в следующий раз.


Когда обучение становится привычкой, знание становится здравым смыслом, отсканируйте код и подпишитесь на общедоступную учетную запись WeChat, чтобы учиться и развиваться вместе. Статьи были включены вgithub, Добро пожаловать в Watch and Star.

微信公众号