Наверное, все близкие к веб-разработке люди уже наслышаны о Progressive Web App. Ещё бы! Эта технология практически уравняла веб и мобильную разработку с точки зрения распространения продуктов и вовлечённости пользователей.

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

  1. Иконка приложения есть на главном экране смартфона.
  2. Приложение открывается в отдельном окне.
  3. Приложение отправляет push-уведомления.

Все 3 пункта снимаются благодаря Progressive Web App или PWA. Теперь, заходя на сайт из мобильного браузера, мы можем «скачать» его, после чего увидим иконку на глав��ом экране. Кроме того, при запуске появляется заставка, как у мобильных приложений, и при желании можно настроить отправку push-уведомлений.

И казалось бы, всё прекрасно! Но увы, за 10 с лишним лет мобильной эпохи пользователи слишком сильно привыкли искать приложения в Google Play и App Store. Ломать привычки пользователей — дело неблагодарное, и потому ребята из Google (кстати, Google является разработчиком PWA) решили, что если гора не идёт к Магомеду, то… В общем, совсем недавно, 6 февраля 2019 года, они обеспечили использование Trusted Web Activities для выкладки веб-приложений в Google Play.

В статье из двух частей будет рассказано, как пройти полный путь от обычного веб-сайта до приложения в Google Play всего за считанные часы. Всё это будет показано на примере реального сервиса — Скорочтец.

  1. Как сделать из сайта приложение и выложить его в Google Play за несколько часов. Часть 1/2: Progressive Web App
  2. Как сделать из сайта приложение и выложить его в Google Play за несколько часов. Часть 2/2: Trusted Web Activity


Lighthouse


На входе у нас есть веб-сайт с мобильной вёрсткой:


Первым делом нужно установить расширение Lighthouse в Google Chrome на своём рабочем компьютере. Это инструмент для анализа сайтов в целом и для проверки соответствия стандарту Progressive Web App в частности.

Далее открываем наш сайт, боевой или запущенный локально, и генерируем отчёт для него при помощи Lighthouse:


В разделе Progressive Web App отчёта вы должны увидеть примерно следующее:



Обратите внимание на раздел Installable. Во-первых, если вы запускаете сайт локально, а вам придётся это делать во время разработки и тестирования, то нужно использовать домен localhost и никакой другой. Благодаря этому будет удовлетворено требование «Use HTTPS», а точнее Lighthouse просто закроет глаза на него, и вы сможете полноценно тестировать свой PWA.

Кроме требования HTTPS, чтобы наше приложение превратилось в PWA и стало устанавливаемым, нужно подключить к сайту service worker и Web app manifest. Давайте сделаем это.

Service worker


Технология service workers позволяет вашему сайту быть online даже тогда, когда сервер недоступен. Это такой посредник между клиентом и сервером, который перехватывает каждый запрос и в случае чего подсовывает данные из кэша в качестве ответа.

Для работы PWA достаточно базовой реализации service worker, которая выглядит следующим образом:

service-worker.js
// Должно быть true в production
var doCache = true;

// Имя кэша
var CACHE_NAME = 'my-pwa-cache-v2';

// Очищает старый кэш
self.addEventListener('activate', event => {
   const cacheWhitelist = [CACHE_NAME];
   event.waitUntil(
       caches.keys()
           .then(keyList =>
               Promise.all(keyList.map(key => {
                   if (!cacheWhitelist.includes(key)) {
                       console.log('Deleting cache: ' + key)
                       return caches.delete(key);
                   }
               }))
           )
   );
});

// 'install' вызывается, как только пользователь впервые открывает PWA 
self.addEventListener('install', function(event) {
   if (doCache) {
       event.waitUntil(
           caches.open(CACHE_NAME)
               .then(function(cache) {
                   // Получаем данные из манифеста (они кэшируются)
                   fetch('/static/reader/manifest.json')
                       .then(response => {
                           response.json()
                       })
                       .then(assets => {
                       // Открываем и кэшируем нужные страницы и файлы
                           const urlsToCache = [
                               '/app/',
                ........
                               '/static/core/logo.svg*',
                           ]
                           cache.addAll(urlsToCache)
                           console.log('cached');
                       })
               })
       );
   }
});

