наш блог

Пошаговое создание динамического manifest'а для скрипта Service Worker и добавление иконки сайта на рабочий стол

Продолжительные словесные прелюдии уместны где угодно, включая художественную литературу, но только не в статьях, описывающих пошаговое создание каких-либо примочек и приложений. Поэтому не будем удаляться в теорию, а моментально углубимся в тему. Точнее, алгоритм.

Итак, начнем с простого html-шаблона:

index.html
<!doctype html>
<html>
<head>
   <meta charset="UTF-8">
   <title>My first PWA</title>
</head>
<body>
   <h1>Index PWA page!</h1>

   <input type="text" id="app_name">
   <button id="set_app_name" type=”button”>Сгенерировать манифест</button>
   <button id="add_app_to_desktop" type=”button”>Добавить приложение</button>

   <script src="sw.js"></script>
   <script src="script.js"></script>
</body>
</html>
sw.js
self.addEventListener('install', event => self.skipWaiting());
self.addEventListener('activate', event => self.clients.claim());
self.addEventListener('fetch', event => {});

Как видно, метод skipWaiting() в ServiceWorkerGlobalScope принудительно активирует ожидающий Service Worker и может быть вызван в любой момент его работы. Но будет действовать только тогда, когда есть свежеустановленный Service Worker. В противном случае последний так и останется в режиме ожидания. Именно поэтому Service Worker обычно вызывается из обработчика InstallEvent.

Кроме того, метод нужно использовать с self.clients.claim(). Чем можно будет гарантировать то, что обновления базового Service Worker вступают в силу немедленно - как для текущего, так и для прочих активных клиентов. В обработчике события fetch делать ничего не будем. Однако он необходим для корректной работы Service Worker.

Далее. Зарегистрируем Service Worker в файле script.js:

if ('serviceWorker' in navigator) {
   navigator.serviceWorker.register('./sw.js').then(
       (reg) => { console.log('SW registered!') },
       (err) => { console.log('SW not registered!', err) }
   )
}

С помощью команды if ('serviceWorker' in navigator) проверяем, поддерживает ли используемый браузер скрипт Service Worker. Если все в порядке, то регистрируем его функцией navigator.serviceWorker.register('./sw.js'). А в качестве параметра задаем путь к файлу sw.js. Рекомендуется, чтобы sw.js находился на уровне с index.html.

Теперь касательно файла script.js. Так как функция регистрации возвращает Promise, то при соблюдении правильности параметров в .then() получим объект регистрации. Но в рассматриваемом примере он не понадобится. А значит, просто-напросто выведем console.log('SW registered!'). В противном случае появится console.log('SW not registered!', err), что будет свидетельствовать об ошибке регистрации.

Следующий шаг. Пробуем запустить приложение с помощью любого виртуального сервера. Воспользуемся модулем http-server. Команда http-server-p 2829 должна запустить приложение по адресу localhost:2829. Если все сделано правильно, то консоль выведет текст «SW registered!».

При использовании браузера Google Chrome вкладка «Application/Service Workers» будет выглядеть следующим образом:

Service Worker успешно зарегистрирован. То есть можно переходить к следующему этапу. Сделаем так, чтобы ярлык приложения можно было сохранить на рабочий стол. Для этого необходим файл manifest.json, в который следует вписать нужные настройки для приложения. Впрочем, в этом примере все-таки придется отказаться от использования manifest.json, ведь цель статьи заключается в описании того, как именно сделать манифест динамическим.

В index.html в тег head добавляем <link rel="manifest" id="custom-manifest">. А в файле script.js пишем функцию, которая и будет генерировать manifest.

script.js

function generateManifest() {
   const myDynamicManifest = {
       "name": "Test Application",
       "short_name": "Test app",
       "scope": window.location.href,
       "start_url": window.location.href,
       "display": "fullscreen",
       "icons": [
           {
               "src":"https://via.placeholder.com/48/",
               "sizes": "48x48",
               "type": "image/png"
           },
           {
               "src": "https://via.placeholder.com/144/",
               "sizes": "144x144",
               "type": "image/png"
           },
           {
               "src": "https://via.placeholder.com/196/",
               "sizes": "196x196",
               "type": "image/png"
           }
       ]
   };
   const stringManifest = JSON.stringify(myDynamicManifest);
   const blob = new Blob([stringManifest], {type: 'application/json'});
   const manifestURL = URL.createObjectURL(blob);
   document.querySelector('#custom-manifest').setAttribute('href', manifestURL);
}

В функции создаем объект myDynamicManifest. В него помещаем начальные настройки. Поле name - это имя приложения. Оно будет указываться под иконкой на рабочем столе. Поле short_name предназначено для краткого названия, которое будет отображаться там, где недостаточно места для полного имени.

Поле scope определяет область навигации контекста веб-приложения, выход за которую вернет его обратно - в вид привычной веб-страницы. Start_url определяет URL, загружаемый в момент, когда пользователь запускает приложение с устройства. Display - определяет режим отображения. Icons - иконки приложения для разных разрешений экрана. Ссылки на иллюстрации возьмем из сервиса placeholder.com.

