Прогрессивное веб-приложение (PWA) на Phaser – как создать офлайн-приложение

Phaser - это фантастическая игровая среда HTML5, которую можно легко запустить на любом устройстве. Недостатком является то, что ее основа - HTML5, поэтому не существует готового решения для создания мобильной версии вашей игры. Вы можете использовать другие инструменты, такие как PhoneGap, CocoonJS, Ionic и т. д., чтобы создать гибридное мобильное приложение, но вы будите зависеть от сторонних инструментов. Тем не менее, если вы оптимизируете свою игру должным образом, любой может играть в нее на мобильном устройстве, но это требует, чтобы пользователь обязательно посещал ваш сайт, и при этом у него не будет некоторых приятных фишек приложения, таких как значок на домашнем экране или возможность играть в игру без сети.

Что если бы у вас было гибридное приложение, представляющее собой смесь собственного мобильного приложения и веб-приложения? Прогрессивные веб-приложения или PWA, напоминают мобильное приложение, могут работать в автономном режиме, быстро реагируют и очень просты в установке. Кроме того, мы можем сделать нашу игру PWA без каких-либо сторонних инструментов. Нам просто нужно добавить несколько файлов в наш проект, и нам нужно добавить дополнительный код на нашу главную html-страницу.

Вы можете скачать все файлы, связанные с исходным кодом, здесь .

Требования к уроку

Для этого урока вам понадобится следующее:

  • Базовые знания и навыки JavaScript
  • Редактор кода
  • Локальный веб-сервер
  • Chrome Web Browser Для этого руководства рекомендуется, чтобы вы были знакомы с основными понятиями Phaser, такими как сцены, настройка конфигурации для вашей игры, локальный запуск игры и т. д. Если вы не знакомы с этими понятиями, вы все равно сможете пройти урок, но мы не будем подробно освещать эти темы. Если вы хотите узнать больше об этих понятиях, то вы можете ознакомиться с учебным пособием Как создать аркадную игру с помощью Phaser 3.

Настройка проекта

В этом руководстве мы собираемся использовать часть кода из шаблона проекта веб-пакета Phaser 3, который доступен на GitHub. Мы не будем использовать данный веб-пакет для этого урока, однако мы будем использовать базовый шаблон и изображения логотипа Phaser. Вы можете скачать базовый код проекта здесь.

В архиве вы увидите три папки (css, js и img) и файл index.html. Если вы откроете index.html в своем редакторе кода, вы должны увидеть следующий код:

<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="css/style.css" />
        <title>PWA-game</title>
    </head>
    <body>
        <script src="//cdn.jsdelivr.net/npm/phaser@3.20.1/dist/phaser.js"></script>
        <script src="js/game.js"></script>
    </body>
</html>

В папке js есть файл с именем game.js, котором есть вся логика для нашей игры Phaser. Если вы откроете game.js, то вы увидите следующий код:

var config = {
  type: Phaser.AUTO,
  parent: 'phaser-example',
  width: window.innerWidth,
  height: window.innerHeight,
  scene: {
    preload: preload,
    create: create
  }
};

var game = new Phaser.Game(config);

function preload() {
  this.load.image('logo', 'img/logo.png');
}

function create() {
  this.logo = this.add.image(0, 0, 'logo');
  this.logo.setScale(0.5);

  Phaser.Display.Align.In.Center(
    this.logo,
    this.add.zone(window.innerWidth/2, window.innerHeight/2, window.innerWidth, window.innerHeight)
  );

  this.tweens.add({
    targets: this.logo,
    y: 450,
    duration: 2000,
    ease: 'Power2',
    yoyo: true,
    loop: -1
  });
}

Наконец, если вы запустите свой сервер и попробуете запустить игру, вы должны увидеть черный экран с логотипом Phaser.

Добавление Service worker

