Красивый и радостный праздник закончился, плачу, иду к машине и изучаю взмах микро передних клаксонов~
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
. Не очень дружелюбное руководство.
Изменить префикс подсистемы времени выполнения
наших комплектных подсистемhtml
Путь импорта ресурсов такой же, как у Sauce.Если вы вставите его в родительский домен, вы не найдете ресурсы.Поэтому вам нужно заменить абсолютный путь, чтобы вытащить ресурсы по разным подсистемам.systemjs-webpack-interop
, сопоставьте файл конфигурации с именем проекта, измените\_\_webpack_public_path\_\_
(вебпак написан глобально, можете набрать сами,\_\_webpack_require\_\_也有
), конфигурация показана на рисунке
изоляция css, изоляция js, изоляция dom
Просто дляstyle
Можно добавить в тело скоуп, чтобы изолировать глобальный стиль, и очистить скоуп при выгрузке, но для jq проектов выбор селекторов все равно будет путаться.window
Недостаточно загрязнять методы, таймеры, глобальные переменные и т. д., полагаясь на ограничения кода.
Если вам лень менять, то идите сразу к qiankun
Большинство из вышеперечисленных проблем можно решить с помощью плагинов, ограничений кода и т. д. Однако для использования неизбежно требуется вторичная упаковка.Если у вас нет энергии повторять колесо, давайте посмотрим на исходный код qiankun.
Что цянькунь сделал для нас?
Посмотрите, что написано на официальном сайте
Если single-spa — это Lego, и для использования его нужно комбинировать, то qiankun — это автомобиль, который может работать на акселераторе 🚘. Эффект заключается в том, что его можно загрузить по запросу, приложение загружается по порядку, загрязнение в стиле переменного загрязнения также было решено, и мое приложение будет запускаться после нескольких строк резюме, удобно ~
Я не позволю вам не знать его обоснование
Начиная с нескольких вопросов, что такое счастливая планета~ Теперь я возьму вас на исследование~
Общедоступный путь среды выполнения для решения проблем с ресурсами подприложения
Прочитав документ, я начал подозревать две возможности, одна модификация\_\_webpack_require\_\_.e
, при загрузке файла измените путь ресурса выборки в соответствии с именем подприложения, но это не очень хорошо, чтобы вторгнуться в веб-пакет; другой - изменить время выполнения\_\_webpack_public_path\_\_
Глобальные переменные, затем следуйте коду, чтобы узнать.
Первый взгляд на использование
-
существует
src
Каталог добавленpublic-path.js
:if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
-
входной файл
main.js
Исправлять.
import './public-path';
Когда вы увидите использование, вы поймете, что оно используется для модификации__webpack_public_path__
образом, этот код вводит файл ввода,__webpack_public_path__
изменен на__INJECTED_PUBLIC_PATH_BY_QIANKUN__
,
Видно, что эта переменная присваивается в хуке жизненного цикла,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
тип ТСГлядя на название, я догадался, что этоregisterMicroApps
Введите запись appconfig, тогда проблема в том, как вы анализируете адрес, который я передаю?
Я изменил свою запись и позже сделал большой хитрый трюк.Это нормально, что скрипт в html тянут, но динамически импортированные картинки и куски не могут быть найдены.Вот почему.Тег 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',
},
Последовательная загрузка ресурсов
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
В дополнение к неприятным ресурсам хостинга, следует сказать, что другие затраты очень низкие, набор хороших обобщенных решений.
Если у вас есть какие-либо вопросы, добро пожаловать, чтобы обсудить утку ~ Теплый прием о (^▽^)┛ Поднимите когти ღ( ´・ᴗ・` )