Proxy API
Дочерние приложения и веб-компоненты общаются с хостом Wippy через среду выполнения прокси (proxy.js). Ваш код никогда не обращается к ней напрямую — вы импортируете именованные геттеры из @wippy-fe/proxy, тонкого синхронного фасада над ней. Один и тот же импорт работает для обеих поверхностей:
- Микрофронтенд-приложения (
view.page) работают внутри srcdoc-iframe, куда хост вставляетproxy.js. - Веб-компоненты (
view.component) работают как ESM-модули на странице хоста; хост предоставляет@wippy-fe/proxyчерез import map.
О том, как среда выполнения загружается в каждый контекст, см. Прокси и изоляция.
Инициализация
@wippy-fe/proxy экспортирует синхронные геттеры — host, api, on, config, state, ws, logger, sanitize, html, loadCss, loadWebComponent, loadByTagName, hostCss, define, classifyLink, installVueWarnSuppressor, addIcons, tailwindConfig. Импортируйте нужное и используйте напрямую. Нет никаких getWippyApi, instance и рукопожатия GetConfig/SetConfig, которого нужно было бы ждать.
Шаблон синхронных геттеров общий для микрофронтенд-приложений и веб-компонентов:
import { host, api, config, state, ws, logger } from '@wippy-fe/proxy'
host.navigate('/dashboard')
const agents = await api.get('/api/v1/agents') // api — это axios; await относится к HTTP-вызову, а не к получению `api`
const token = config.auth.token
Приложения в iframe и Web Fragment получают сведения о видимости жизненного
цикла через тему прокси @visibility. Прямые веб-компоненты — нет: используйте
useHostVisibility() или useHostVisibilityRefresh() из
@wippy-fe/webcomponent-vue либо эквивалентные API WippyElement.
Эти геттеры синхронны — host, api, on, config и т. д. доступны в тот момент, когда ваш код начинает выполняться. Хост вставляет дочернюю конфигурацию синхронно, до загрузки среды выполнения (и для приложений view.page, и для веб-компонентов view.component), поэтому среда выполнения инициализируется до выполнения вашего скрипта. Вам никогда не нужно await, чтобы получить геттер, и никакого рукопожатия GetConfig/SetConfig нет. Единственный await, который вы пишете, относится к реальной асинхронной операции (HTTP-вызов через api, чтение state и т. п.).
Один раз во время разработки загрузите import-map.json целевого релиза Web
Host и используйте каждый ключ его объекта imports как внешнюю зависимость
Rollup. Это касается и @wippy-fe/proxy; не поддерживайте список внешних
зависимостей из одного пакета или только из импортированных. Загружайте заново
только при смене тега Web Host или при добавлении зависимости, чтобы проверить,
может ли её точный спецификатор быть внешним:
// vite.config.ts (после сохранения загруженного ответа как import-map.json)
import { readFileSync } from 'node:fs'
const hostImportMap = JSON.parse(
readFileSync(new URL('./import-map.json', import.meta.url), 'utf8'),
)
export default defineConfig({
build: {
rollupOptions: {
external: Object.keys(hostImportMap.imports),
},
},
})
Типы TypeScript
Типы прокси — AppConfig, ProxyApiInstance, StateApi, ProxyWsApi и типы сообщений WebSocket — поставляются как ambient-декларации в @wippy-fe/types-global-proxy, а не как именованные экспорты какого-либо пакета. Добавьте пакет в types вашего tsconfig.json (или используйте triple-slash-ссылку), и они станут доступны глобально — без импорта:
// tsconfig.json
{ "compilerOptions": { "types": ["@wippy-fe/types-global-proxy"] } }
// AppConfig, ProxyApiInstance, … — ambient-глобальные типы; аннотируйте ими напрямую, без импорта:
function render(cfg: AppConfig) { /* … */ }
type HostApi = ProxyApiInstance['host'] // HostApi — это индексный тип, а не отдельный экспорт
Для перечисленных выше API прокси не существует import … from '@wippy-fe/shared'. @wippy-fe/shared несёт межпакетные типы и константы имён GLOBAL_*; начиная с 0.0.52 он также экспортирует рантайм-помощники для удерживаемых веб-компонентов
readWippyVisibility, setWippyVisibility и
WIPPY_VISIBILITY_ATTRIBUTE. Авторы прямых веб-компонентов обычно используют
useHostVisibility() или useHostVisibilityRefresh() из
@wippy-fe/webcomponent-vue; событие прокси @visibility остаётся каналом
iframe/Web Fragment.
Внутреннее устройство (не использовать)
Среда выполнения устанавливает несколько глобальных переменных для собственных нужд — window.$W, window.getWippyApi, window.initWippyApi и набор window.__WIPPY_*. Код приложений и компонентов никогда не должен их читать или переопределять. Вместо этого всегда идите через @wippy-fe/proxy. Они перечислены лишь для того, чтобы вы случайно их не затёрли — см. Прокси и изоляция § Внутреннее устройство.
@wippy-fe/proxy(описанный здесь) — это API, который использует ваш дочерний код. Собственный bootstrap хоста,initWippyApp(config, rootContainer?), монтирует весь Web Host на пути module-embed / фасада — код дочернего приложения никогда его не вызывает.
Конфигурация
config
Конфигурация дочернего приложения, доставленная хостом. Это обычный объект (не функция) — импортируется напрямую и готов к синхронному чтению. Новая документация ориентируется только на текущий контракт wippy-context-2.0.
import { config } from '@wippy-fe/proxy'
const token = config.auth.token
interface ChildAppConfig {
$schema: 'wippy-context-2.0'
auth: {
token: string
expiresAt: string
}
env: {
APP_API_URL: string
APP_AUTH_API_URL: string
APP_WEBSOCKET_URL: string
[key: string]: string | undefined
}
axiosDefaults?: Partial<AxiosDefaults>
routePrefix?: string
apiRoutes?: Record<string, string>
themeMode?: 'auto' | 'light' | 'dark'
theming: {
global?: {
customCSS?: string
cssVariables?: Record<string, string>
icons?: Record<string, unknown>
iconSets?: Record<string, Record<string, unknown>>
}
}
context: {
resourceId: string
resourceType: 'page' | 'artifact'
route?: string
[key: string]: unknown
}
selfPageId?: string
mountRoutes?: Record<string, string>
}
Для динамических страниц, если URL хоста — /c/page-id/something/else?foo=1:
config.context?.routeнесёт/something/else?foo=1.config.path— устаревшее поле совместимости из полезных нагрузок доwippy-context-2.0, его не следует использовать в новом коде.
Управление хостом
host
API взаимодействия с хостом (HostApi). Импортируется напрямую и используется синхронно.
import { host } from '@wippy-fe/proxy'
host.setThemeMode(mode) и host.getThemeMode()
Режим темы — это состояние хоста, переносимое в AppConfig. Переключайте его только через публичный API прокси:
import { host, on } from '@wippy-fe/proxy'
async function setThemeMode(mode: 'auto' | 'light' | 'dark') {
await new Promise<void>((resolve, reject) => {
const unsubscribe = on('@theme', (appliedMode) => {
if (appliedMode !== mode) return
unsubscribe()
const currentMode = host.getThemeMode()
if (currentMode !== mode) {
reject(new Error(`Theme propagation mismatch: ${currentMode}`))
return
}
resolve()
})
// Подписка до команды, чтобы быстрое событие распространения не потерялось.
host.setThemeMode(mode)
})
}
await setThemeMode('dark')
Допустимые режимы: auto, light и dark. auto следует настройке
операционной системы. Изменение применяется к хосту, записывается обратно в
AppConfig, транслируется в живые iframe страниц и веб-компоненты и передаётся
через вложенные контейнеры Wippy. Подписывайтесь на @theme, когда коду нужно
дождаться применённого состояния дочернего контекста. Освобождайте подписку при
размонтировании компонента.
Хост не владеет сохранением. Встраивающий фасад слушает событие смены темы хоста и сохраняет пользовательский выбор так, как описано в Сохранение темы.
Не добавляйте и не удаляйте классы w-theme-dark / w-theme-light, не
вызывайте внутренний applyThemeMode, не изменяйте хранилища AppConfig, не
синтезируйте сообщения прокси и не используйте window.getWippyApi. Это детали
реализации Web Host, а не API приложений или браузерных тестов. Рантайм-тесты
должны вызывать host.setThemeMode(), дожидаться распространённого события
@theme и проверять host.getThemeMode() перед снятием внешнего вида.
AppConfig — это транспорт от хоста к дочернему контексту; не изменяйте его
внутреннее хранилище и не полагайтесь на ранее импортированный снимок
конфигурации как на сигнал завершения.
Метода host.applyTheme() не существует.
host.startChat(agentToken, options?)
Открывает новую сессию чата, используя предоставленный стартовый токен агента.
host.startChat(agentToken: string, options?: { sidebar?: boolean }): void
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
agentToken |
string |
— | Токен, определяющий, какого агента запустить |
options.sidebar |
boolean |
false |
true открывает чат в правой боковой панели; false — в основной области |
host.startChat('my-agent-token') // Основная область
host.startChat('my-agent-token', { sidebar: true }) // Правая боковая панель
host.openSession(sessionId, options?)
Открывает существующую сессию чата по UUID.
host.openSession(sessionId: string, options?: { sidebar?: boolean }): void
host.openSession('abc-123-uuid', { sidebar: false })
host.navigate(url)
Запрашивает у хоста SPA-навигацию. Поддерживаемые шаблоны:
/c/<page-id>— переход к динамической странице/c/<page-id>/<sub-path>— динамическая страница с подпутём/chat/<session-id>— открыть сессию чата- Любой маршрут монтирования, занятый страницей с
mountRouteв её записи реестра
host.navigate(url: string): void
host.navigate('/c/my-page-id')
host.navigate('/chat/session-uuid')
host.navigate('/keeper')
Оговорка про управляемую вёрстку.
startChat,openSession,openArtifactиnavigateнацелены на стандартную совместимую оболочку (представление чата, правую панель и корневой маршрут). Приfe_mode = managedони по-прежнему отправляются, но не имеют встроенной поверхности отрисовки — вместо этого отображайте чат, артефакты и подмаршруты через объявленные панели. См. Многопанельная вёрстка § Что работает в каком режиме.
host.onRouteChanged(internalRoute, navId?) — низкоуровневая интеграция с роутером
Уведомляет хост об изменении внутреннего маршрута страницы. Хост обновляет адресную строку браузера, включая в неё маршрут дочернего приложения. Этот вызов обязателен — без него URL хоста остаётся на корне страницы, а кнопка «Назад» браузера не работает для дочерней навигации.
host.onRouteChanged(internalRoute: string, navId?: number): void
Переносимые Vue-приложения используют createAppRouter() из @wippy-fe/router; пакет владеет этим вызовом, соответствующей подпиской @history, нормализацией и подавлением эхо-циклов. Не собирайте эти части вручную в коде приложения. Этот метод остаётся документированным для авторов платформенных адаптеров и не-Vue-интеграций.
host.confirm(options) → Promise<boolean>
Показывает диалог подтверждения PrimeVue. Разрешается в true, если пользователь соглашается, и в false, если отклоняет или закрывает диалог.
host.confirm(options: LimitedConfirmationOptions): Promise<boolean>
const confirmed = await host.confirm({
message: 'Delete this item permanently?',
header: 'Confirm Delete',
icon: 'tabler:trash',
acceptLabel: 'Delete',
rejectLabel: 'Cancel',
acceptClass: 'p-button-danger',
})
if (confirmed) {
await api.delete('/api/v1/items/123')
}
host.toast(options)
Показывает всплывающее уведомление PrimeVue.
host.toast(options: ToastMessageOptions): void
severity |
Внешний вид |
|---|---|
success |
Зелёный |
info |
Синий |
warn |
Жёлтый |
error |
Красный |
host.toast({
severity: 'success',
summary: 'Saved',
detail: 'Your changes have been saved.',
life: 3000,
})
host.openArtifact(artifactUUID, options?)
Открывает артефакт в боковой панели или модальном окне.
host.openArtifact(
artifactUUID: string,
options?: { target?: 'sidebar' | 'modal' }
): void
Цель по умолчанию — 'sidebar'.
host.openArtifact('artifact-uuid-123', { target: 'modal' })
host.setContext(context, sessionUUID?, source?)
Отправляет контекстные данные в текущую сессию чата. Если ни одна сессия ещё не открыта, контекст ставится в очередь и применяется к следующей сессии, открытой через startChat или openSession. При необходимости ограничьте контекст конкретным UUID сессии или пометьте его дескриптором источника.
host.setContext(
context: Record<string, unknown>,
sessionUUID?: string,
source?: { type: 'page' | 'artifact', uuid: string, instanceUUID?: string }
): void
host.setContext({
currentPage: 'dashboard',
selectedItemIds: [1, 2, 3],
})
host.classifyLink(url) → LinkClassification
Классифицирует href как навигацию хоста, навигацию дочернего приложения, внешнюю ссылку или игнорируемую. Использует mountRoutes и routePrefix из дочерней конфигурации плюс встроенные сегменты системных маршрутов. Чистая функция — без побочных эффектов.
host.classifyLink(href: string): LinkClassification
interface LinkClassification {
kind: 'host-nav' | 'child-nav' | 'external' | 'ignore'
href: string
normalizedPath?: string
targetPageId?: string // задаётся, когда host-nav совпал с конкретным mountRoute
}
// Обработчик ссылок с учётом классификатора
import { host } from '@wippy-fe/proxy'
document.addEventListener('click', (ev) => {
const a = (ev.target as HTMLElement)?.closest('a')
if (!a) return
const cls = host.classifyLink(a.getAttribute('href') ?? '')
if (cls.kind === 'host-nav') {
ev.preventDefault()
host.navigate(cls.normalizedPath ?? cls.href)
}
// child-nav / external / ignore: пусть отработают существующие обработчики
})
Для Vue-приложений замените RouterLink из vue-router на RouterLink из @wippy-fe/router — он использует classifyLink внутри и совместим по props с настоящим RouterLink.
host.handleError(code, error)
Сообщает об ошибке хосту для централизованной обработки.
host.handleError(
code: 'auth-expired' | 'other',
error: Record<string, unknown>
): void
'auth-expired'— запускает поток повторной аутентификации хоста'other'— общая ошибка; логируется и при необходимости показывается пользователю
try {
await api.get('/protected-endpoint')
} catch (error) {
if ((error as any).response?.status === 401) {
host.handleError('auth-expired', error as Record<string, unknown>)
} else {
host.handleError('other', error as Record<string, unknown>)
}
}
host.logout()
Выполняет выход текущего пользователя и завершает его сессию.
host.logout(): void
host.bridge
Обмен сообщениями между родителем и потомком по каналам, когда страница встроена внутрь <w-iframe>. Полный протокол см. в Прокси и изоляция § Мост родитель-потомок.
// Отправка родителю без ожидания ответа
host.bridge.post(channel: string, payload?: unknown): void
// Запрос/ответ (разрешается возвращаемым значением обработчика родителя)
host.bridge.request<T>(
channel: string,
payload?: unknown,
options?: { timeoutMs?: number }
): Promise<T>
// Регистрация обработчика входящих сообщений от родителя
host.bridge.on(
channel: string,
handler: (payload: unknown) => unknown | Promise<unknown>
): () => void // возвращает функцию отписки
Если вы опустите options.timeoutMs, host.bridge.request() использует срок по умолчанию в 10 секунд (10000 мс). По истечении срока возвращённый промис отклоняется с Error, сообщение которого — Bridge request <id> timed out after <ms>ms. Запрос к каналу, для которого у родителя нет обработчика, отклоняется немедленно с No handler registered for channel "<channel>", а не ждёт истечения срока.
host.layout
Доступ к API управляемой вёрстки. Доступен только когда задан hostConfig.layout (то есть fe_mode = managed). Вне этого контекста host.layout.snapshot равен null, а вызовы изменений ничего не делают.
const layout = host.layout
// Чтение текущего снимка
if (layout.snapshot) {
console.log(layout.snapshot.activeBreakpoint) // 'default' | 'sm' | ...
console.log(layout.snapshot.panels) // карта определений панелей
console.log(layout.snapshot.layouts) // деревья панелей по контрольным точкам
}
// Подписка на изменения (свежий снимок передаётся в обработчик)
import { on } from '@wippy-fe/proxy'
on('@layout-change', (snapshot) => {
console.log(snapshot.activeBreakpoint)
})
// Изменения
layout.resizePanel('right', '40%')
layout.collapsePanel('nav')
layout.expandPanel('nav')
layout.movePanel('right', { relativeTo: 'main', position: 'after' })
layout.removePanel('right')
layout.updatePanel('right', { kind: 'page', id: 'chat-panel' }) // {kind,id} заменяет содержимое целиком
layout.updatePanel('right', { props: { artifactId: 'abc-123' } }) // {props} поверхностно сливается с существующими props
layout.addFloating('flap', {
kind: 'component',
tagName: 'w-right-flap',
position: { x: 0, y: 200 },
size: { width: 48, height: 80 },
dismissable: false,
})
layout.removeFloating('flap')
layout.closeModal('confirm-discard')
// Шина внутри вкладки
layout.broadcast('open-chat', { token: 'abc' }) // 1:N (отправитель исключён)
layout.send('right', 'open-chat', { token: 'abc' }) // 1:1 именованной панели
const off = layout.on('open-chat', ({ payload, sourcePanelId, targetPanelId }) => {
// обработка
})
off() // отписка
Полную модель управляемой вёрстки см. в Многопанельная вёрстка.
API
api
Предварительно настроенный экземпляр axios с:
- базовым URL из окружения развёртывания
- автоматической подстановкой
Authorization: Bearer <token>в каждый запрос
import { api } from '@wippy-fe/proxy'
const response = await api.get('/api/v1/users')
const result = await api.post('/api/v1/items', { name: 'New item' })
Загрузка файлов
import { api, on } from '@wippy-fe/proxy'
const formData = new FormData()
formData.append('file', file)
const abort = new AbortController()
const response = await api.post('/api/v1/uploads', formData, {
signal: abort.signal,
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (evt) => {
if (!evt.total) return
const pct = Math.round((evt.loaded * 100) / evt.total)
uploadProgress.value = pct
},
})
const uploadedUuid = response.data.uuid // { success: boolean, uuid: string }
// Отслеживание статуса обработки через WebSocket
on(`upload:${uploadedUuid}`, (msg) => {
// msg.data.status: 'uploaded' | 'completed' | 'error' | 'processing'
})
// Отмена загрузки в процессе
abort.abort()
Максимальный размер файла: 100 МБ.
Скачивание файлов
const response = await api.get('/api/v1/uploads/{uuid}/download', {
responseType: 'blob',
})
const url = URL.createObjectURL(response.data)
const a = document.createElement('a')
a.href = url
a.download = 'filename.pdf'
a.click()
URL.revokeObjectURL(url)
Получение информации о загрузках
// Постраничный список
const list = await api.get('/api/v1/uploads/list', {
params: { limit: 10, offset: 0 },
})
// list.data.uploads: Array<{ uuid, mime_type, size, status, meta: { filename } }>
// Одна загрузка
const upload = await api.get(`/api/v1/uploads/${uuid}`)
// upload.data: { uuid, mime_type, size, status, meta: { filename, content_sample? } }
Потоковая передача SSE
api прокси поддерживает потоки server-sent events через fetch-адаптер. Используйте это для потокенной генерации LLM, длительных потоков прогресса или любого ответа text/event-stream.
Не используйте нативный браузерный
EventSource— он не может добавлять собственные заголовки и потому не может нести токенAuthorization: Bearerпрокси.
import { api } from '@wippy-fe/proxy'
const abort = new AbortController()
const response = await api.post('/api/v1/agents/stream', { prompt: 'Hello' }, {
adapter: 'fetch', // обязательно — адаптер xhr по умолчанию буферизует всё тело
responseType: 'stream',
headers: { Accept: 'text/event-stream' },
signal: abort.signal,
})
const reader = (response.data as ReadableStream<Uint8Array>).getReader()
const decoder = new TextDecoder()
let buffer = ''
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
while (true) {
const sep = buffer.indexOf('\n\n')
if (sep === -1) break
const rawEvent = buffer.slice(0, sep)
buffer = buffer.slice(sep + 2)
const dataLines = rawEvent
.split('\n')
.filter(line => line.startsWith('data:'))
.map(line => line.slice(5).trimStart())
if (dataLines.length === 0) continue
const payload = dataLines.join('\n')
if (payload === '[DONE]') return
try {
const evt = JSON.parse(payload)
handleEvent(evt)
} catch {
handleText(payload)
}
}
}
} finally {
reader.releaseLock()
}
// Отмена потока
abort.abort()
Чтобы по умолчанию направлять все запросы через fetch-адаптер:
// В package.json → wippy.configOverrides или в window.__WIPPY_CONFIG_OVERRIDES__
{
"axiosDefaults": { "adapter": "fetch" }
}
Surface
Геометрия области, которую Web Host выделил этому приложению. Эта область обычно не совпадает с окном браузера — приложение может быть одной панелью из нескольких, — поэтому window.innerWidth и единицы viewport не подходят для расчёта размеров. Полный контракт см. в Переносимость surface, а рецепты преобразования — в Миграция surface.
host.surface.snapshot
Текущая геометрия, читаемая из тех же вычисленных пользовательских свойств, которые разрешает CSS приложения, — поэтому она не может разойтись с тем, что видят @container wippy-surface (…) и cqw.
const { contract, revision, engine, sizing, width, widthUnit, height, heightUnit } = host.surface.snapshot
| Поле | Тип | Примечания |
|---|---|---|
contract |
1 |
версия контракта |
revision |
number |
монотонная; растёт при изменении геометрии |
engine |
'iframe' | 'fragment' | 'host' |
host означает, что surface не выделен |
sizing |
'container' | 'content' |
|
width / widthUnit |
number |
полная ширина и 1% от неё в CSS-пикселях |
height / heightUnit |
number | null |
null при content-размерности — блочная ось действительно недоступна |
host.surface.onChange(listener) → () => void
Подписка на изменения геометрии. Возвращает идемпотентную функцию отписки, которую обязательно нужно вызвать при разрушении.
const off = host.surface.onChange((snapshot) => {
canvas.width = snapshot.width
})
host.surface.supports(capability) → boolean
if (host.surface.supports('block-size')) {
// блочная ось доступна (container-размерность)
}
Возможности: block-size и surface-scroll сегодня отвечают правдиво. registered-hit-testing, native-document-hit-testing и owner-visibility — зарезервированный словарь и всегда возвращают false.
Предпочитайте supports() ветвлению по engine — важно, доступна ли возможность, а не какой движок выполняет отрисовку.
host.surface.engine и host.surface.sizing
Сокращения только для чтения тех же значений из снимка. engine: 'host' означает, что код смонтирован непосредственно в документ хоста (или работает под автономным dev-прокси) без выделенного surface; снимок по замыслу сообщает width: 0 и sizing: 'content'.
engine — ненадёжная проверка того, «был ли выделен surface». Страница, встроенная через <w-iframe>/<w-artifact>, тоже не получает surface — вложенные встраивания отказываются от него, пока не появится поддержка вложенных surface, — и всё же сообщает engine: 'iframe' с width: 0. Когда это различие важно, проверяйте snapshot.width.
События
on(topic, handler) → () => void
on подписывается на события из слоя WebSocket хоста или на внутренние события прокси. Возвращает функцию отписки.
on(topic: string, handler: (event: unknown) => void): () => void
Темы состоят из сегментов, разделённых двоеточиями. * — подстановочный знак для одного сегмента. Шаблон должен иметь столько же сегментов, сколько тема, которой он соответствует.
import { on } from '@wippy-fe/proxy'
// Отписаться по завершении
const unsub = on('session:abc:message:*', (msg) => {
console.log(msg.data)
})
unsub()
Каждый вызов on() возвращает функцию отписки. Всегда вызывайте её при размонтировании компонента, чтобы избежать утечек. При выгрузке iframe оставшиеся подписки очищаются автоматически, но для компонентов, монтируемых и размонтируемых внутри долгоживущего iframe, явная очистка всё равно обязательна.
// Vue Composition API
import { onUnmounted } from 'vue'
const unsub1 = on('session:*:message:*', handler)
const unsub2 = on('artifact:*', handler)
onUnmounted(() => {
unsub1()
unsub2()
})
// Vanilla / веб-компонент
import { on } from '@wippy-fe/proxy'
class MyEl extends HTMLElement {
private unsubs: Array<() => void> = []
connectedCallback() {
this.unsubs.push(on('session:*:message:*', handler))
}
disconnectedCallback() {
this.unsubs.forEach(fn => fn())
this.unsubs = []
}
}
Встроенные темы
| Тема | Полезная нагрузка обработчика | Описание |
|---|---|---|
@history |
{ path: string } |
URL хоста изменился (SPA-навигация). Срабатывает, когда родитель проталкивает новый маршрут. |
@visibility |
boolean |
Изменилась видимость iframe/Web Fragment. Прямые веб-компоненты вместо этого используют типизированный контракт видимости хоста. |
@message |
Полное WS-сообщение | Все сообщения WebSocket. Внутренне подписывается на *, *:*, *:*:*, *:*:*:*. |
@state-error |
{ error: string, key?: string } |
Операция сохранения состояния не удалась (превышена квота, ошибка сериализации). |
@layout-change |
LayoutSnapshot |
Снимок управляемой вёрстки обновлён; свежий снимок передаётся в обработчик. Эквивалентно чтению host.layout.snapshot. |
@layout-breakpoint |
{ name: string, width: number } |
Активная контрольная точка управляемой вёрстки изменилась; name — новая точка, width — её порог (px). |
Шаблоны с подстановочными знаками
// Только страницы iframe/Web Fragment; прямые веб-компоненты используют useHostVisibility().
on('@visibility', (visible: boolean) => { /* показано или скрыто */ })
// Все сообщения в конкретной сессии
on('session:abc-123:message:*', (msg) => { /* ... */ })
// Все сообщения во всех сессиях
on('@message', (msg) => { /* ... */ })
// Темы, части которых содержат ':', должны быть закодированы
on(`session:${encodeURIComponent('id:with:colons')}:message:*`, handler)
@history перечислена для полноты протокола. Переносимые Vue-приложения должны позволить @wippy-fe/router подписаться на неё; не добавляйте второй обработчик на стороне приложения.
Многократная подписка на одну и ту же тему из одного фрейма безопасна. Прокси выполняет дедупликацию на уровне хоста. Каждый вызов on() всё равно получает собственный независимый хэндл отписки.
Состояние
state — межфреймовое хранилище ключ-значение
state предоставляет опосредованное хостом хранилище, переживающее уничтожение iframe. Состояние ограничено областью страницы или UUID артефакта; каждое приложение получает изолированное пространство имён.
Все методы принимают необязательный параметр { scope?: string } для переопределения области по умолчанию. Используйте scope, когда нескольким экземплярам одного компонента нужны отдельные корзины состояния.
Уникальность области: значения области передаются как есть сырым API
stateи должны быть глобально уникальными в пределах вашего приложения. Плагин@wippy-fe/pinia-persistавтоматически добавляет к пользовательским областям префикс@custom:, чтобы предотвратить коллизии с системными областями.
import { state } from '@wippy-fe/proxy'
// Запись (без ожидания результата; при превышении квоты срабатывает @state-error)
await state.set('filters', { search: 'john', status: 'active' })
// Чтение (возвращает null, если ключ не найден)
const filters = await state.get<{ search: string, status: string }>('filters')
// Удаление ключа
await state.remove('filters')
// Очистка всего состояния этой страницы
await state.clear()
// Чтение всего сразу (удобно для массовой гидратации)
const all = await state.getAll()
// Пользовательская область
await state.set('count', 42, { scope: 'my-widget-instance-1' })
const count = await state.get<number>('count', { scope: 'my-widget-instance-1' })
Сигнатуры методов:
state.get<T = unknown>(key: string, options?: { scope?: string }): Promise<T | null>
state.set(key: string, value: unknown, options?: { scope?: string }): Promise<void>
state.remove(key: string, options?: { scope?: string }): Promise<void>
state.clear(options?: { scope?: string }): Promise<void>
state.getAll(options?: { scope?: string }): Promise<Record<string, unknown>>
Рекомендуемый шаблон сохранения для iframe/Web Fragment — сохранять, когда страница уходит в фон, а не при каждом изменении. Прямые веб-компоненты используют useHostVisibility() для того же решения по жизненному циклу:
on('@visibility', async (visible) => {
if (!visible) {
await state.set('scrollY', document.documentElement.scrollTop)
await state.set('formData', currentFormData)
}
})
Ограничения: 2 МБ на страницу (в JSON-сериализованном виде, настраивается хостом через hostConfig.stateCache). Состояние живёт в памяти хоста — переживает перезагрузку iframe, но не полное обновление страницы браузера.
Интеграция с Pinia
Для Vue-приложений, использующих Pinia, @wippy-fe/pinia-persist автоматизирует сохранение:
import { createWippyPersist, preloadWippyState } from '@wippy-fe/pinia-persist'
const preloaded = await preloadWippyState()
const pinia = createPinia()
pinia.use(createWippyPersist(preloaded))
app.use(pinia)
Затем пометьте хранилища:
const useMyStore = defineStore('my-store', () => {
const filters = ref({ search: '' })
return { filters }
}, {
wippyPersist: true,
// или: wippyPersist: { pick: ['filters'], debounce: 500 }
})
WebSocket
ws
ws отправляет команды через WebSocket-соединение хоста. Ответы приходят через подписки на темы on().
ws.send(command)
Без ожидания ответа. Ответ не доставляется — сначала подпишитесь на нужную тему.
ws.send(command: WsCommand): void
import { ws, on } from '@wippy-fe/proxy'
on('session:my-session:message:*', (msg) => {
console.log('Response:', msg.data)
})
ws.send({
type: 'session_message',
session_id: 'my-session',
message_id: crypto.randomUUID(),
data: { text: 'Hello from child app' },
})
ws.sendWithResponse(command) → Promise<WsMessage>
Отправляет команду и ждёт соответствующего ответа сервера. Тайм-аут — 30 секунд.
ws.sendWithResponse(command: WsCommand): Promise<WsMessage>
const response = await ws.sendWithResponse({
type: 'session_open',
start_token: 'my-token',
})
console.log('Session opened:', response.data)
ws.sendCommand(sessionId, data)
Удобная обёртка для команд управления сессией.
ws.sendCommand(sessionId: string, data: { command: string, [key: string]: unknown }): void
ws.sendCommand('session-uuid', { command: 'stop' })
ws.sendCommand('session-uuid', { command: 'model', name: 'gpt-4' })
ws.sendCommand('session-uuid', { command: 'agent', name: 'my-agent' })
Логгер
logger
Структурированное логирование, пересекающее границы iframe. Логи идут от потомка к хосту и далее к родительскому сайту, где их обрабатывают транспорты (Sentry, Graylog, консоль). Контекст каждого потомка (resourceId, resourceType, глубина вложенности) автоматически прикрепляется к каждой записи лога.
Используйте logger вместо console.log/error для всего, что должно попадать в продакшн-мониторинг.
import { logger } from '@wippy-fe/proxy'
logger.debug('Component mounted', { pageId: 'abc' })
logger.info('User loaded page', { pageId: 'abc' })
logger.warn('Slow API response', { ms: 3200 })
logger.error('Failed to save', { endpoint: '/api/save' })
logger.captureException(error, context?)
Перехватывает и пересылает исключение. Необработанные ошибки (window.onerror, unhandledrejection) перехватываются автоматически, когда ProxyConfig.injections.errorCapture равен true.
try {
await riskyOperation()
} catch (error) {
logger.captureException(error, { operation: 'riskyOperation' })
}
Хлебные крошки и контекст
// Хлебные крошки прикрепляются к следующему исключению как отладочный контекст
logger.addBreadcrumb({ category: 'navigation', message: 'Navigated to /settings' })
logger.addBreadcrumb({ category: 'ui', message: 'Clicked Save button' })
// Постоянный контекст — прикрепляется ко всем последующим логам этого потомка
logger.setContext('user', { id: 'user-123', role: 'admin' })
// Теги — пары ключ/значение для фильтрации и поиска
logger.setTag('version', '1.2.0')
logger.setTag('feature', 'dashboard')
Веб-компоненты
loadByTagName(tagName, options?) → Promise<void>
Загружает и регистрирует соседний веб-компонент по имени его HTML-тега. Разрешается после срабатывания customElements.define — сразу после этого безопасно вызывать document.createElement(tagName). При успехе тег автоматически добавляется в список разрешённых sanitize.
import { loadByTagName } from '@wippy-fe/proxy'
await loadByTagName('wc-thread-picker')
await loadByTagName('wc-slow-pkg', { timeoutMs: 60_000 })
// Можно использовать сразу
document.body.appendChild(document.createElement('wc-thread-picker'))
options.timeoutMs переопределяет 30-секундный срок ожидания customElements.define по умолчанию после добавления скрипта. Выявляет зависшие или сломанные компоненты (404, ошибка разбора, отсутствие вызова define) как отклонение промиса, а не бесконечное ожидание.
loadWebComponent(componentId, tagName?) → Promise<void>
Загружает веб-компонент по идентификатору артефакта в реестре Wippy, а не по имени тега. Полезно, когда идентификатор реестра приходит из значения конфигурации или ответа бэкенда.
import { loadWebComponent } from '@wippy-fe/proxy'
await loadWebComponent('wippy.components:my-chart')
Загрузчик по сканированию DOM (<script type="wippy-components-loader">)
Для страниц, которым нужно несколько компонентов, прокси при инициализации сканирует такие теги скриптов и загружает каждую запись через loadWebComponent:
<script type="wippy-components-loader">
{ "wc-foo": "wippy.components:foo", "wc-bar": "wippy.components:bar" }
</script>
Поведение дедупликации и автоматического обновления списка разрешённых тегов то же, что у loadByTagName.
Утилиты
sanitize(html, options?) → string
Санитайзер HTML со списком разрешённого по умолчанию, ограниченный текущим контекстом прокси. Совмещает умолчания отрисовки чата (<p>, <a>, <code>, <table> и т. д.) со всеми тегами веб-компонентов, зарегистрированными в данный момент в этой среде выполнения.
import { sanitize, loadByTagName } from '@wippy-fe/proxy'
const safe = sanitize('<p>hi</p><script>alert(1)</script>')
// → '<p>hi</p>'
// После loadByTagName тег разрешается автоматически:
await loadByTagName('wc-thread-picker')
sanitize('<wc-thread-picker thread-id="42"></wc-thread-picker>')
// → '<wc-thread-picker thread-id="42"></wc-thread-picker>'
// Разовые дополнительные теги
sanitize(dialogBody, { extraTags: { 'iconify-icon': ['icon'] } })
sanitize перечитывает список разрешённых тегов при каждом вызове, поэтому теги, зарегистрированные после импорта, тоже подхватываются.
html.inject(sourceHtml, options) → Promise<string>
Применяет преобразование исходного HTML в srcdoc без монтирования элемента. Для обычного использования предпочтительнее <w-iframe>; применяйте это только при построении собственной инфраструктуры размещения.
import { html } from '@wippy-fe/proxy'
const processed = await html.inject(sourceHtml, {
baseUrl: 'https://example.com/app/',
resourceId: 'child-id',
resourceType: 'page',
route: '/initial',
})
Переопределения конфигурации
Страницы могут переопределять отдельные поля конфигурации, обращённые к дочернему контексту, без отдельного развёртывания. Форма переопределения по-прежнему использует customization ради совместимости, а хост проецирует эти значения в текущий результат theming.global дочернего контекста до того, как страница получит конфигурацию wippy-context-2.0.
Задание переопределений
Страницы реестра (рекомендуется): задайте meta.config_overrides в _index.yaml страницы. Хост включает их в ответ content API и вставляет автоматически.
Автономные пакеты: задайте wippy.configOverrides в package.json страницы.
Вручную / для тестов: задайте window.__WIPPY_CONFIG_OVERRIDES__ в теге <script>, выполняющемся до proxy.js.
window.__WIPPY_CONFIG_OVERRIDES__ = {
customization: {
cssVariables: {
'--p-primary': '#ff6b00',
},
customCSS: '.my-page-header { border-radius: 12px; }',
},
}
Правила слияния
| Поле | Поведение при слиянии |
|---|---|
cssVariables |
Заменяет значения хоста — страница задаёт собственную тему |
customCSS |
Заменяет значение хоста |
iconSets |
Сливается дополняюще |
axiosDefaults |
Глубокое слияние |
routePrefix |
Заменяется |
apiRoutes |
Глубокое слияние |
Каждый вложенный потомок, встраиваемый страницей — <w-iframe>, <w-artifact> и содержимое html.inject, — строится из уже слитой конфигурации страницы и наследует её автоматически, рекурсивно вниз по поддереву. Поэтому переопределения страницы (особенно оформление) распространяются на всё, что под ней, а не только на саму страницу.
Утилиты Vue
installVueWarnSuppressor(app)
Доступна в текущем согласованном семействе @wippy-fe/proxy. Заглушает [Vue warn]: Failed to resolve component: foo-bar для тегов, зарегистрированных через customElements.define(...), а не через app.component(...). Компилятор шаблонов Vue выдаёт эти предупреждения для незнакомых ему тегов веб-компонентов — элементы отображаются корректно, но консоль заполняется шумом.
import { installVueWarnSuppressor } from '@wippy-fe/proxy'
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
installVueWarnSuppressor(app)
app.use(router)
app.mount('#app')
Что он подавляет:
- Теги, уже зарегистрированные через
customElements.define(...)— системные теги (w-iframe,w-artifact,wippy-loading,wippy-error) и каждый тег, зарегистрированный конвейером автозагрузки (loadByTagName, сканер). - Теги, соответствующие форме имени пользовательского элемента (
^[a-z][a-z0-9]*-[a-z0-9-]*$), но ещё не зарегистрированные — это покрывает окно гонки, когда Vue отрисовывает до того, как подгрузился скрипт автозагрузки.
О чём он по-прежнему предупреждает:
- Опечатки в PascalCase-компонентах (
<UsreCard />). Подавитель не сопоставляет их с kebab-шаблоном, аcustomElements.getвозвращаетundefined, поэтому они проходят в консоль, сохраняя сигнал, который отличает настоящие ошибки от шума.
Функция идемпотентна: повторный вызов на том же app действительно ничего не делает. На app.config устанавливается маркер Symbol.for('@wippy-fe/proxy/vue-warn-suppressor-installed'); маркер экспортируется как VUE_WARN_SUPPRESSOR_INSTALLED_MARKER для тестовых окружений, которым нужно сбрасывать его между перезагрузками.
Если warnHandler уже был установлен, он сохраняется как previous и вызывается для предупреждений, которые подавитель не заглушает.
createAppRouter(routes, options?) из @wippy-fe/router
Канонический фабричный метод memory-роутера для srcdoc-подприложений. Заменяет шаблонный код, который сейчас дублирует каждое подприложение (memory history, синхронизация маршрута с хостом через afterEach, подписка на @history):
import { createAppRouter } from '@wippy-fe/router'
import { config } from '@wippy-fe/proxy'
import { routes } from './routes'
const router = createAppRouter(routes, {
initialPath: config.context?.route,
})
app.use(router)
Компоненты загрузки и ошибок
Два веб-компонента автоматически регистрируются через loading.js (вставляется до proxy.js). Импорты и ручная регистрация не нужны.
<wippy-loading>
Полноэкранный индикатор загрузки с цветами, учитывающими тему.
| Атрибут | Описание |
|---|---|
title |
Основной текст (например, "Loading...") |
subtitle |
Второстепенный текст |
no-bg |
Boolean — прозрачный фон для использования как оверлей |
<wippy-loading title="Loading..." subtitle="Please wait"></wippy-loading>
<wippy-loading no-bg title="Loading page content..."></wippy-loading>
<wippy-error>
Полноэкранное отображение ошибки с окраской по уровню серьёзности.
| Атрибут | Значения | По умолчанию |
|---|---|---|
title |
Любая строка | "Something went wrong" |
message |
Любая строка | (пусто) |
icon |
circle, triangle, sad |
circle |
severity |
danger, warning |
danger |
no-bg |
Boolean | (отсутствует) |
<wippy-error title="Failed to load" message="Server returned 500" severity="danger"></wippy-error>
<wippy-error title="Connection Lost" message="Retrying..." icon="triangle" severity="warning"></wippy-error>
Оба компонента используют Shadow DOM с CSS-переменными из @wippy-fe/theme и содержат зашитые запасные значения для контекстов без темы.
Рекомендуемый шаблон для обычных HTML-страниц:
<body>
<wippy-loading id="loader" title="Loading..."></wippy-loading>
<div id="content" style="display:none"><!-- содержимое --></div>
<script type="module">
import { api, host } from '@wippy-fe/proxy'
async function init() {
try {
// получить данные, подготовить страницу...
document.getElementById('loader').remove()
document.getElementById('content').style.display = 'block'
} catch (error) {
const errorEl = document.createElement('wippy-error')
errorEl.setAttribute('title', 'Initialization failed')
errorEl.setAttribute('message', error.message)
document.getElementById('loader').replaceWith(errorEl)
}
}
init()
</script>
</body>
Vue 3 — точка входа app.html:
<div id="app">
<wippy-loading title="Loading..."></wippy-loading>
</div>
<script type="module" src="./src/app.ts"></script>
Когда Vue монтируется в #app, он автоматически заменяет элемент <wippy-loading>.