Динамическая маршрутизация
Маршрутизатор Web Host не настраивается статически. При запуске он получает от бэкенда текущий набор маршрутов монтирования страниц и добавляет их в экземпляр Vue Router. Это означает, что новая запись view.page с заявкой mountRoute вступает в силу без каких-либо изменений в самом бандле Web Host.
Синхронизация маршрутов монтирования при запуске
Когда приложение Web Host инициализируется, до отрисовки какой-либо навигации оно вызывает:
GET /api/public/pages/routes
Ответ — это конверт { success, count, routes }, где routes — отображение шаблона маршрута монтирования в id страницы (сюда входят скрытые/необъявленные страницы, которые всё же заявляют URL). Для каждой записи хост регистрирует маршрут Vue Router, сопоставляющий объявленный путь с компонентом загрузчика страницы, добавляя его как дочерний к родительскому маршруту 'app'.
// Упрощённо из bootstrap Web Host
const { routes } = await api.get('/api/public/pages/routes')
for (const [mountRoute, pageId] of Object.entries(routes)) {
router.addRoute('app', {
path: mountRoute,
component: MountRoutePage,
props: () => ({ pageId }),
})
}
После этого переход на /home/anything заставляет маршрутизатор отрисовать iframe страницы main, а переход на /demo/anything — отрисовать iframe страницы iframe-demo, без какого-либо жёстко зашитого знания об этих путях в бандле хоста.
Заявка на путь через mountRoute
Запись view.page заявляет путь в маршрутизаторе хоста, устанавливая mountRoute в блоке meta своего _index.yaml:
- name: main
kind: registry.entry
meta:
type: view.page
mountRoute: /home/:part(.*)*
...
mountRoute — текущее написание для совместимости с ошибкой регистра на бэкенде.
Предполагаемый ключ бэкенда — mount_route; продолжайте писать mountRoute,
пока исправление бэкенда не будет выпущено.
mountRoute принимает только формы «поймай всё»: /:part(.*)* (корень) или /<literal-prefix>/:part(.*)*, где префикс — один или несколько литеральных сегментов из строчных букв, цифр и дефисов, заканчивающихся обязательным подстановочным :part(.*)*. Произвольные шаблоны Vue Router — именованные параметры, пользовательские регулярные выражения или другие имена параметров (например, /home/:id, /users/:userId(\d+)) — отклоняются: хост поднимает конфликт маршрута монтирования вида syntax, validate_mount_route_syntax на бэкенде завершается ошибкой, а GET /api/public/pages/routes возвращает HTTP 500 (отображается как фатальная полноэкранная ошибка). Подстановочный сегмент :part(.*)* позволяет дочернему приложению управлять собственными подмаршрутами (например, /home/settings, /home/profile/edit), пока хост владеет префиксом /home.
Две записи не должны заявлять один и тот же маршрут. Если две записи view.page заявляют один и тот же mountRoute, валидатор бэкенда (validate_mount_routes в page_registry.lua) фиксирует конфликт дублирующегося маршрута в том же списке проблем, что и синтаксические ошибки, поэтому GET /api/public/pages/routes возвращает HTTP 500, а Web Host отрисовывает фатальный полноэкранный <wippy-error> — ровно как при некорректном mountRoute. Это не игнорируется молча.
Единственное поведение «побеждает первый» — приоритет во время выполнения Vue Router между корневым «поймай всё» (/:part(.*)*) и более конкретным системным маршрутом (chat, c, web, page, keeper, login, logout) или монтированием с более длинным литеральным префиксом: более конкретный маршрут срабатывает первым. Это приоритет разрешения маршрутов, а не обработка дублирующихся маршрутов.
Цикл синхронизации URL
Как только страница загружена в свой iframe, дочернее приложение навигирует внутри себя собственным маршрутизатором. Эти внутренние переходы должны отражаться в адресной строке хоста, чтобы кнопка «назад» браузера, закладки и копирование URL работали корректно. Это делается через пару PostMessage.
Дочернее приложение → хост: CmdRouteChanged
Когда маршрутизатор дочернего приложения фиксирует переход (например, пользователь переходит с /home/settings на /home/profile), дочернее приложение отправляет сообщение в родительское окно:
// В дочернем приложении, при смене внутреннего маршрута.
// Код приложения никогда не должен отправлять эти сообщения напрямую — используйте proxy API:
import { host } from '@wippy-fe/proxy'
host.onRouteChanged('/profile', navId) // только внутренний маршрут; хост сам добавляет префикс монтирования. navId — необязательное число
Прокси сериализует это во внутренний конверт протокола. Этот протокол не является API приложения: не копируйте его и не вызывайте window.parent.postMessage напрямую.
Обработчик сообщений хоста перехватывает это, вызывает router.push(path) для обновления адресной строки через SPA-переход (добавляя запись в историю браузера) без полной перезагрузки страницы, а затем отправляет обратно:
Хост → дочернее приложение: UrlWasUpdatedInParent
После того как хост обновил адресную строку, прокси генерирует @history для дочернего приложения. @wippy-fe/router потребляет это событие и согласует memory router.
Хост отправляет обратно внутренний маршрут дочернего приложения (подпуть после префикса монтирования), а не полный путь хоста, поэтому круговой обмен симметричен: дочернее приложение отправляет internalRoute: '/profile', хост устанавливает адресную строку в /home/profile и отражает обратно path: '/profile', который memory router дочернего приложения выполняет дословно. Дочернее приложение слушает канал событий @history и трактует это как подтверждение того, что URL хоста теперь согласован с его внутренним состоянием.
Круговой обмен держит адресную строку хоста, маршрутизатор дочернего приложения и запись в истории браузера в синхронном состоянии, при этом хосту не нужно ничего знать о внутренней структуре маршрутизации дочернего приложения.
classifyLink
Когда у страницы установлено preventLinkClicks: true в её proxy-инъекциях (см. view.page), хост перехватывает клики по <a> внутри iframe до того, как их обработает браузер. Каждая перехваченная ссылка передаётся в classifyLink, который решает, как её обработать:
LinkKind |
Условие | Действие |
|---|---|---|
host-nav |
Верхний сегмент пути совпадает с известным литералом mountRoute, встроенным системным маршрутом (chat, c, web, page, keeper, login, logout) или корневым «поймай всё» |
preventDefault + host.navigate(normalizedPath) |
child-nav |
Собственный маршрутизатор iframe разрешает путь в реальный (не «поймай всё») маршрут либо никто другой его не заявил | RouterLink подприложения решает внутри приложения; хост НЕ вызывает preventDefault и НЕ перезагружает iframe |
external |
Другой origin или схема не http (javascript/mailto/tel/sms/ftp/file/data/blob) |
Поведение браузера по умолчанию (например, открытие в новой вкладке) |
ignore |
Пустой href или чистый якорь (#…) |
preventDefault |
Классификатор сначала проверяет собственный локальный маршрутизатор iframe, поэтому ссылка, которую дочернее приложение может разрешить само, остаётся внутри приложения.
classifyLink обращается к тому же списку маршрутов, что был получен при запуске. Ссылка на /demo/step-2 классифицируется как host-nav, потому что /demo/:part(.*)* — зарегистрированный маршрут монтирования: хост переходит на страницу iframe-demo вместо полной перезагрузки страницы.
Это означает, что дочернему приложению не нужно знать о других страницах системы. Оно может отрисовывать обычные ссылки <a href="/demo/step-2">, а классификатор ссылок хоста корректно обработает переход.