// Когда приложение запущено, сервис-воркер перехватывает запросы и отвечает на них данными из кэша, если они есть
self.addEventListener('fetch', function(event) {
   if (doCache) {
       event.respondWith(
           caches.match(event.request).then(function(response) {
               return response || fetch(event.request);
           })
       );
   }
});

Здесь реализованы обработчики для трёх событий: install, activate и fetch. Как только пользователь откроет сайт, на котором есть service worker, вызовется событие install. Это процедура установки сервис-воркера в браузер пользователя. В её обработчике в массиве urlsToCache вы можете указать страницы сайта, которые будут кэшироваться, включая статику. Затем вызывается activate, которое очищает ресурсы, использованные в предыдущей версии скрипта сервис-воркера. И теперь, когда сервис-воркер успешно установлен, он будет перехватывать каждое событие fetch и искать в кэше запрашиваемые ресурсы, прежде чем идти за ними на сервер.

Чтобы всё это заработало, нужно добавить скрипт для регистрации сервис-воркера в html-файлы. Так как Скорочтец является одностраничным приложением (SPA), то у него один единственный html, который после добавления указанного скрипта выглядит вот так:

index.html
<!DOCTYPE html>
<html lang="ru">
<head>
   <meta charset="UTF-8">
   <title>Скорочтец</title>


</head>
<body>
   <div id="root"></div>
   <script src="/static/build/app.js"></script>

   <script>
       if ('serviceWorker' in navigator) {
           window.addEventListener('load', function() {
               navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
                   // Registration was successful
               console.log('ServiceWorker registration successful with scope: ', registration.scope);
             }, function(err) {
               // registration failed :(
               console.log('ServiceWorker registration failed: ', err);
             }).catch(function(err) {
               console.log(err)
             });
           });
         } else {
           console.log('service worker is not supported');
         }
   </script>
</body>
</html>

Функция navigator.serviceWorker.register('/service-worker.js') принимает в качестве аргумента URL, по которому расположен файл сервис-воркера. Здесь не важно, как именно называется файл, но важно, чтобы он был расположен в корне домена. Тогда областью видимости сервис-воркера станет весь домен, и он будет получать события fetch из любой страницы.

Таким образом, расположив файл сервис-воркера по адресу skorochtec.ru/service-worker.js и добавив нужный скрипт в html, мы получаем следующую картину в отчёте Lighthouse:



Если сравнивать с предыдущим отчётом, то теперь у нас удовлетворён второй пункт и сайт отвечает 200 даже offline, а также в 5-м пункте мы видим, что сервис-воркер обнаружен, но вот стартовой страницы не хватает. Информация о стартовой странице и не только указывается в Web App Manifest, давайте добавим его!

Web App Manifest


Манифест предоставляет информацию о нашем приложении: короткое и длинное имя, иконки всех размеров, стартовая страница, цвета и ориентация.

manifest.json
{
 "short_name": "Скорочтец",
 "name": "Скорочтец",
 "icons": [
       {
     "src":"/static/core/manifest/logo-pwa-16.png",
     "sizes": "16x16",
     "type": "image/png"
   },
   {
     "src":"/static/core/manifest/logo-pwa-32.png",
     "sizes": "32x32",
     "type": "image/png"
   },
   {
     "src":"/static/core/manifest/logo-pwa-48.png",
     "sizes": "48x48",
     "type": "image/png"
   },
   {
     "src":"/static/core/manifest/logo-pwa-72.png",
     "sizes": "72x72",
     "type": "image/png"
   },
   {
     "src":"/static/core/manifest/logo-pwa-96.png",
     "sizes": "96x96",
     "type": "image/png"
   },
   {
     "src":"/static/core/manifest/logo-pwa-144.png",
     "sizes": "144x144",
     "type": "image/png"
   },
   {
     "src":"/static/core/manifest/logo-pwa-192.png",
     "sizes": "192x192",
     "type": "image/png"
   },
   {
     "src":"/static/core/manifest/logo-pwa-512.png",
     "sizes": "512x512",
     "type": "image/png"
   }
 ],

 "start_url": "/app/",
 "background_color": "#7ACCE5",
 "theme_color": "#7ACCE5",
 "orientation": "any",
 "display": "standalone"
}

