Прежде чем углубляться в детали, полезно увидеть общую картину: сколько именно действий отделяют обычный сайт от устанавливаемого PWA. Ответ — три. После этих трёх шагов Chrome уже предложит пользователю «Добавить приложение на главный экран», а сайт начнёт работать офлайн.

В этой главе пройдём минимальный путь от и до — на уровне «вот что это такое, вот зачем нужно, вот файл, положите его сюда». В следующих главах разберём каждую часть детально, но сейчас задача — чтобы в голове сложилась полная последовательность.

Шаг 1. HTTPS на домене

HTTPS — это обычный HTTP, но завёрнутый в шифрование. Любой трафик между браузером пользователя и вашим сервером идёт по защищённому каналу: даже если кто-то подключится к той же Wi-Fi сети в кафе, он не сможет прочитать содержимое запросов или подменить ответ. Технически HTTPS работает поверх протоколов TLS (старое название — SSL), и для его включения сайту нужен сертификат — цифровой документ, подтверждающий, что сайт действительно принадлежит указанному домену.

На обычном сайте HTTPS полезен, но не обязателен — сайт работает и по HTTP. В PWA HTTPS обязателен. Браузер откажется регистрировать service worker, если страница загружена не по HTTPS, и без service worker никакого PWA не получится.

Почему именно так строго? Service worker — это фоновый JavaScript, который умеет перехватывать все сетевые запросы страницы и подменять ответы. Если бы такую возможность можно было получить через незащищённое соединение, то злоумышленник в публичной Wi-Fi мог бы подсунуть свой service worker на любой сайт — и он перехватывал бы пароли, банковские данные и всё остальное. Поэтому браузеры пускают service worker только на проверенные источники.

Одно исключение для разработки. На localhost и 127.0.0.1 service worker работает по HTTP — браузер считает локальный адрес доверенным. Это значит, что на этапе разработки никакие сертификаты настраивать не нужно. Запустили локальный сервер на http://localhost:3000 — и пишите код.

На проде: два самых простых варианта получить HTTPS бесплатно.

  • Let’s Encrypt — бесплатный центр сертификации. Выдаёт сертификаты автоматически через утилиту certbot, которая ставится на сервер, получает сертификат и продлевает его каждые 90 дней. Один раз настроили — и забыли.
  • Cloudflare — сервис-прослойка перед сайтом. Переключаете DNS домена на Cloudflare, и они выдают свой SSL поверх вашего сайта бесплатно. Настройка — около десяти минут, ничего ставить на сервер не надо.

Как проверить. Откройте сайт — в адресной строке должен быть значок замка слева от URL. Тапните по нему — браузер покажет, что соединение защищено и какой сертификат у сайта.

Шаг 2. Манифест приложения

Манифест — это маленький JSON-файл, в котором написано всё, что нужно операционной системе, чтобы показать ваш сайт как приложение. Имя, иконки, цвета, режим запуска. По сути это «паспорт» вашего PWA: ОС читает его, когда пользователь устанавливает приложение, и потом использует данные оттуда для отрисовки.

Создайте в корне сайта файл app.webmanifest (или manifest.json — оба варианта работают, нюансы разберём в главе 3) с таким содержимым:

{
  "name": "Минимальное PWA",
  "short_name": "MinPWA",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#316bbe",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Расшифровка полей, чтобы было понятно, зачем каждое:

  • name — полное имя приложения. Показывается на сплэш-экране, который пользователь видит, пока загружается основной интерфейс после нажатия на иконку.
  • short_name — короткое имя для подписи под иконкой на главном экране. Места там мало, поэтому здесь обычно 8–12 символов.
  • start_url — URL, который открывается по тапу на иконку. Если PWA должна стартовать с главной, пишут "/".
  • display: standalone — говорит ОС: «запусти приложение в собственном окне, без адресной строки браузера и вкладок». Выглядит как нативное приложение.
  • background_color — цвет фона сплэш-экрана.
  • theme_color — цвет шапки приложения. На Android окрашивает строку состояния телефона.
  • icons — массив иконок разного размера. Минимум для Chrome — две: 192×192 и 512×512 пикселей. Меньшая используется на иконке главного экрана, бо́льшая — на сплэш-экране и в магазинах приложений, если PWA туда будут публиковать.

Зачем именно две иконки разных размеров? Экраны телефонов и десктопов разные, плотность пикселей разная, а ОС хочет, чтобы иконка выглядела чётко везде. Если положить только маленькую — на большом экране её придётся растягивать, и она будет мыльной. Если только большую — на маленьком ОС будет её сжимать каждый раз при отрисовке, что медленнее. Поэтому отдают сразу два варианта, чтобы система могла выбрать подходящий.

Когда файл готов, подключите его в HTML-каркасе на каждой странице сайта:

<link rel="manifest" href="/app.webmanifest">
<meta name="theme-color" content="#316bbe">

Тег <link rel="manifest"> говорит браузеру: «на этом адресе лежит описание приложения». Мета-тег theme-color дублирует значение из манифеста — на случай, если страница уже отрисована, а манифест браузер ещё не скачал. Так строка состояния телефона окрашивается сразу при первом рендере, не дёргается.

Как проверить. Откройте сайт в Chrome, нажмите F12 (или Cmd+Opt+I на Mac), перейдите на вкладку Application в DevTools, выберите слева пункт Manifest. Браузер покажет распарсенный манифест, иконки в превью и все ошибки/предупреждения, если что-то не так.

Шаг 3. Service worker

Service worker — это JavaScript-файл, который выполняется в отдельном фоновом потоке браузера, независимо от страниц сайта. Он не имеет доступа к DOM (то есть не может ничего нарисовать на странице, не видит, что показывается пользователю), но умеет другое: перехватывать сетевые запросы, идущие со страницы. Любой fetch в браузере проходит через service worker, и тот решает, что вернуть в ответ — пойти в сеть, отдать из кэша, скомбинировать.

Именно service worker отвечает за работу офлайн: при первом визите он сохраняет важные файлы в кэш, а при последующих визитах без сети — отдаёт их из кэша вместо ошибки «нет интернета».

Создайте файл sw.js в корне сайта:

const CACHE_NAME = "minimal-pwa-v1";
const PRECACHE = ["/", "/index.html", "/offline.html"];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE))
  );
});

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request).catch(() => caches.match("/offline.html"));
    })
  );
});