Далее. Превращаем объект в строку посредством метода JSON.stringify(). И записываем в переменную stringManifest. Создаем новый BLOB-объект, после чего помещаем туда строку манифеста. А вторым параметром указываем необходимый тип, а именно - application/json. BLOB-объект является неким подобием файла с неизменяемыми и необработанными значениями. При этом BLOB'ы представляют собой данные, которые могут находиться в не родном для JavaScript формате.

Интерфейс File основан на BLOB. Он наследует функциональность последнего, а также расширяет ее для поддержки файлов на стороне пользователя. URL.createObjectURL() - статический метод, создающий DOMString, который содержит URL с указанием на объект, заданный в качестве параметра. Время жизни URL напрямую связано с document - окном, в котором он был создан. Новый URL-объект может представлять собой File-объект или BLOB-объект.

Добавляем к тегу <link rel="manifest" id="custom-manifest"> атрибут href, после чего записываем в него полученный manifestURL. Теперь смотрим, что происходит в момент вызова функции  generateManifest().

script.js

window.addEventListener('DOMContentLoaded', () => {
   generateManifest();
});

Если все верно, то в теге head - в <link rel="manifest" id="custom-manifest"> - появится атрибут href со ссылкой на манифест. Перейдя по ней, сможем увидеть манифест приложения.

Отлично. Теперь попробуем получить запрос для установки приложения на рабочий стол. Создаем глобальную переменную promptEvent. И набрасываем обработчик на событие beforeinstallprompt.

Добавляем в script.js

let promptEvent;
window.addEventListener('beforeinstallprompt', (event) => {
   event.preventDefault();
   console.log(event);
   promptEvent = event;
}); 

В обработчике запрещаем стандартное действие события. Выводим в консоль объект события и сохраняем его в глобальную переменную. После чего появится возможность в любом другом месте вызвать метод prompt у переменной promptEvent. Это, в свою очередь, позволит получить запрос на установку приложения. Перезагрузим страницу, чтобы просмотреть значение, которое выводится в консоль.

beforeinstallprompt будет вызвано только при соблюдении следующих условий:

  • PWA не должно быть уже установлено;
  • Пользователь должен взаимодействовать с веб-приложением не менее 30 секунд;
  • Приложение должно содержать manifest;
  • Приложение должно работать на защищенном HTTPS-соединении;
  • Должен быть зарегистрирован Service Worker, который содержит обработчик события fetch.

После регистрации Service Worker, видим нужный объект события. Добавляем возможность изменения манифеста. Предположим, что нужно отредактировать имя приложения. Передаем аргументом название в функцию generateManifest().

function generateManifest(app_name) {
 const myDynamicManifest = {
   "name": "Test Application",
   "short_name": "Test app",
   "scope": window.location.href,
   "start_url": window.location.href,
   "display": "fullscreen",
   "icons": [
     {
       "src":"https://via.placeholder.com/48/",
       "sizes": "48x48",
       "type": "image/png"
     },
     {
       "src": "https://via.placeholder.com/144/",
       "sizes": "144x144",
       "type": "image/png"
     },
     {
       "src": "https://via.placeholder.com/196/",
       "sizes": "196x196",
       "type": "image/png"
     }
   ]
 };
 if (app_name) {
   myDynamicManifest.name = app_name
 }
 const stringManifest = JSON.stringify(myDynamicManifest);
 const blob = new Blob([stringManifest], {type: 'application/json'});
 const manifestURL = URL.createObjectURL(blob);
 document.querySelector('#custom-manifest').setAttribute('href', manifestURL);
}

После создания объекта манифеста, делаем проверку на наличие аргумента. Если он есть, то меняем нужное поле в манифесте и генерируем новый URL. Далее получаем, а также сохраняем кнопки и инпут в переменные. Это необходимо для того, чтобы набросить обработчики событий.

window.addEventListener('DOMContentLoaded', () => {
   const setAppNameBtn = document.querySelector('#set_app_name');
   const addAppToDesktopBtn = document.querySelector('#add_app_to_desktop');
   const input = document.querySelector('#app_name');

   generateManifest();

  setAppNameBtn.addEventListener('click', (e) => {
       generateManifest(input.value);
   });
   addAppToDesktopBtn.addEventListener('click', (e) => {
       promptEvent.prompt();
   });
});

Теперь, после нажатия клавиши «Добавить приложение», появится окошко с предложением установить его на рабочий стол. Имя окажется таким, которое было изначально указано в объекте манифеста. Нажимаем кнопку «Отмена». Пробуем отредактировать название. Вводим в инпут желаемое имя, нажимаем на «Сгенерировать манифест», а потом на «Добавить приложение». В результате имя изменилось на то, которое было вписано в инпут. Ссылки: Git, Demo.

Юрий Кизилов, компания Craft Group