PlatformContext
PlatformContext — объект, который оболочка внедряет, чтобы описать платформу коду, отрисовывающемуся внутри неё: кто вошёл, какая организация, какое здание, локаль и тема, плюс два типизированных клиента.
Прочитайте это, прежде чем использовать его в федеративном модуле
PlatformContext передаётся через React Context, а React Context не пересекает границу Module Federation — это работало бы, только если бы @tv/extension-sdk был общим синглтоном и в хосте, и в каждом remote, а сегодня это не так.
Поэтому внутри федеративного модуля usePlatformContext(), useBuilding(), useCurrentUser() и useOptionalBuilding() бросают исключение — даже при том, что Building OS действительно отрисовал <PlatformProvider> вокруг вашего Shell. На этапе сборки ничего не предупредит: вы узнаете об этом при монтировании.
| Где выполняется код | Что использовать |
|---|---|
Федеративный модуль (ваш ./Shell и всё под ним) | useSelectedBuildingId(), useBuildingSelection() из @tv/extension-sdk/react; вне React — readBuildingSelection(), subscribeBuildingSelection() из @tv/extension-sdk/context; для собственных запросов к API — getAccessToken() / authorizationHeader() из @tv/extension-sdk/context (ниже); для чтения и записи графа — createBuildingGraphClient() из @tv/extension-sdk/graph поверх createPlatformApiClient() из @tv/extension-sdk/context; для событий платформы — usePlatformEvent() из @tv/extension-sdk/react (События); последним двум нужен SDK 1.18.0 или новее |
| Код внутри бандла оболочки | хуки PlatformContext, описанные ниже |
| Ваши тесты | хуки ниже — <PlatformProvider> вы отрисовываете сами, поэтому границы не существует |
Дальше страница описывает сам объект контекста. Всё это верно для кода внутри оболочки и для тестов — примеры с хуками относятся именно к этим двум местам.
Форма
interface PlatformContext {
user: PlatformUser; // кто вошёл + его роли в realm
organization: PlatformOrganization; // организация + её лицензированные модули
building: PlatformBuilding | null; // активное здание — null на уровне организации
locale: string; // "en", "ru", "hi"
theme: 'light' | 'dark';
api: PlatformApiClient; // уже аутентифицированный HTTP-клиент в пределах организации
eventBus: EventBusClient; // публикация / подписка на события платформы
}Обратите внимание: building может быть null — пользователь может находиться на уровне организации, не выбрав здание. Используйте useBuilding(), когда маршрут действительно требует здания, и useOptionalBuilding(), когда нет.
Поверхность намеренно узкая: пользователь, организация, выбор здания, локаль и тема, два типизированных клиента — вот и весь контракт. Всё, до чего вы дотягиваетесь помимо него, находится вне поддержки платформы и может измениться без предупреждения.
Хуки
import {
usePlatformContext,
useBuilding,
useCurrentUser,
useOptionalBuilding,
} from '@tv/extension-sdk/react';
function MyComponent() {
const ctx = usePlatformContext(); // весь контекст; бросает исключение вне провайдера
const building = useBuilding(); // бросает исключение, если здание не выбрано
const user = useCurrentUser(); // вошедший пользователь
const maybe = useOptionalBuilding(); // null вместо исключения
}Ререндеры — сколько стоит подписка
PlatformContext — настоящий React-контекст, поэтому применимо стандартное опасение: при смене значения провайдера перерисовываются все подписчики. Здесь оно ограничено самой конструкцией:
- Identity значения стабильна — оболочка держит один объект контекста и заменяет его только при смене вошедшего пользователя, выбранного здания, локали или темы. Всё это редкие, инициированные пользователем моменты; первые три и так обесценивают всё, что модуль отрисовал.
- Переключение темы — точечный патч, а не пересборка. Оболочка обновляет поле
themeв объекте контекста, сохраняя всё остальное: подписчики перерисовываются один раз с новым значением, соединения не рвутся. Модулям, которые красятся через CSS-переменные,ctx.themeвообще не нужен; читайте его только для поверхностей, рисуемых из JS (canvas, материалы Three.js). - Высокочастотные данные через значение контекста не текут. Телеметрия и события платформы приходят в обработчик, который вы передаёте в
usePlatformEvent()из@tv/extension-sdk/react, а вне React — вsubscribePlatformEvent()из@tv/extension-sdk/context(События) — шквал событий перерисует только компоненты, чьё состояние вы сами обновили в обработчике, а не всех подписчиков контекста. - У
usePlatformEvent()нет списка зависимостей. Он всегда вызывает последний переданный обработчик и переподписывается только при смене типа события, поэтому обработчик, объявленный прямо в компоненте, ничего не «дёргает». apiсохраняет identity при патче темы и заменяется только при полной пересборке, поэтому указывать его в зависимостях хуков корректно и ничего не «дёргает».
Если профилировщик показывает шторм ререндеров в вашем модуле, причина — в вашем собственном управлении состоянием ниже обработчика, а не в контексте.
Клиент api
ctx.api — это HTTP-клиент, который уже несёт аутентификацию пользователя и ограничен активной организацией. Вы никогда не видите токен.
В федеративном модуле ctx.api, как и весь контекст, недоступен. Там вызывайте API сами, как показано в разделе Вызов API из модуля.
// /spaces отвечает страницей: { data, total, limit, offset }
const spaces = await ctx.api.get<{ data: Space[]; total: number }>(`/api/v1/buildings/${building.id}/spaces`);
await ctx.api.post(`/api/v1/buildings/${building.id}/faults`, dto);Доступны get / post / put / patch / delete, каждый принимает необязательный { headers, query, signal }.
Почему это важно: один и тот же компонент работает и в вашей песочнице, и у клиента в продакшене, потому что единственное, что между ними меняется — аутентификация и базовый URL, — поставляет платформа, а не ваш код.
Чтение данных организации
ctx.organization.activeModuleIds показывает, на какие модули у организации есть лицензия. Используйте это для мягкой деградации UI, а не как средство безопасности: проверка на клиенте — удобство, а не граница. Граница должна быть на сервере, и строить её вам. @RequiresLicense() из @tv/extension-sdk/nestjs только записывает в метаданные, какая лицензия нужна маршруту; пока при поступлении запроса её никто не проверяет.
const { organization } = usePlatformContext();
const hasCafm = organization.activeModuleIds?.includes('@tv/module-cafm');Как следить за выбранным зданием из модуля
В Building OS ровно один селектор здания — тот, что в шапке, — и до федеративных модулей он доходит через глобальный канал, а не через React Context. Поэтому следующее работает по обе стороны границы:
import { useSelectedBuildingId, useBuildingSelection } from '@tv/extension-sdk/react';
const buildingId = useSelectedBuildingId();
// null = здание ещё не выбрано: оболочка грузится или вы работаете автономно.
const { buildingId, buildingIds, isAllBuildings, buildings } = useBuildingSelection();
// когда пользователь выбрал несколько зданий, а не одноВне React ту же задачу без привязки к фреймворку решают readBuildingSelection() и subscribeBuildingSelection() из @tv/extension-sdk/context.
Два правила, которые не про стиль:
- Для
nullрисуйте явное пустое состояние. Никогда не подставляйте другое здание как запасной вариант: названия зданий повторяются между организациями, и так вы молча покажете данные одной организации под именем другой. - Не добавляйте второй селектор здания. Селектор оболочки группирует здания по организации-владельцу, чего локальный список голых названий не умеет, — а два селектора начинают наглядно противоречить друг другу, стоит тронуть любой.
Вызов API из модуля
Модуль подписывает свои запросы токеном вошедшего пользователя, который оболочка выдаёт через @tv/extension-sdk/context (SDK 1.15.0 и новее):
import { authorizationHeader } from '@tv/extension-sdk/context';
async function fetchSpaces(buildingId: string) {
const res = await fetch(`/api/v1/buildings/${encodeURIComponent(buildingId)}/spaces`, {
headers: authorizationHeader(), // { Authorization: 'Bearer …' } или {}, если вход не выполнен
});
if (!res.ok) throw new Error(`spaces: HTTP ${res.status}`);
return (await res.json()) as { data: unknown[]; total: number; limit: number; offset: number };
}- Вызывайте на каждый запрос и не кэшируйте токен. Оболочка незаметно обновляет его, и сохранённая копия устаревает.
getAccessToken()возвращает сам токен (илиnull), если нужен не заголовок. - Используйте относительный путь
/api/…. Везде это тот же origin, что и у оболочки: в продакшене веб-сервер оболочки передаёт/api/в tv-api, а в песочнице её хост направляет/apiв собственный tv-api песочницы. Не прописывайте адрес tv-api жёстко. - Списки возвращают страницу, а не массив.
/spacesи/elementsотвечают{ data, total, limit, offset }(/elementsсчитаетtotalтолько на первой странице);/storeysотвечает обычным массивом. Типизированный клиентcreateBuildingGraphClient()из@tv/extension-sdk/graphописывает их какPage<T>иElementsPage<T>. - Вне оболочки токена нет. В
pnpm devgetAccessToken()равенnull, аauthorizationHeader()—{}, поэтому реальные запросы получают 401; подменяйте их или запускайте модуль в песочной оболочке. - Если пришёл 401:
resolveAccessToken()возвращает{ token, source }.source: 'platform'означает, что токен дала собственная функция оболочки;'oidc-storage'— запасной путь для оболочек, в которых этой функции ещё нет. - Никогда не читайте
localStorage['access_token']и хранилище OIDC-библиотеки напрямую. Первое — устаревшая копия, которая уходит и уже выключена в песочных оболочках; второе — внутренний формат.
Начиная с SDK 1.18.0 это делает createPlatformApiClient(), а типизированный клиент графа работает поверх него. createPlatformApiClient() и PlatformApiError имеют уровень @experimental: параметры и поля ошибки могут измениться, а возвращаемый PlatformApiClient — стабильный контракт (Уровни стабильности):
import { createPlatformApiClient } from '@tv/extension-sdk/context';
import { createBuildingGraphClient } from '@tv/extension-sdk/graph';
const api = createPlatformApiClient(); // токен на каждый запрос, относительный путь /api
const graph = createBuildingGraphClient(api);
const building = await graph.buildings.get(buildingId); // этажи, счётчики, тип
const spaces = await graph.spaces.list(buildingId, { limit: 50 }); // { data, total, limit, offset }Любой другой эндпоинт — api.get<T>('/api/v1/…', { query }). Ответ не из 2xx выбрасывает PlatformApiError со status и телом ошибки tv-api в body. Типы ответов следуют OpenAPI-документу tv-api, который SDK поставляет как openapi.snapshot.json.
Не обходите его стороной
Если вы замечаете, что вручную разбираете ключи localStorage оболочки, собираете URL Keycloak или жёстко прописываете https://tv-api… — остановитесь. Такие пути не переживают переход между организациями и находятся вне того, что поддерживает платформа: когда оболочка меняет ключ хранилища, SDK это поглощает, а самописный парсер ломается молча.
Для каждого из этих случаев есть поддерживаемый вызов: API выбора здания выше, getAccessToken() из @tv/extension-sdk/context для аутентификации, resolveLocale() из @tv/extension-sdk/i18n для языка и createBuildingGraphClient() из @tv/extension-sdk/graph для графа здания.
→ Далее: Федерация и оболочка