Теперь, когда наш проект настроен, мы добавим в нашу игру Service worker. Прежде чем мы начнем писать код, давайте рассмотрим, что такое Service worker. Service worker - это служба JavaScript, являющаяся по сути файлом со сценарием JavaScript, который запускается в фоновом потоке браузера и может использоваться для перехвата запросов, кеширования и извлечения кешированных ресурсов, а также для доставки push-уведомлений. Что это значит для нас? Мы можем использовать Service worker для кеширования ресурсов нашей игры в браузере пользователя, что позволит нам использовать кешированные ресурсы для ускорения загрузки и автономной работы.

Service Worker имеет жизненный цикл, полностью отделенный от веб-страницы, поэтому он не может получить доступ к DOM напрямую. Вместо этого, Service Worker может обмениваться данными со страницами, которые он контролирует, реагируя на сообщения, отправленные через интерфейс postMessage и эти страницы могут манипулировать DOM, если это необходимо. Он прерывается, когда не используется, и перезапускается заново, если необходим. Поэтому нельзя полагаться на какое-то глобальное состояние в обработчиках onfetch и onmessage. Если необходимо сохранить какую-либо информацию между перезапусками, Service Worker’ы имеют доступ к API IndexedDB.

Чтобы использовать Service Worker’ы, ваша игра должна быть доступна по протоколу HTTPS. В процессе разработки вы можете использовать Service Worker на localhost. Наконец, большинство браузеров поддерживают Service Worker’ы, но есть и исключения. Вы можете узнать какие браузеры поддерживаются их на странице: Service Worker Ready.

Итак, давайте, наконец, добавим нашего Service Worker. Чтобы его установить, первое, что нам нужно сделать - это его зарегистрировать. В папке вашего проекта создайте новый файл с именем sw.js. Пока мы оставим этот файл пустым. Затем откройте index.html и добавьте следующий код перед добавлением других файлов JavaScript:

<script>
    if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
            navigator.serviceWorker.register('/sw.js', {scope: '/'}).then(function(registration) {
                console.log('ServiceWorker зарегистрирован с областью видимости в пределах: ', registration.scope);
            }, function(err) {
                console.log('Регистрация ServiceWorker не удалась: ', err);
            });
        });
    }
</script>

Давайте рассмотрим код, который мы только что добавили:

  • Сначала мы проверяем, доступен ли API Service Worker.
  • Если API доступен, то после загрузки страницы мы регистрируем ServiceWorker в /sw.js и устанавливаем область видимости (scope) для нашего Service Worker в корень сайта: /. В принципе данное значение является значением по умолчанию и его можно не указывать. Параметр scope используется для управления тем, какие части вашего приложения могут использоваться Service Worker. Например, если вы установите scope значение /games/, то Service Worker сможет получить доступ только к файлам расположенным в /games/.
  • Наконец, мы регистрируем наш Service Worker и выводим информацию об этом в консоль.

Теперь, если вы сохраните и перезагрузите свою игру в браузере, откроете Сonsole в инструментах разработчика Chrome, вы должны увидеть сообщение об успешной регистрации Service Worker. Вы также можете проверить, что Service Worker был установлен, щелкнув на вкладку Application в инструментах разработчика Chrome. Там, если вы выберите пункт Service Workers , вы увидите зарегистрированный нами Service Worker.

Кеширование ресурсов

Хоть наш Service Worker установлен, но он ничего не делает. Чтобы исправить это, мы его обновим, чтобы он кешировал все ресурсы, используемые нашей игрой. В sw.js добавьте следующий код:

var cacheName = 'phaser-v1';
var filesToCache = [
  '/',
  '/index.html',
  '/img/logo.png',
  '/img/icon-192.png',
  '/img/icon-256.png',
  '/img/icon-512.png',
  '/js/game.js',
  '/css/style.css',
  'https://cdn.jsdelivr.net/npm/phaser@3.20.1/dist/phaser.min.js'
];

