Установленное приложение может глубже врастать в систему, но каждая такая возможность - отдельный capability, который где-то есть, а где-то нет. Правило одно: feature detection и осмысленный fallback. Проверяем наличие API, при отсутствии тихо деградируем до базового поведения. Наивная альтернатива - решить, что раз работает в вашем Chrome, значит работает у всех, - разбивается о зоопарк браузеров.
Несколько возможностей расширяют точки входа. shortcuts в манифесте дают быстрые действия из контекстного меню иконки - например, Новая заметка сразу на нужном экране. Share Target позволяет принимать текст и файлы из системного share sheet, будто это нативное приложение: пользователь делится статьёй - и она падает новой заметкой. Protocol handlers регистрируют схемы ссылок, чтобы они открывались в установленном PWA. Каждая из этих вещей - усиление, а не фундамент.
Badging рисует ненавязчивое число - количество несинхронизированных или непрочитанных элементов - прямо на иконке, если API поддержан. Web Push уместен только после явной пользы и разрешения, и payload не должен нести секретов, ведь он проходит через сторонний сервис доставки. Медиазапрос display-mode: standalone адаптирует UI к фактическому режиму. Важная оговорка про iOS: там Web Push доступен для приложений, добавленных на домашний экран, но Background Sync переносимым считать нельзя - планируйте foreground-досыл из прошлых глав.
В standalone-режиме исчезает адресная строка и часть браузерной навигации. Не рисуйте собственную кнопку Back, если ваша навигационная модель без неё ломается, - лучше опереться на историю и жесты платформы. Учитывайте safe areas на устройствах с вырезами (env(safe-area-inset-*)), возможный title bar overlay на десктопе и экранную клавиатуру, которая съедает высоту вьюпорта.
Собирать service worker и стратегии руками поучительно, но в продакшене этим занимается Workbox - библиотека Google, реализующая проверенные стратегии кэширования и lifecycle-механику. Она снимает рутину, но не решает за вас: какой ресурс какой стратегией обслуживать - ваша политика. В стеке Vite её подключает vite-plugin-pwa. Для offline-first разумен режим strategies: injectManifest - вы пишете собственный sw.ts, а плагин лишь внедряет в него precache-манифест (список файлов с ревизиями).
Ключевой выбор - registerType. Значение prompt согласуется с безопасным обновлением: новый worker ждёт, пока пользователь подтвердит перезагрузку, и вкладки не обновляются под ногами. Не включайте autoUpdate механически - для приложения с несохранёнными черновиками внезапная подмена версии посреди работы опаснее, чем устаревший на минуту клиент. Отдельно проверяйте сгенерированный precache-манифест: в него по недосмотру легко попадают sourcemaps, гигантские видео или приватные файлы, раздувая установку и утекая наружу.
Рантайм-кэширование описывается через registerRoute: сопоставляете запрос со стратегией - скажем, картинки того же origin через CacheFirst с ExpirationPlugin, ограничивающим число записей и срок жизни. У Workbox есть и собственный Background Sync-плагин, складывающий упавшие запросы в IndexedDB, но для доменных конфликтов и статусов в UI собственный outbox прозрачнее. Показательный провал автогенерации: команда включила autoUpdate и широкий glob, precache втянул старые видео, первая установка выросла до сотни мегабайт, а обновление молча сбросило открытый черновик.
pnpm add -D vite-plugin-pwa@1.3.0 workbox-build@7.4.1 workbox-precaching@7.4.1
pnpm add idb@8.0.3
// vite.config.ts
VitePWA({
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.ts',
registerType: 'prompt',
injectRegister: false,
manifest: false,
injectManifest: {
globPatterns: ['**/*.{js,css,html,svg,png,woff2}']
}
})registerRoute(
({ request, url }) =>
request.destination === 'image' && url.origin === self.location.origin,
new CacheFirst({
cacheName: 'images-v3',
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({ maxEntries: 80, maxAgeSeconds: 30 * 86400 })
]
})
);