Самый дешёвый способ превратить веб в мобильное приложение — не упаковывать его в native-обёртку, а сделать сам веб устанавливаемым. Progressive Web App — это обычный сайт, который браузер умеет установить на домашний экран, запустить без адресной строки и обслужить офлайн. Никакого магазина приложений, никакого ревью, та же кодовая база React + TypeScript. Для продукт-инженера это нижняя точка отсчёта мобильной стратегии: прежде чем тянуть Capacitor и native API, стоит понять, что веб умеет сам.
Что такое PWA
PWA — это веб-приложение, которое можно установить и которое работает офлайн. Не отдельный фреймворк и не формат сборки, а набор веб-возможностей поверх обычного сайта: декларация приложения (manifest), скрипт-прокси между страницей и сетью (service worker) и кеш ресурсов (Cache API). Сложенные вместе, они дают браузеру право предложить установку, а приложению — жить без сети. «Progressive» означает постепенность: на старом браузере это просто сайт, на современном — устанавливаемое приложение с офлайном и push.
Web app manifest
Manifest — это JSON-файл, который описывает приложение операционной системе: как назвать иконку, в каком окне открыть, с какого адреса стартовать. Подключается через <link rel="manifest" href="/manifest.webmanifest">.
{
"name": "Складской учёт",
"short_name": "Склад",
"start_url": "/",
"display": "standalone",
"theme_color": "#2F5B4F",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" }
]
}
name и short_name — полное и короткое имя (второе показывается под иконкой). start_url — адрес запуска (формально опционален: при отсутствии браузер берёт URL страницы, к которой подключён manifest, но для предсказуемой установки задавайте явно). display: standalone убирает адресную строку и кнопки браузера — приложение выглядит как native. theme_color красит системные элементы вокруг окна. icons — массив, где у каждого объекта обязателен src, а также sizes и type; 192 и 512 px — минимальный практичный набор.
Service worker и офлайн
Service worker — это скрипт, который браузер запускает отдельно от страницы и который перехватывает её сетевые запросы. Он работает как прокси: страница просит ресурс, service worker решает, отдать его из кеша или сходить в сеть. Это и есть механизм офлайна. У него три ключевых события жизненного цикла: install (срабатывает один раз после регистрации — момент наполнить кеш), activate (старые версии закрыты, можно подчистить устаревший кеш) и fetch (на каждый сетевой запрос страницы).
const CACHE = "app-v1";
const ASSETS = ["/", "/index.html", "/app.js", "/styles.css"];
self.addEventListener("install", (event: ExtendableEvent) => {
event.waitUntil(caches.open(CACHE).then((c) => c.addAll(ASSETS)));
});
self.addEventListener("activate", (event: ExtendableEvent) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
)
);
});
self.addEventListener("fetch", (event: FetchEvent) => {
event.respondWith(
caches.match(event.request).then((hit) => hit ?? fetch(event.request))
);
});
Cache API (caches.open, addAll, match) — это хранилище ответов, ключом к которому служит запрос. Регистрируют worker со страницы один раз:
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
Стратегия выше — cache-first: сперва кеш, потом сеть. Что и как хранить долговременно — данные, а не статику — разбирается в статье про офлайн и хранилища.
Установка на домашний экран
Чтобы браузер предложил установку, приложение должно отвечать критериям установимости: подключённый manifest, зарегистрированный service worker и отдача по HTTPS (для разработки годятся localhost и 127.0.0.1). При выполненных условиях Chromium-браузеры стреляют событием beforeinstallprompt — его можно перехватить, чтобы показать свою кнопку вместо стандартной подсказки.
let deferred: BeforeInstallPromptEvent | null = null;
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
deferred = e as BeforeInstallPromptEvent;
showInstallButton();
});
async function install() {
if (!deferred) return;
await deferred.prompt();
deferred = null;
}
После установки приложение получает свою иконку в лаунчере и запускается в окне без браузерного обвеса — по display из manifest.
Ограничения на iOS
iOS — главная причина, по которой PWA не всегда достаточно. Safari не показывает beforeinstallprompt: установка только вручную через «Поделиться → На экран „Домой"». Web Push на iOS появился лишь в 16.4 и работает только для PWA, добавленных на домашний экран, — в самом Safari push недоступен. Дальше — лимиты выполнения и хранилища: нет фонового выполнения (worker засыпает, когда приложение свёрнуто), часть Web API недоступна (Web Bluetooth, Web NFC и ряд интеграционных), а у script-writable хранилища есть квоты и политика вытеснения. Когда этих ограничений становится слишком много, веб упаковывают в native-обёртку — Capacitor даёт настоящие native push (см. push-уведомления) и доступ к платформенным API, которых PWA лишён.
Где это в UCP
PWA — это первый рубеж мобильной стратегии продукт-инженера: те же компоненты и данные frontend, но устанавливаемые и работающие офлайн, без затрат на native-сборку и магазины. Граница его возможностей — это и есть критерий решения: пока хватает manifest, service worker и Cache API, продукт-инженер остаётся на вебе; как только нужны фоновое выполнение или native push, наступает черёд Capacitor с его доступом к платформе.