self.addEventListener('install', function(event) {
  console.log('установка sw');
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      console.log('sw кеширует файлы');
      return cache.addAll(filesToCache);
    }).catch(function(err) {
      console.log(err);
    })
  );
});

Давайте рассмотрим код, который мы только что добавили:

  • Вначале мы объявили две переменные cacheName и filesToCache. Переменная cacheName используется для хранения имени кеша, который мы будем использовать для хранения кешированных версий наших файлов. Переменная filesToCache является массивом имен файлов, которые мы хотим кешировать для нашей игры.
  • Затем мы добавили прослушиватль событий для события установки Service Worker и предоставили функцию обратного вызова, которая будет запускаться при срабатывании данного события.
  • В функции обратного вызова мы вызвали метод event.waitUntil(), который принимает промис в качестве аргумента и использует его для определения успешности установки.
  • В методе event.waitUntil мы сначала вызываем метод caches.open(), который используется для открытия кеша в браузере пользователя. Этот метод берет имя кеша, который вы хотите открыть (cacheName) и возвращает промис, который будет преобразован в объект кеша, хранящийся в браузере пользователя.
  • Наконец, мы вызываем метод addAll() возвращаемого объекта cache. Этот метод берет массив строк filesToCache с URL-адресами файлов, которые мы хотим кешировать, и возвращает промис, который ничего не вернет, если все файлы будут кешированы. Важно отметить, что если какой-либо из файлов не загружается в кеш, весь этап установки завершится неудачно.

Это может быть сложновато для понимания, проще говоря мы сделали следующее:

  • Мы открыли кеш.
  • Мы кешировали наш список файлов.
  • Мы проверяем были ли файлы кешированы.

Теперь, с учетом нашей логики кеширования, мы должны добавить функционал, который позволит нам использовать кешированные ресурсы. Чтобы использовать кешированные ресурсы, нам нужно добавить новый прослушиватель для события fetch, который запускается каждый раз, когда пользователь посещает страницу после установки Service Worker. В sw.js добавте следующий код в конец файла:

self.addEventListener('fetch', (event) => {
  console.log('sw fetch');
  console.log(event.request.url);
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    }).catch(function (error) {
      console.log(error);
    })
  );
});

В приведенном выше коде мы делаем следующее:

  • Мы добавили прослушиватель события fetch для нашего Service Worker. Это событие вызывается каждый раз, когда делается запрос, который входит в область видимости нашего Service Worker.
  • Когда происходит событие, мы вызываем метод event.respondWith(). Этот метод перехватывает обращения к сети браузера и использует наш промис.
  • В event.respondWith() методе мы сначала вызвали метод caches.match() и передали ему объект event.request . Затем, если запрошенный ресурс был найден в кеше, мы возвращаем кешированный ресурс. Если запрошенный ресурс не был найден в кеше, мы получаем этот запрос и возвращаем ответ.

Тестирование в Chrome

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

Когда браузер обнаруживает изменение в файле Service Worker, он устанавливает новый Service Worker в фоновом режиме. Когда это происходит, ваш старый Service Worker все еще контролирует текущую страницу, на которой вы находитесь, и он перейдет в состояние waiting. Как только вы закроете текущую страницу или перейдете на другой сайт, старый сервисный работник будет уничтожен, а управление перейдет к новому.

Чтобы увидеть это в Chrome, если вы вернетесь на вкладку Application и щелкните на Service Workers, вы увидите текущий активный Service Worker и обновленный. Если вы закроете вкладку, в которой запущена ваша игра, и перезагрузите игру в другой вкладке, Service Worker должен показать, что он обновлен. ! Чтобы обойти это, вы можете установить флажок Update on reload в верхней части вкладки Service Workers. В этом случае Chrome автоматически установит обновленный Service Worke.

