праздник закончился! Я не позволю вам пока не знать принципы микрофронтендов!

внешний фреймворк

Красивый и радостный праздник закончился, плачу, иду к машине и изучаю взмах микро передних клаксонов~

single-spa+qiankun(Это два очень знакомых имени), я выгляжу так, как ты хочешь~ Если вы хотите увидеть движение анализа Module Federation, я опущу последнюю статью.Наггетс.Талант/пост/694979…

Конечно, наша правильная операция:

ошибся, опять: 👇Текст начинается

Что делает одноместный спа?

Давайте посмотрим, что сделал single-spa

Сначала подумайте, что вам нужно сделать, если вы реализуете систему, которая может переключать подприложения самостоятельно, если вы реализуете изменение маршрутизации (моя привычка, сначала подумать, как это реализовать самостоятельно, а затем перейти к исходному коду, чтобы найти и подтвердите свои мысли), примерно следите за изменениями url, переключайте нас на под-приложения, как получить под-приложения?() => import()илиfetchВсе ресурсы можно запихнуть под наш соответствующий дом и удалить при удалении приложения.

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

Затем прочитайте исходный код и расскажите о нескольких основных методах и идеях.

отregisterApplicationПосмотрите на метод регистрации суб-приложений и сохраните конфигурацию и инициализацию суб-приложений после выполнения.status. Третий параметр — это оценка состояния активации Активация может быть просто понята как видимая на интерфейсе.

Жизненный цикл: Подсистема должна быть экспортированаbootstrap(инициализация),mount(при загрузке) иunmount(при удалении) функции жизненного цикла,unload(если снято) Необязательно. иметь следующие состояния

  • start: Старт как следует из названия, помимо отметки приложение запущено, приходится перенаправлять.
  • rerouteВремя выполнения: Либо вручную,registerAppа такжеstart, или при переключении маршрута оценивается текущее состояние и выполняется функция жизненного цикла.
    • Если не в настоящее времяstartДа, описание первое время, активное состояние будетappsвоплощать в жизньloadApps,нагрузкаjs entryПосле окончания состояние изменяетсяNOT_BOOTSTRAPPED.
    • еслиstartпрошел, будетappstoUnmontудалить, будетapptoloadзагружаем и монтируем.LOADING_SOURCE_CODE -> NOT_BOOTSTRAPPED, загруженный в ресурс, вы можете получить методы жизненного цикла, предоставленные пользователем, сохранить их и выполнить жизненный цикл начальной загрузки,BOOTSTRAPPING -> NOT_MOUNTED, загрузка завершена. Следующим шагом является монтирование, изменение состояния для выполнения жизненного цикла монтирования,MOUNTING -> MOUNTED, монтаж завершен. для未激活的, то есть если роут не совпадает, значит его надо деинсталлировать в это время, и выполняется жизненный цикл unmout.UNMOUNTING -> NOT_MOUNTED.

Для одностраничных приложений, таких какreact-routerно также использовать нативную маршрутизациюhistory.pushState history.replaceStateреализацию, поэтому мы улучшаем эти два метода, чтобы они вызывали в дополнение к нативной способностиreroute, реализуется замкнутый цикл.

Различные состояния:

  • NOT_LOADED: приложение не загружено или удалено;
  • LOADING_SOURCE_CODE: указывает, что загружается исходный код подприложения;
  • NOT_BOOTSTRAPPED: законченоapp.loadApp, то есть состояние после загрузки субприложения
  • BOOTSTRAPPING: Инициализация приложения, выполнение жизненного цикла начальной загрузки;;
  • NOT_MOUNTED: Инициализация завершена, а загрузка или выполнение unmout завершены;
  • MOUNTING: Монтаж, выполнить монтирование;
  • MOUNTED: после того, как приложение смонтировано, можно выполнить рендеринг;
  • UNMOUNTING: выполнить жизненный цикл unmout и изменить его на NOT_MOUNTED после выполнения;
  • UNLOADING: Удалить приложение, у приложения больше нет жизненного цикла, но на самом деле оно не удаляется.При его последующей активации нет необходимости повторно загружать ресурсы, достаточно внести некоторые изменения состояния;
  • SKIP_BECAUSE_BROKEN: Ошибка загрузки

улучшенwindow.history.pushState,replaceState, в дополнение к выполнению нативного также будет выполнятьсяreroute.

События Single-spa используют микрозадачиreturn Promise.resolve().then(() => actions()), Такой способ записи не может повлиять на основную задачу, а сообщение об ошибке не приведет к прерыванию основной задачи.