Разберём, что происходит:

  • CACHE_NAME — имя кэша. Каждый кэш в браузере именованный; в одном сайте их может быть несколько (один под статику, другой под аватарки и так далее). Версия в имени (-v1) пригодится позже — при выкатке новой версии меняют на -v2, и старый кэш можно удалить.
  • PRECACHE — список файлов, которые надо положить в кэш сразу при установке service worker. Тут — главная, основной HTML и оффлайн-страница для случая «нет сети».
  • Событие install — срабатывает один раз при регистрации service worker. Открываем кэш с нашим именем, добавляем туда файлы из списка. event.waitUntil() говорит браузеру: «не считай установку завершённой, пока этот промис не разрешится».
  • Событие fetch — срабатывает на каждый сетевой запрос, который идёт со страницы. Сначала ищем файл в кэше; если нашли — отдаём из кэша; если нет — идём в сеть; если сети тоже нет — отдаём оффлайн-страницу.

Сам файл создан, но пока он лежит мёртвым грузом — браузер о нём ничего не знает. Нужна регистрация: отдельный код на главной странице, который скажет браузеру «вот этот файл — service worker, начни его использовать».

if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    navigator.serviceWorker
      .register("/sw.js")
      .then(() => console.log("Service worker зарегистрирован"))
      .catch((err) => console.warn("Ошибка регистрации SW:", err));
  });
}

Что здесь важно:

  • "serviceWorker" in navigator — страховка от старых браузеров, в которых API нет. На современных всегда даёт true.
  • Регистрация после load, а не в начале <script>. Если зарегистрировать раньше — браузер начнёт качать sw.js параллельно с первичной загрузкой страницы, и сайт начнёт визуально подгружаться медленнее. Ждём, пока страница отрисуется, и только потом подгружаем инфраструктуру.
  • navigator.serviceWorker.register("/sw.js") — собственно регистрация. Промис разрешается успешным .then, если всё прошло, или падает в .catch, если в файле SW синтаксическая ошибка или путь неверный.

Как проверить. В Chrome DevTools (F12) откройте вкладку Application → раздел Service Workers. После первой загрузки страницы там должна появиться запись с зелёной точкой и статусом activated and is running. Это значит, что service worker зарегистрирован и работает.

Что произойдёт после этих трёх шагов

При первом заходе пользователя:

  1. Браузер скачивает sw.js, регистрирует service worker. Тот срабатывает на событие install и предзагружает в кэш главную, index.html и offline.html.
  2. Параллельно браузер видит подключённый манифест с обязательными полями. Через 5–10 секунд он показывает в адресной строке иконку «Установить приложение» (на десктопе) или предлагает добавить на главный экран (на Android).
  3. Если пользователь нажмёт «установить», на устройстве появляется иконка с дизайном из манифеста. При тапе на иконку запускается приложение в собственном окне без UI браузера — ровно как нативное.
  4. Если интернет пропал во время следующего визита — страницы открываются из кэша; если запрашиваемой страницы в кэше нет, показывается offline.html вместо обычной ошибки ERR_INTERNET_DISCONNECTED.

Этого минимума достаточно, чтобы Lighthouse-аудит PWA прошёл с зелёной плашкой. Дальше идут улучшения: продуманные стратегии кэширования (глава 5), push-уведомления (глава 6), кастомная кнопка установки и публикация в магазинах (глава 7). Этим займёмся дальше.

Чек-лист одной строкой

  1. HTTPS на проде (или localhost на разработке) — обязательно, иначе SW не зарегистрируется.
  2. app.webmanifest в корне + <link rel="manifest"> в HTML на каждой странице.
  3. sw.js в корне + регистрация через navigator.serviceWorker.register() на главной.

Каждый из этих трёх пунктов — тема собственной главы, в которой разберём детали и реальные нюансы.