Человек заходит в интернет-магазин с телефона третий раз за неделю, и браузер сам предлагает добавить его на главный экран. Через секунду на рабочем столе появляется знакомый значок, а запуск больше не требует ни адресной строки, ни поиска в закладках. За этой простой возможностью стоит файл настроек, несколько обязательных условий и событие, которое умеет перехватывать само приложение.
Манифест веб-приложения задаёт имя, иконку и поведение окна
Разговор о любой установке начинается с файла, который браузер читает перед тем, как предложить пользователю что-то добавить. Этот файл называется манифестом веб-приложения. Формально это обычный текстовый файл в формате JSON, в котором один объект верхнего уровня описывает внешний вид и поведение приложения.
Подключается он строкой в разделе заголовка документа:
<!doctype html>
<html lang="ru">
<head>
<link rel="manifest" href="/manifest.json" />
</head>
<body></body>
</html>
Если у приложения несколько страниц, ссылку на манифест нужно поставить на каждой. Забытая ссылка на одной из страниц приводит к тому, что установка предлагается не всегда, и поиск причины отнимает время.
Расширение файла может быть как привычным для JSON, так и специальным для манифестов. Второй вариант предпочтительнее, поскольку сервер отдаёт для него правильный тип содержимого. Если файл манифеста требует передачи учётных данных, в ссылке добавляется атрибут с режимом использования учётных данных, причём даже тогда, когда файл лежит на том же домене.
Минимальный набор полей выглядит скромно:
{
"name": "Мой магазин",
"icons": [
{
"src": "icons/512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
Этого достаточно для чтения файла, но недостаточно для установки. Браузеры на движке с открытым исходным кодом требуют большего, и об этих требованиях стоит поговорить отдельно.
Браузеры предъявляют точный список обязательных требований к установке
Первый пункт касается имени. Нужно поле с полным названием либо его краткий вариант, который попадёт под значок на экране. Практика показывает, что краткое имя длиннее двенадцати символов обрезается, поэтому его стоит подбирать с оглядкой на ширину подписи.
Второй пункт касается иконок. Требуются изображения как минимум двух размеров: сто девяносто два пикселя и пятьсот двенадцать пикселей по стороне. Отсутствие любого из них делает приложение неустанавливаемым, причём проверка эта строгая и молчаливая.
Третий пункт это адрес запуска. Поле указывает, с какой страницы открывается приложение. Если оно отсутствует, браузер подставит адрес текущей страницы, что для внутренних разделов даёт неверный результат.
Четвёртый пункт касается режима отображения. Нужно указать, как выглядит окно после запуска. Значение с отдельным окном без адресной строки даёт тот самый эффект приложения, ради которого всё затевается. Допускается и переопределение режима для разных условий.
Пятый пункт запрещает предпочитать связанные приложения из магазинов. Соответствующий флаг должен либо отсутствовать, либо стоять в значении "ложь". Иначе браузер решит, что у сайта есть нативное приложение, и предложит установить его вместо веб-версии.
Полный рабочий манифест собирается из этих пунктов и нескольких необязательных:
{
"name": "Мой магазин",
"short_name": "Магазин",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone"],
"background_color": "#ffffff",
"theme_color": "#1f6feb",
"description": "Заказы, доставка и бонусная программа",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
Параметр источника в адресе запуска не декоративен. Он позволяет аналитике отличать запуски из установленного приложения от обычных визитов, и без него разделить эти потоки не получится.
Защищённое соединение обязательно, а сервис-воркер нужен для офлайна
Установка невозможна без защищённого соединения. Исключение сделано только для локальной разработки, где допустимы адреса локального узла и петлевого интерфейса. Публичный сайт обязан работать по защищённому протоколу, иначе браузер просто не покажет предложение.
Второе условие касается сервис-воркера. Формально установка возможна и без него, но практическая ценность установленного приложения держится именно на нём. Сервис-воркер это сценарий, работающий в фоне и перехватывающий сетевые запросы страницы. Он позволяет показывать интерфейс при пропавшей связи, отдавать сохранённые ранее данные и корректно обрабатывать медленную сеть.
Регистрация выглядит коротко:
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
try {
const registration = await navigator.serviceWorker.register("/sw.js");
console.log("Сервис-воркер зарегистрирован", registration.scope);
} catch (error) {
console.log("Регистрация не удалась", error);
}
});
}
Здесь важна область действия. Сервис-воркер управляет только теми адресами, которые находятся внутри его области. Если файл лежит в подпапке, он не сможет перехватывать запросы корня сайта. Размещение файла в корне снимает это ограничение.
Отдельно стоит сказать о кэшировании. Простейшая стратегия сохраняет оболочку приложения при установке и отдаёт её из кэша, когда сеть недоступна. Продвинутые схемы различают типы запросов: статика берётся из кэша, данные запрашиваются из сети с запасным вариантом. Ошибка в этой логике приводит к тому, что пользователь неделями видит устаревшую версию интерфейса, поэтому стратегию для оболочки стоит пересматривать при каждом обновлении.
Событие beforeinstallprompt позволяет показать собственную кнопку
Браузер сам решает, когда предлагать установку. Пользователь может не увидеть предложения неделями, если редко возвращается на сайт, а разработчик не может повлиять на этот момент. Решение состоит в перехвате события.
Когда браузер готов показать своё предложение, он сначала сообщает об этом странице. Приложение может отменить стандартное поведение, сохранить объект события и показать собственную кнопку в подходящий момент.
let installPrompt = null;
const installButton = document.querySelector("#install");
window.addEventListener("beforeinstallprompt", (event) => {
event.preventDefault();
installPrompt = event;
installButton.removeAttribute("hidden");
});
installButton.addEventListener("click", async () => {
if (!installPrompt) {
return;
}
const result = await installPrompt.prompt();
console.log("Решение пользователя:", result.outcome);
installPrompt = null;
installButton.setAttribute("hidden", "");
});
У сохранённого события есть полезные свойства. Список платформ сообщает, какие варианты установки доступны на устройстве. Промис выбора разрешается объектом с исходом, и по этому исходу можно понять, согласился человек или отказался.
Ключевая тонкость состоит в одноразовости. Событие можно использовать только один раз. После вызова предложения сохранённый объект становится бесполезным, и попытка вызвать его повторно ничего не даст. Если пользователь отказался, повторное предложение появится только после того, как браузер снова сочтёт это уместным, обычно через некоторое время.
Вторая тонкость касается совместимости. Это экспериментальный и нестандартизированный интерфейс, он есть не во всех браузерах. Поэтому кнопка должна быть скрыта по умолчанию и появляться только после события. Обратный порядок приведёт к появлению мёртвой кнопки у части аудитории.
Третья тонкость связана с навязчивостью. Показывать предложение сразу при загрузке почти бессмысленно: человек ещё не понял, зачем ему это приложение. Момент стоит выбирать по действию, которое говорит о вовлечённости. Например, после третьего заказа или после добавления товара в избранное.
Экран запуска и внешний вид собираются из полей манифеста автоматически
После установки приложение получает собственный значок и запускается в отдельном окне. При запуске на части систем показывается заставка, которая генерируется браузером, а не разработчиком.
Что формирует эту заставку. Название приложения, цвет фона и иконка. Отсюда практический совет: цвет фона стоит подбирать в тон верхней части интерфейса, иначе при запуске возникнет неприятный цветовой рывок. Иконка должна хорошо смотреться на этом фоне.
Отдельного внимания заслуживает маскируемая иконка. Системы по-разному обрезают значки: где-то кругом, где-то скруглённым квадратом. Если нарисовать логотип до самых краёв, при обрезке от него отвалится часть. Маскируемый вариант отличается тем, что важное содержимое помещается в центральную область, а края отводятся под фон.
Цвет темы влияет на оформление системной панели и элементов окна. Его стоит согласовывать с визуальным стилем, иначе приложение будет выглядеть чужеродно рядом с системными.
Проверить манифест удобно через инструменты разработчика в браузере. Там видно, какие поля распознаны, какие иконки найдены и соответствует ли приложение критериям установки. Эта проверка экономит время, которое иначе уходит на догадки.
Практические ошибки установки чаще связаны с мелочами, а не с кодом
Первая частая ошибка это отсутствие иконки нужного размера. Файл есть, размер не тот, установка не предлагается. Проверять стоит обе требуемые стороны: и сто девяносто два, и пятьсот двенадцать пикселей.
Вторая ошибка связана с областью действия. Адрес запуска указывает на раздел, который выходит за пределы области. Браузер считает такой манифест некорректным, и предложение установки не появляется.
Третья ошибка касается относительных путей. Если манифест лежит в подпапке, относительные адреса иконок считаются от его расположения, а не от корня сайта. Разница в один уровень ломает всё.
Четвёртая ошибка это игнорирование уже установленного приложения. Если приложение уже стоит на устройстве, событие предложения не сработает. Интерфейс должен уметь определять этот случай и вместо кнопки установки показывать что-то осмысленное, иначе пользователь будет нажимать на кнопку, которая молчит.
function isStandalone() {
return window.matchMedia("(display-mode: standalone)").matches
|| window.navigator.standalone === true;
}
if (isStandalone()) {
installButton.setAttribute("hidden", "");
}
Пятая ошибка связана с обновлением. Пользователь установил приложение, а через месяц вышла новая версия. Если сервис-воркер отдаёт старую оболочку из кэша и не проверяет обновления, человек будет видеть устаревший интерфейс и не поймёт почему. Решение состоит в том, чтобы периодически проверять наличие новой версии и показывать неназойливое уведомление о готовности обновиться.
Установка меняет метрики и привычки аудитории
За установкой стоит не только удобство запуска. Меняется само поведение человека.
Первое следствие касается возврата. Значок на экране работает как постоянное напоминание, и доля повторных визитов у установивших приложение обычно выше, чем у обычных посетителей. Это не значит, что установка сама по себе удерживает людей: если приложение бесполезно, значок будет удалён.
Второе следствие связано с глубиной сессии. В отдельном окне нет адресной строки и вкладок, поэтому человек меньше отвлекается на другие сайты. Время, проведённое в интерфейсе, растёт.
Третье следствие касается доступности функций. Установленное приложение может получать уведомления, работать в офлайне, иметь доступ к камере и другим возможностям с меньшим числом лишних подтверждений.
Четвёртое следствие это стоимость поддержки. Один код обслуживает и сайт, и установленное приложение, в отличие от отдельного нативного клиента с собственным циклом выпуска.
Обратная сторона тоже есть. Установленное приложение сложнее обновить принудительно, а ошибки в кэшировании приводят к тому, что часть аудитории застревает на старой версии. Требования магазинов приложений здесь не действуют, зато и проверок перед выпуском никто не проводит. Ответственность за качество целиком лежит на команде.
Установка сайта на домашний экран получилась редким случаем, когда технология не требует от пользователя никаких решений. Ему не нужно выбирать между сайтом и приложением, скачивать установочный файл или искать обновления. Он нажимает одну кнопку и получает то же самое, но под рукой. Вся сложность при этом остаётся на стороне разработчика, и именно из этой сложности складывается ощущение, что всё работает само.