одноместный спа нуждается в большом ремонте

Дефекты, вызванные записью js

все еще помнюregisterApplicationВторой параметр , как запись ресурса загрузки, вloadAppsбудет выполняться в нашем пакетном проекте webpack, который будет использоватьsplit chunkсделать распаковку, но от одиночной записи придется отказатьсяsplit chunk; Подприложения не имеют приоритета, записи будут загружаться вместе, и будет ограничение на количество одновременных подприложений.

Как общаться

Хотя сплит-система должна избегать обмена данными, связь между системами по-прежнему неизбежна.aбыл изменен вbдолжны быть синхронизированы, требуется определяемая пользователем передача событий,aСоздавайте собственные события,windowзарегистрировать пользовательские прослушиватели событий наaтриггерное событие, вbПолучить, общедоступные данные хранятся вlocalstorage. Не очень дружелюбное руководство.

Изменить префикс подсистемы времени выполнения

picture20210426160801

наших комплектных подсистемhtmlПуть импорта ресурсов такой же, как у Sauce.Если вы вставите его в родительский домен, вы не найдете ресурсы.Поэтому вам нужно заменить абсолютный путь, чтобы вытащить ресурсы по разным подсистемам.systemjs-webpack-interop, сопоставьте файл конфигурации с именем проекта, измените\_\_webpack_public_path\_\_(вебпак написан глобально, можете набрать сами,\_\_webpack_require\_\_也有), конфигурация показана на рисунке

picture20210426162028

изоляция css, изоляция js, изоляция dom

Просто дляstyleМожно добавить в тело скоуп, чтобы изолировать глобальный стиль, и очистить скоуп при выгрузке, но для jq проектов выбор селекторов все равно будет путаться.windowНедостаточно загрязнять методы, таймеры, глобальные переменные и т. д., полагаясь на ограничения кода.

Если вам лень менять, то идите сразу к qiankun

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

Что цянькунь сделал для нас?

Посмотрите, что написано на официальном сайтеbiaoqingbao02.png

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

Я не позволю вам не знать его обоснование

kuailexingqiu.gif

Начиная с нескольких вопросов, что такое счастливая планета~ Теперь я возьму вас на исследование~

Общедоступный путь среды выполнения для решения проблем с ресурсами подприложения

Прочитав документ, я начал подозревать две возможности, одна модификация\_\_webpack_require\_\_.e, при загрузке файла измените путь ресурса выборки в соответствии с именем подприложения, но это не очень хорошо, чтобы вторгнуться в веб-пакет; другой - изменить время выполнения\_\_webpack_public_path\_\_Глобальные переменные, затем следуйте коду, чтобы узнать.

