Глава 50 / 51
Том 5 · Расширения, эксплуатация и практика

webview и узел app: превращаем сайт в настольное приложение

Всё, что дали предыдущие сорок девять глав, — это «сайт»: открыть браузер, ввести адрес. Эта глава даёт ему окно: сервисный узел class: "app" = локальный HTTP-сервис + окно WebView2. Достаточно конфигурации, деградация автоматическая, а вместе с xs pack получается одиночный exe «по двойному щелчку».

Введение

Место главы: финал настольной формы (домашняя сцена 15-го расширения webview). Главная ментальная модель одной фразой — окно это слой отображения, сервис — суть: узел app не шестой протокол, а «фронтендизация» http-узла. host_default / hosts / static / TLS / горячая перезагрузка значат то же самое; добавляются лишь дескриптор window и жизненный цикл фронта.

50.1 Первое настольное приложение

Возьмите сервисный узел любого готового сайта, поменяйте класс на class: "app" и добавьте дескриптор window:

Пример конфигурации · узел app
{
  "enabled": true, "class": "app", "name": "main",
  "ip": "127.0.0.1", "port": 0,
  "host_default": { "name": "app", "path": "wwwroot", "devlang": "c",
                    "devfile": "script/http_main.c" },
  "window": {
    "title": "Моё приложение", "width": 1280, "height": 800,
    "url": "/", "devtools": false,
    "close": "stop", "install_fallback": "ask"
  }
}

Запустите xs: сначала появляется окно загрузки (заглушка до готовности сервиса); когда локальный HTTP готов, автоматически открывается главный интерфейс по url. Обратите внимание на два отличия от класса http: ip по умолчанию 127.0.0.1 (привязка к loopback не вызывает диалог брандмауэра Windows) и port допускает 0 (автоназначение, см. 50.3). Постатейные ограничения девяти полей window (включая min_width / min_height и таблицу значений install_fallback) — в справочнике полей xs.json.

Вариант сборки должен включать расширение webview: build.bat webview (или all); вариант без расширения по-прежнему разбирает конфигурацию — просто без окна (см. матрицу в 50.4).

50.2 Жизненный цикл и семантика close

  • Запуск: сперва окно загрузки (закрывает паузу инициализации сервиса) → сервис готов → автоматический переход на главный интерфейс
  • close: "stop" (по умолчанию): закрытие окна пользователем = плавная остановка по той же цепочке, что Ctrl+C — не убийство процесса
  • close: "hide": закрытие лишь скрывает окно, сервис продолжает работать. Повторный запуск того же приложения будит существующее окно через обнаружение единственного экземпляра (именованный mutex, производный от пути exe), а новый процесс сразу завершается. В v1 нет значка в трее; в режиме hide остановка идёт по сигнальному пути
  • Изоляция сбоев: рендерер WebView2 — отдельный процесс, падение страницы не заражает xs; любой сбой потока окна деградирует в строку лога и никогда не уводит сборку двигателя на путь fail

50.3 Автоназначение port: 0

Порт настольного приложения не должен быть виден пользователю и не должен конфликтовать с другими — port: 0 отдаёт выделение ОС атомарно (разрешено только классу app, остальные fail-fast):

  • Считывание: после успешной привязки фактический порт попадает в XS_ServerInfo.PortBound (Port также перезаписывается, идентичность конечной точки совпадает со скриптовым представлением); фронт собирает URL именно из него
  • Из скрипта: xsServerFind("main")->PortBound, отрисовать в страницу, чтобы фронт знал, кто он (например адрес WS)
  • Прилипание при горячей перезагрузке: таблица прилипания по имени сервиса (живёт сквозь поколения) при смене поколения явно привязывает старый порт — открытые окна ничего не замечают; занятый прилипший порт — кандидат проваливается и чистится по обычному пути отказа
  • Граница: таблица прилипания не сохраняется на диск. Для стабильного порта между перезапусками задайте фиксированный порт (особенно когда внешние системы ходят к вам обратными вызовами)

50.4 Матрица деградации и выпуск

Вариант сборкиПлатформаWebView2Поведение
расширение webviewWindowsустановленполная функциональность окна
расширение webviewWindowsотсутствуетпо install_fallback: ask спрашивает / silent ставит молча / headless сразу деградирует; любая неудача — headless
расширение webviewLinux—headless, адрес доступа печатается в лог
без расширениялюбая—то же headless плюс строка-уведомление; конфигурация разбирается как обычно

Комбинация выпуска: xs pack (глава 48) упаковывает сайт в бинарник + xsw.exe (вариант GUI-подсистемы без консоли, идёт в релизном пакете) — продукт представляет собой одиночный exe «по двойному щелчку»: окно загрузки → интерфейс → закрытие-в-остановку, чёрной консоли нет ни на одном шаге. Ноль DLL времени выполнения: таблица импорта — только системные DLL; WebView2Loader.dll находится в рантайме в каталоге установленного Runtime, отсутствие Runtime покрывает install_fallback.

Типичные ошибки

Грабли: после перезапуска порт изменилсяПрилипание port:0 живёт, пока жив процесс (сценарий горячей перезагрузки); между перезапусками каждый запуск получает новое значение. Когда внешние системы должны обращаться к вам или пользователь сохраняет адрес в закладки — задайте фиксированный порт.
Грабли: ручная замена ip на 0.0.0.0 вызывает диалог брандмауэраКласс app по умолчанию 127.0.0.1 именно затем, чтобы привязка к loopback не вызывала окно авторизации брандмауэра Windows. Не возвращайте 0.0.0.0, если настольному приложению не нужен внешний приём.
Грабли: на Linux окно не появилосьЭто проектное поведение, не баг — оконный бэкенд webview только Windows; Linux автоматически уходит в headless. Адрес берите из строки лога open http://… in a browser. Одна и та же xs.json переносима между платформами.
Грабли: в релизе включён devtoolswindow.devtools по умолчанию false; в разработке удобно, перед выпуском выключите — пользователь по F12 увидит ваши интерфейсы и структуры данных.

Упражнения

База
Окно узла app на статическом сайте из тома 2; пройдите close: stop затем close: hide (включая пробуждение при втором запуске).
Продвинутый
Запуск с port: 0, чтение PortBound из скрипта через xsServerFind и отрисовка в страницу (например адрес подключения WS).
Зачёт
xs pack в одиночный exe, скопируйте на машину без WebView2 Runtime и проверьте запрос install_fallback: "ask", затем смените на "headless" и проверьте прямую деградацию.

Итоги

class: "app" = фронтендизированный http (та же конфигурация + 9 полей window) · загрузка → готовность → открытие URL → закрытие-в-остановку = жизненный цикл · port:0 + PortBound = невидимый порт (для стабильности между перезапусками — фиксированный) · матрица деградации = окно по возможности, сервис прежде всего · xs pack + xsw.exe = настольный exe по двойному щелчку