Последняя переменная указывает, что это будет отдельное приложение. Файл манифеста необходимо расположить на сайте (не обязательно в корне) и подключить его в html:

index.html
<!DOCTYPE html>
<html lang="ru">
<head>
   <meta charset="UTF-8">
   <title>Скорочтец</title>

   <!-- Add manifest -->

<link rel="manifest" href="{% static "core/manifest/manifest.json" %}">

   <!-- Tell the browser it's a PWA -->
</head>
<body>
   <div id="root"></div>
   <script src="/static/build/app.js"></script>

   <script>
       if ('serviceWorker' in navigator) {
           window.addEventListener('load', function() {
               navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
                   // Registration was successful
               console.log('ServiceWorker registration successful with scope: ', registration.scope);
             }, function(err) {
               // registration failed :(
               console.log('ServiceWorker registration failed: ', err);
             }).catch(function(err) {
               console.log(err)
             });
           });
         } else {
           console.log('service worker is not supported');
         }
   </script>
</body>
</html>

Давайте снова проанализируем сайт Lighthouse-ом:



Ура! Теперь у нас не просто сайт, а Progressive Web App! Возможно, вы заметили, что скорость загрузки резко подросла. Это никак не связано с тем, что мы делали, просто я заменил development-сборку React-приложения на production, чтобы отчёт выглядел максимально красиво.

Ну что ж, заходим на сайт из мобильного Chrome и что же мы видим?


Да! Можно открывать шампанское! Добавляем приложение на главный экран:



Бонусом получаем заставку при запуске, которая собирается из указанных в манифесте name, background_color и иконки 512x512 в массиве icons:


К сожалению, цвет текста подбирается автоматически, что в случае Скорочтеца немного ломает стиль.

Ну и само приложение:


Ограничения


На данный момент PWA поддерживается только в Chrome и Safari (начиная с iOS версии 11.3). Причём, Safari поддерживает эту технологию «по-тихому». Пользователь может добавить приложение на главный экран, но только никакого сообщения об этом нет, в отличие от Chrome.

Полезные советы и трюки


1. Предложение об установке на Safari


Поскольку в Apple этого не сделали (надеемся, что пока не сделали), то приходится реализовывать «руками». Получается вот такое:


Реализуется следующим JavaScript-кодом:

       const isIos = () => {
           const userAgent = window.navigator.userAgent.toLowerCase();
           return /iphone|ipad|ipod/.test( userAgent );
       };
       // Проверяем, открыто ли приложение отдельно или в браузере
       const isInStandaloneMode = () => ('standalone' in window.navigator) && (window.navigator.standalone);



       // Если приложение открыто на iOS и в браузере, то предлагаем установить
       if (isIos() && !isInStandaloneMode()) {
           this.setState({ isShown: true }); // На примере React
       }

2. Отслеживание установок


Это работает только в Google Chrome. Нужно добавить в html скрипт, отлавливающий событие appinstalled и, например, отправлять на свой сервер сообщение об этом:

<script>
   window.addEventListener('appinstalled', (evt) => {
       fetch(<your_url>, {
           method: 'GET',
           credentials: 'include',
       });
   });
</script>

3. Правильный выбор start_url


Обязательно нужно позаботиться о том, что url всех страниц приложения являются продолжением start_url, указанного в манифесте. Потому что, если вы укажете "start_url": "/app/", а затем пользователь перейдёт на страницу, скажем, "/books/", то тут же покажет себя адресная строка браузера и весь пользовательский опыт сломается. Кроме того, человек почувствует себя обманутым: думал, что использует приложение, а это замаскированный браузер. И даже theme_color из манифеста, который окрасит интерфейс браузера в ваш фирменный цвет, не спасёт.

В случае Скорочтеца, все страницы, относящиеся к приложению, начинаются с /app/, поэтому таких казусов не возникает.

Что дальше?


Ну что ж, теперь вы знаете, как забраться к пользователю на главный экран смартфона через ваш сайт. Но это только одна из дверей, и скорее всего не парадная. Во второй части будет рассказано, как войти через парадную дверь: вы узнаете, как выложить ваше прогрессивное веб-приложение в Google Play.

Полезные ссылки