Теперь, если вы посмотрите на вкладку Console, вы должны увидеть некоторые события fetch. Чтобы увидеть, будет ли наша игра работать, когда мы не в сети, мы можем либо выключить сервер, на котором расположена наша игра, либо вернуться на вкладку Service Workers и установить флажок Offline. Теперь, если вы попытаетесь перезагрузить свою игру, вы увидите, что Service Worker загружает ресурсы для нашей игры из кеша, и что наша игра доступна для игры в автономном режиме.

Управление кешем Если вы когда-нибудь захотите заставить Service Worker использовать новую версию кеша или решите использовать несколько кешей, то вам потребуется способ очистки старых кешей на пользовательском устройстве. Для этого мы можем добавить слушатель события для события activate. Это событие вызывается каждый раз, когда Service Worker получает контроль, и мы будем использовать это событие для очистки нашего кеша.

Добавьте в конец файла sw.js:

self.addEventListener('activate', function(event) {
  console.log('событие activate sw');
  event.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (key !== cacheName) {
          console.log('удаление старого кеша sw', key);
          return caches.delete(key);
        }
      }));
    })
  );
});

Давайте рассмотрим код, который мы только что добавили:

  • Сначала мы добавили прослушиватель для события activate и передали ему функцию обратного вызова, которая будет запускаться при срабатывании этого события.
  • В этой функции обратного вызова мы используем метод caches.key(), чтобы получить все текущие кеши для нашего Service Worker.
  • Затем мы перебираем все эти ключи и удаляем их, если они не равны переменной cacheName - имя текущего кеша.

Добавление на наш домашний экран

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

Chrome используется для автоматического отображения запроса на добавление PWA на домашний экран пользователя, однако, начиная с Chrome 68, Chrome больше не делает это автоматически, и вы должны прослушивать beforeinstallprompt.

Для запуска Chrome beforeinstallprompt ваш PWA должен соответствовать следующим критериям:

  • Веб-приложение еще не установлено.
  • Пользователь должен быть на сайте в течение 30 секунд.
  • В веб-приложении есть файл манифеста, который включает в себя следующее:
    • short_name или же name;
    • icons должны включать значки размером 192px и 512px;
    • start_url;
    • display должен быть один из: fullscreen, standalone, minimal-ui.
  • Должен быть доступен по протоколу HTTPS (или через localhost).
  • Имеет зарегистрированный Service Worker, в котором есть обработчик события fetch.

Давайте начнем для этого добавлять код в нашу игру. Первое, что мы сделаем, это создадим файл манифеста. Файл манифеста веб-приложения представляет собой простой JSON-файл, который сообщает браузеру о вашем веб-приложении и о том, как оно должно вести себя при установке на устройстве пользователя. В своем проекте создайте новый файл с именем manifest.json и добавьте в него следующий код:

{
    "name": "Приложение Phaser PWA",
    "short_name": "PWA_Phaser",
    "description": "Шаблон оффлайн веб-приложения.",
    "start_url": "/",
    "background_color": "#000000",
    "theme_color": "#0f4a73",
    "display": "standalone",
    "icons": [{
      "src": "img/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },{
      "src": "img/icon-256.png",
      "sizes": "256x256",
      "type": "image/png"
    },{
      "src": "img/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }]
}

Давайте рассмотрим код, который мы только что добавили:

  • Свойства name и short_name - названия, которые показываются пользователю на домашнем экране и в строке, которая отображается пользователю при установке приложения.
  • Свойство icons представляет собой массив иконок , которые будут использоваться для значка приложения на главном экране и при запуске приложения.
  • Свойство start_url сообщает браузеру начальный адрес при запуске.
  • Свойство display используется для настройки пользовательского интерфейса браузера, который отображается, когда приложение запускается. Вы можете прочитать больше о различных вариантах здесь.
  • Свойство theme_color используется для управления цветом на панели задач.
  • Свойство background - цвет, который используется на заставке веб-приложения.

Имея код для файла манифеста, нам просто нужно обновить наш index.html файл. Откройте index.html и замените весь код в файле следующим кодом:

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta charset="utf-8">
        <meta name="theme-color" content="black" />
        <link rel="manifest" href="manifest.json" />
        <link rel="icon" href="img/icon-192.png" sizes="192x192" />
        <link rel="icon" href="img/icon-256.png" sizes="256x256" />
        <link rel="icon" href="img/icon-512.png" sizes="512x512" />
        <link rel="stylesheet" href="css/style.css" />
    </head>
    <body>
        <!-- Модальное окно -->
        <div id="myModal" class="modal">
            <!-- Контент в модальном контенте -->
            <div class="modal-content">
                <span class="close">&times;</span>
                <p>Добавить на домашний экран?</p>
                <button onclick="offlinePrompt()">Установить</button>
            </div>
        </div>
        <script>
            if ('serviceWorker' in navigator) {
                window.addEventListener('load', function() {
                    navigator.serviceWorker.register('/sw.js', {scope: '/'}).then(function(registration) {
                        console.log('ServiceWorker зарегистрирован с областью видимости в пределах: ', registration.scope);
                    }, function(err) {
                        console.log('Регистрация ServiceWorker не удалась: ', err);
                    });
                });
            }

            let deferredPrompt;
            window.addEventListener('beforeinstallprompt', function (e) {
                console.log('beforeinstallprompt запущен');
                e.preventDefault();
                deferredPrompt = e;
                modal.style.display = 'block';
            });
            // Получаем модальное окно
            var modal = document.getElementById('myModal');
            // Получаем элемент <span> закрывающее модальное окно
            var span = document.getElementsByClassName('close')[0];
            // Если пользователь щелкает за пределами модального окна - оно закрывается
            window.onclick = function(event) {
                if (event.target == modal) {
                    modal.style.display = 'none';
                }
            }
            // Если пользователь щелкает <span> (x), модальное окно закрывается
            span.onclick = function() {
                modal.style.display = 'none';
            }
            function offlinePrompt() {
                deferredPrompt.prompt();
            }
        </script>
        <script src="https://cdn.jsdelivr.net/npm/phaser@3.20.1/dist/phaser.min.js"></script>
        <script src="js/game.js"></script>
    </body>
</html>

В приведенном выше коде мы сделали следующее:

  • В разделе <head> нашего кода мы добавили метатег, чтобы сделать нашу игру отзывчивой. Мы также добавили ссылку на наш файл манифеста и добавили ссылки на наши иконки.
  • В разделе <body> нашего кода мы добавили модальный окно в нашу игру. Оно содержит кнопку установки, и при нажатии на нее будет отображаться приглашение Chrome для установки PWA. По умолчанию мы скрываем это окно и показываем его только при запуске beforeinstallprompt.
  • Наконец, в разделе <script> мы добавили прослушиватель для события beforeinstallprompt, а в функции обратного вызова мы предотвращаем событие по умолчанию, мы сохраняем это событие для последующего доступа и, наконец, отображаем наше модальное окно. Когда нажата кнопка установки в модальном окне, мы вызываем метод prompt() для этого события.

Теперь, если вы сохраните и перезагрузите свою игру, вы можете проверить функциональность добавления на домашний экран. Предварительно нужно удалить старую версию приложения на странице chrome://apps. Когда вы разрабатываете свой PWA, вы можете включить локальное обновление. Чтобы сделать это, вам нужно включить флаг #enable-desktop-pwas-local-updating в Chrome, начиная с версии Chrome 67. Чтобы включить этот флаг, вы можете посетить chrome://flags/#enable-desktop-pwas-local-updating, а затем выбрать из раскрывающего списка значение Enabled.

Как только вы это сделаете, то локальные изменения в манифесте будут сразу синхронизироваться с приложением. Например, измените в файле manifest.json "name": "Измененное приложение Phaser PWA". Это сразу отобразится на приложении. Теперь у вас есть первая автономная игра Phaser, которая представляет собой PWA.

Оригинал