Тема
Bootstrap и mount
Bootstrap — единственный публичный вход в UI remote. Это обычный TypeScript module, который экспортирует функцию mount(options).
mount получает всё необходимое для одного запуска remote и возвращает функцию очистки.
Минимальный контракт без Query Layer
Сначала полезно понять чистый UI lifecycle.
ts
export interface BillingBootstrapOptions {
// DOM-элемент, внутрь которого remote должен смонтировать React.
container: HTMLElement
}
export interface BillingRemoteModule {
// Host вызывает mount и получает disposer.
mount(options: BillingBootstrapOptions): () => void
}Реализация remote
ts
import { createRoot } from "react-dom/client"
import { BillingApp } from "./BillingApp"
import type { BillingBootstrapOptions } from "@portal/contracts"
export const mount = (options: BillingBootstrapOptions): (() => void) => {
// Remote не ищет элемент по глобальному id.
// Конкретный container выбирает host.
const root = createRoot(options.container)
root.render(<BillingApp />)
// Host вызовет эту функцию при смене route или shutdown.
return () => {
root.unmount()
}
}Вызов из host
ts
export const mountBilling = async (container: HTMLElement): Promise<() => void> => {
// Federation plugin разрешает этот динамический import.
const remote = await import("billing/bootstrap")
// Host явно передаёт container аргументом.
return remote.mount({ container })
}Этот этап проверяет только три вещи:
- host действительно загружает remote bundle;
- React remote монтируется в правильный container;
- disposer полностью выполняет unmount.
Это не обязательная отдельная production-версия. Это способ изолировать ошибки federation loader/React lifecycle от ошибок server-state интеграции.
Как это связано с router
Router решает, когда показать remote. Composition function делает остальную работу:
ts
// Упрощённый route handler host.
let unmountCurrentRoute: (() => void) | null = null
export const openBillingRoute = async (): Promise<void> => {
// Сначала удаляем UI предыдущего route.
unmountCurrentRoute?.()
const container = document.getElementById("route-root")
if (!container) {
throw new Error("route-root not found")
}
// Router вызвал composition function, а она вызвала remote mount.
unmountCurrentRoute = await mountBilling(container)
}Router не обязан хранить runtime или scope. Эти зависимости может замкнуть функция mountBilling из composition layer.
Добавление Query Layer
После понятного mount-контракта расширяем options:
ts
import type { IScopedQueryRuntime } from "@dubium/query-layer/mf"
export interface BillingBootstrapOptions {
container: HTMLElement
// Узкий transport contract только для billing feature.
api: BillingApi
// Ограниченные store factories одного mount.
scope: IScopedQueryRuntime
}Host создаёт оба объекта и передаёт их явно:
ts
const scope = runtime.createParticipantScope(billingRegistration)
const unmountUi = remote.mount({
container,
api: billingApi,
scope,
})
return () => {
// Сначала React перестаёт использовать stores.
unmountUi()
// Затем host освобождает все ресурсы participant.
scope.dispose()
}Как scope попадает в глубокие компоненты
Не передавайте scope через каждый React prop. Bootstrap создаёт domain stores или composition object один раз, а компоненты получают уже stores.
ts
export const mount = (options: BillingBootstrapOptions): (() => void) => {
// Scope и API используются только на composition boundary remote.
const billingStore = new BillingStore(options.scope, options.api)
const root = createRoot(options.container)
// UI получает предметный store, а не инфраструктурный scope.
root.render(<BillingApp store={billingStore} />)
return () => {
root.unmount()
billingStore.dispose()
}
}React Context допустим для набора domain stores, но root runtime всё равно не должен становиться глобальной переменной remote.
Контракт disposer
Функция, возвращённая mount, должна:
- вызвать
root.unmount(); - освободить domain stores remote;
- удалить собственные DOM/window listeners;
- остановить timers remote;
- быть безопасной при повторном вызове.
Participant scope освобождает host после unmount UI. Полный порядок: Lifecycle MF.