Первый взгляд на использование

  1. существуетsrcКаталог добавленpublic-path.js:

    if (window.__POWERED_BY_QIANKUN__) {
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    
  2. входной файлmain.jsИсправлять.

  import './public-path';

Когда вы увидите использование, вы поймете, что оно используется для модификации__webpack_public_path__образом, этот код вводит файл ввода,__webpack_public_path__изменен на__INJECTED_PUBLIC_PATH_BY_QIANKUN__, tupian002.pngВидно, что эта переменная присваивается в хуке жизненного цикла,beforeloadВнедрять переменные перед загрузкой подприложений,beformountВнедрить переменные перед монтированием субприложения (данный цикл может выполняться несколько раз, если оно было смонтировано ранее, то оно будет выполнено. Если не смонтировано, значит, оно было смонтированоbeforeloadвводится в него)beforeUnmountПрежде чем субприложение будет удалено, удалите переменную.publicPathпередается этим методом, по умолчанию'/', чтобы увидеть, кто звонилgetAddOn, что передано. отслеживание фактического поступленияpublicpathместо, вставьте сокращенную версию.

export async function loadApp<T extends ObjectType>(
  app: LoadableApp<T>,
  configuration: FrameworkConfiguration = {},
  lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {
  const { entry, name: appName } = app;
  ...//这里返回了publicpath,importEntry解析entry返回解析好的对象
    const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
    ...omit
    // getAddon实际是空方法,getAddOns调用了他
    getAddOns(global, assetPublicPath);
  }

Любопытно увидеть этоentryчто это такое?entryтип ТСentry ts 类型Глядя на название, я догадался, что этоregisterMicroAppsВведите запись appconfig, тогда проблема в том, как вы анализируете адрес, который я передаю? Я изменил свою запись и позже сделал большой хитрый трюк.Это нормально, что скрипт в html тянут, но динамически импортированные картинки и куски не могут быть найдены.Вот почему.scriptТег script — это абсолютный путь, например, наш путьhttp://localhost:7777/subapp/sub-vue/dacongming/, даже если написание неправильное, доменное имя и порт правильные, и документ одностраничного приложения все еще там.После замены абсолютного пути это доменное имя + порт + путь, поэтому нет проблема. Проблема динамически вставляется через js, наши картинки, чанки и т.д., webpack имеет глобальные переменные при упаковке__webpack_public_path__, склейка относительного пути в абсолютный путь, когда мы тянем запись подприложения под основное приложение, если мы не модифицируем__webpack_public_path__, вы не сможете получить правильный ресурс, поэтому вам нужно заменить эту переменную.Вход в код также подтверждает, что выплевывающийся ресурсassetpath является введенной записью.Если совпадение неверно, вы, естественно, не сможете получить ресурс.

ноregisterMicroAppsЕсли запись переданного appconfig совпадает с записью моего ресурсаpublicpathЧто делать, если он не совпадает?Если моя запись html-ресурса не является корневым путем или хеш-режимом, он точно не будет использоваться в качестве адреса ресурса, который необходимо изменить, не паникуйте, start поддерживает изменение входящего параметра, конфигурацияgetPublicPath, чтобы вы могли беспрепятственно получать ресурсы. (Скажите, почему этот метод называется get, потому что он используется qiankun, что разумно)

start({
// 这个优先级最高,会干掉你的entry,传入方法,可以判断子应用返回不同的public_path
  getPublicPath: () => 'http://localhost:7777'
})
// webpack output 参数配置
output: {
  ..omit
  assetModuleFilename: 'static/media/[name].[hash:8][ext]',
  filename: 'static/js/[name].[contenthash:8].js',
  chunkFilename: 'static/js/[name].[contenthash:8].chunk.js',
},

image.png

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

startФункция передает первый параметр для настройки стратегии загрузки.trueСначала загружается хит, а другие ресурсы идут предварительно загруженными, что можно понимать как отложенную загрузку. Вышеприведенный анализ производительности односпального спа знает, что ресурсы будутPromise.allЗагрузка вместе, как добиться упорядоченного?

export function registerMicroApps<T extends ObjectType>(
  apps: Array<RegistrableApp<T>>,
  lifeCycles?: FrameworkLifeCycles<T>,
) {
  // Each app only needs to be registered once
  const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name));

  microApps = [...microApps, ...unregisteredApps];
//  看这里是是所有的app都会被执行registerApplication
  unregisteredApps.forEach((app) => {
    const { name, activeRule, loader = noop, props, ...appConfig } = app;

    registerApplication({
      name,
      app: async () => {
        loader(true);
        // 这里划重点1
        await frameworkStartedDefer.promise;
        // 划重点2
        const { mount, ...otherMicroAppConfigs } = (
          await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
        )();

        return {
          mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
          ...otherMicroAppConfigs,
        };
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}
  • фокус 1 : awaitЗабудь о спинеawaitчто если яawaitохватыватьnew Promise(res => window.res = res)Таким образом, загрузка ресурсов single-spa остановлена ​​мной, только последующаяres()продолжить, в qiankun'sstartметод называетсяreslove.
  • фокус 2: Похоже, что Qiankun настраивает собственный loadapp для подтягивания ресурсов. Продолжай читатьstartметод
export function start(opts: FrameworkConfiguration = {}) {
 frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts }
 const { prefetch, sandbox, singular, urlRerouteOnly, ...importEntryOpts } = frameworkConfiguration

 if (prefetch) {
  doPrefetchStrategy(microApps, prefetch, importEntryOpts)
 }

 // ...sandbox先不看 omit

 startSingleSpa({ urlRerouteOnly })
 frameworkStartedDefer.resolve() // 这里就是上面的defer promise res的地方,之后可以开始加载资源。
}

doPrefetchStrategyстратегия предварительной загрузки, еслиprefetchStrategyЕсли это правда, он предварительно загружается и вызываетсяprefetchAfterFirstMountedметод, а затем посмотрите, как qiankun выполняет предварительную загрузку

function prefetchAfterFirstMounted(apps: AppMetadata[], opts?: ImportEntryOpts): void {
 window.addEventListener('single-spa:first-mount', function listener() {
  const notLoadedApps = apps.filter((app) => getAppStatus(app.name) === NOT_LOADED)
  notLoadedApps.forEach(({ entry }) => prefetch(entry, opts))
  window.removeEventListener('single-spa:first-mount', listener)
 })
}

в первом приложенииmount, single-spa отправит пользовательский методsingle-spa:first-mount,

prefetchAfterFirstMountedметод контролируетсяsingle-spa:first-mount, срабатывает при первом применении монтирования, чтобы определить, является ли состояние незагруженным ресурсом или нет.NOT_LOADED,БудуnotLoadedApps(то же, что и appstoload) выполнить обновление.

prefetchиспользоватьrequestidlecallbackметод ленивой загрузки ресурсов, если такого метода нет, используйтеsetTimeoutмоделирование(requestidlecallbackсделает обратный вызов после завершения рендеринга, используйтеsetTimeoutЕго также можно смоделировать, и браузер не будет выполнять синхронный код, пока он не завершит выполнение.setTimeout),requestidlecallbackвходящийgetExternalStyleSheetsа такжеgetExternalScripts, обычный матчscriptмаркировать, выполнятьfetchИзвлеките строку кода, сохраните результат и выполните его непосредственно в loadApp.

ps: Кроме того, есть еще одна идея реализации.Если single-spa не проталкивает ресурс активного маршрута, то есть совпадающий маршрут, в appstoload, и не использует загрузку ресурсов single-spa, он также можно управлять им самостоятельно.Определить, активен он или нет, и выделить активный. Вот мои мысли >-

class Defer {
 constructor() {
  this.promise = new Promise((res, rej) => {
   this.resolve = res
   this.reject = rej
  })
 }
}

function register(entry, opts) {
 const defer = new Defer()
 const app = {
  name: '',
  loadApp() {
   await defer.promise()
   fetch(entry, opts)
  },
  defer: defer
 }
 apps.push(app)
}
function judgeRoute() {
 return apps.filter((i) => i.route === location.path)
}
function start() {
 const apps = judgeRoute()
 apps.forEach((i) => i.defer.resolve())
 singleSpa.start()
}

Что такое песочница

в реальных ресурсах исполненияscriptsМесто,loadAppДля достижения изоляции js сначала будет создана песочница.

Совместимость не рассматриваетсяproxy, в настоящее время qiankun находится в единственном экземпляре (пока толькоmountмикроприложение) с помощьюSingularProxySandbox, используется в нескольких случаяхproxySandbox, на самом деле использоватьproxy.

SingularProxySandboxИзменение новых свойств по-прежнему верноwindow, если токwindowЭтот атрибут не существует, используйтеaddedPropsMapзапись, если текущаяwindowсвойство существует на объекте, иmodifiedMapне записывается вmodifiedMapЗапишите начальное значение этого свойства; независимо от того, было оно записано или нет, оно будет записано вcurrentUpdatedPropsValueMap, каждый раз, когда песочница удаляется, Цзянцзы ставитwindowиспользовано вышеmodifiedMap 给还原,windowсвойств, которых не было изначально,addedPropsMapизkeyСоответствующая установка значенияundefinedФактически он удаляется, он будет использован снова при следующей активации песочницы.currentUpdatedPropsValueMapЧтобы восстановить его, если одновременно существует несколько микроприложений, это будет беспорядочно.

Используйте несколько экземпляровproxySandbox, прямо кwindowоперация помещается вfakeWindow, одно приложение соответствует одномуfakewindow,правильноfakewindowОбъект проксируется без загрязнения глобальногоwindow. При взятии значения, если нет

fakewindowС этим свойством отfakewindowВозьми, если нет, возьмиwindowВозьмите его, при настройке установите его прямо наfakewindow.fakewindowзаключается в создании нового объекта,windowКопируются атрибуты, которые можно переопределить в копии.

let activeSandboxCount = 0
class ProxySandbox {
 active() {
  this.sandboxRunning = true
 }
 inactive() {
  this.sandboxRunning = false
 }
 constructor() {
  const rawWindow = window
  const fakeWindow = {}
  const proxy = new Proxy(fakeWindow, {
   set: (target, prop, value) => {
    if (this.sandboxRunning) {
     target[prop] = value
     return true
    }
   },
   get: (target, prop) => {
    let value = prop in target ? target[prop] : rawWindow[prop]
    return value
   }
  })
  this.proxy = proxy
 }
}

угонять

createSandboxContainerМетод создания песочницы возвращает текущий экземпляр песочницы и метод, который будет выполняться с жизненным циклом подприложения.mountа такжеunmount. После того, как песочница запущена, она начинает захватывать все виды глобального мониторинга, фокусироваться на анализеpatchAtMounting.

return {
 instance: sandbox,

 /**
  * 沙箱被 mount
  * 可能是从 bootstrap 状态进入的 mount
  * 也可能是从 unmount 之后再次唤醒进入 mount
  */
 async mount() {
  /* ------------------------------------------ 因为有上下文依赖(window),以下代码执行顺序不能变 ------------------------------------------ */

  /* ------------------------------------------ 1. 启动/恢复 沙箱------------------------------------------ */
  sandbox.active()

  const sideEffectsRebuildersAtBootstrapping = sideEffectsRebuilders.slice(0, bootstrappingFreers.length)
  const sideEffectsRebuildersAtMounting = sideEffectsRebuilders.slice(bootstrappingFreers.length)

  // must rebuild the side effects which added at bootstrapping firstly to recovery to nature state
  if (sideEffectsRebuildersAtBootstrapping.length) {
   sideEffectsRebuildersAtBootstrapping.forEach((rebuild) => rebuild())
  }

  /* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/
  // render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用
  // 沙箱启动后开始劫持各类全局监听 这个劫持方法敲重点
  mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter)

  /* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/
  // 存在 rebuilder 则表明有些副作用需要重建
  if (sideEffectsRebuildersAtMounting.length) {
   sideEffectsRebuildersAtMounting.forEach((rebuild) => rebuild())
  }

  // clean up rebuilders
  sideEffectsRebuilders = []
 },

 /**
  * 恢复 global 状态,使其能回到应用加载之前的状态
  */
 async unmount() {
  // record the rebuilders of window side effects (event listeners or timers)
  // note that the frees of mounting phase are one-off as it will be re-init at next mounting
  sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map((free) => free())

  sandbox.inactive()
 }
}

Все методы, которые могут загрязнить мир, должны быть захвачены, такие как таймеры, методы в окнах, динамические таблицы стилей и js.

const basePatchers = [
 () => patchInterval(sandbox.proxy), // 计时器劫持
 () => patchWindowListener(sandbox.proxy), // window 事件监听劫持
 () => patchHistoryListener() // history劫持
]
const patchersInSandbox = {
 [SandBoxType.LegacyProxy]: [...basePatchers, () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter)],
 [SandBoxType.Proxy]: [...basePatchers, () => patchStrictSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter)],
 [SandBoxType.Snapshot]: [...basePatchers, () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter)]
}

Идея захвата таймера состоит в том, чтобы переписатьsetIntervalметод сбора текущего приложенияwindowтаймераid, который сбрасывает таймер при удалении текущего приложения. дляwindowВ способе мониторинга тоже та же идея, указать при выполненииproxyизwindow.

Для таблиц стилей это сделать сложнее, когдаdomВыгруженные стили удаляются, если они не вставлены динамически.cssЕго нужно собрать, когда он вот-вот выйдет, и в следующий разmountпора восстанавливать. Динамически вставленные таблицы стилей будут перехвачены и вставлены в свою собственную область видимости.

Интересно перестроить таблицу стилей, еслиHTMLStyleElementОн не вставляется в документ и не может быть прочитанsheetда, вернисьnull. Если вставить в документlink, не может быть прочитан напрямуюlinkизcssRule, он сообщит об ошибке, вам нужно поставитьlinkПревратиться вstyle.

fetch(temp1.sheet.href)
 .then((res) => res.text())
 .then((res) => {
  const styleElement = document.createElement('style')
  //console.log(res)
  styleElement.appendChild(document.createTextNode(res))
  document.body.appendChild(styleElement)
  console.log(styleElement.sheet.cssRules) // 拿到css rules可以重建
 })

для вставки вbodyизjsбудет заблокирован сfetchуточняется после приобретенияproxy window. таких надо заблокироватьappendChildметод, который также необходимо обрабатыватьremoveChild,НапримерReact.createPortalсозданныйstyle, вставленный в подприложениеdivв контейнере, но при выгрузкеReactбудет отbodyНайдите его внутри, так что с ним еще нужно разобратьсяremoveChild, оцените подконтейнер и удалите егоstyle.

Суммировать

В дополнение к разделению приложений я ищу несколькоhtmlВ дополнение к неприятным ресурсам хостинга, следует сказать, что другие затраты очень низкие, набор хороших обобщенных решений.

image.png

Если у вас есть какие-либо вопросы, добро пожаловать, чтобы обсудить утку ~ Теплый прием о (^▽^)┛ Поднимите когти ღ( ´・ᴗ・` )