Pull to refresh

PWA — это просто. Hello Habr

Reading time6 min
Views22K
Продолжаем знакомство с Progressive Web Applications. После теоретической прошлой части самое время перейти к практике.

Сегодня мы построим простое, но полноценное PWA «Hello Habr».




Приложение доступно по адресу https://altrusl.github.io/habr-pwa/hello-habr/. При открытии в браузере на мобильном устройстве возможно добавление ярлыка на домашний экран и запуск в полноэкранном режиме.

Если кто хочет попробовать рассматриваемый пример на своем компьютере, то Chrome позволяет работать локально с простыми PWA приложениями без установки сторонних веб серверов с SSL сертификатами.
Инструкция для запуска "Hello Habr" локально
Необходимо установить из интернет-магазина Chrome вот это или аналогичное расширение, являющееся локальным веб-сервером. Без поддержки PHP, естественно.



Файлы «Hello Habr» можно взять с GitHub-a — https://github.com/altrusl/habr-pwa/tree/master/hello-habr

Поместите всё в одну директорию и укажите ее веб серверу.


«Hello Habr» состоит из одной страницы. Он показывает на ней картинку (лого) и анимированную надпись.

"Hello Habr" code

index.html


<html>
    <head>
        <title>Hello Habr</title>
        <script src="hh.js"></script>
        <link rel="stylesheet" href="hh.css" />
        <script type="text/javascript">
            if ('serviceWorker' in navigator) {
                navigator.serviceWorker.register('/sw.js')
                .then(function(registration) {
                    console.log('Registration successful, scope is:', registration.scope);
                })
                .catch(function(error) {
                    console.log('Service worker registration failed, error:', error);
                });
            }
        </script>
    </head>
    <body>
        <div class="center">
            <p id="text"></p>
        </div>
        <div id="logo"></div>
    </body>
</html>

hh.css


@font-face {
    font-family: Zaplyv-Heavy;
    src: url(Zaplyv-Heavy.otf);
   }
   
body {
    display: flex;
    align-items: center;
    align-content: center; 
    justify-content: center; 
    overflow: auto;   
}

.center {
    font-family: Zaplyv-Heavy;
    font-size: 8vmax;
}

#logo {
    background-image: url(logo.jpg);
    background-size: 100%;
    width: 100px;
    height: 100px;
    position: absolute;
    top: 0;
    right: 0;
    margin: 10px;
}

hh.js


window.onload = function() {
    fetch("hh.txt?mode=nocache").then(data => data.text()).then(data => {
        animateText(data)
      });
}

function animateText(data) {
    var ele = document.getElementById("text"),
        txt = data.split("");
    var interval = setInterval(function(){
    if(!txt[0]){
        return clearInterval(interval);
    };
    ele.innerHTML += txt.shift();
    }, 150);
}

hh.txt


Hello Hubr


Также присутствует кастомный шрифт. Итого — минимальный полный набор ресурсов среднего веб сайта. Если открыть index.html в браузере, отобразится картинка и надпись. Надпись загружается javascript-ом через fetch из файла hh.txt — простейшая модель общего PWA приложения.

Если открывать без sw.js, то это будет обычный веб сайт. Добавим к нашим файлам Service Worker.

sw.js
// Caches
var CURRENT_CACHES = {
    font: 'font-cache-v1',
    css:'css-cache-v1',
    js:'js-cache-v1',
    site: 'site-cache-v1',
    image: 'image-cache-v1'
};

self.addEventListener('install', (event) => {
  self.skipWaiting();
    console.log('Service Worker has been installed');
});

self.addEventListener('activate', (event) => {
    var expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) {
        return CURRENT_CACHES[key];
    });
  
    // Delete out of date caches
    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(
                cacheNames.map(function(cacheName) {
                    if (expectedCacheNames.indexOf(cacheName) == -1) {
                        console.log('Deleting out of date cache:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );

    console.log('Service Worker has been activated');  
});

self.addEventListener('fetch', function(event) {
  console.log('Fetching:', event.request.url);  
  event.respondWith(async function() {
    const cachedResponse = await caches.match(event.request);
    if (cachedResponse) {
      console.log("\tCached version found: " + event.request.url);
      return cachedResponse;
    } else {        
      console.log("\tGetting from the Internet:" + event.request.url);
      return await fetchAndCache(event.request);
    }
  }());

});

function fetchAndCache(request) {

  return fetch(request)
  .then(function(response) {
    // Check if we received a valid response
    if (!response.ok) {
      return response;
      // throw Error(response.statusText);
    }
    
    var url = new URL(request.url);
    if (response.status < 400 &&
      response.type === 'basic' &&
      url.search.indexOf("mode=nocache") == -1 
      ) {
      var cur_cache;
      if (response.headers.get('content-type') && 
        response.headers.get('content-type').indexOf("application/javascript") >= 0) {
        cur_cache = CURRENT_CACHES.js;
      } else if (response.headers.get('content-type') && 
            response.headers.get('content-type').indexOf("text/css") >= 0) {
        cur_cache = CURRENT_CACHES.css;
      } else if (response.headers.get('content-type') && 
            response.headers.get('content-type').indexOf("font") >= 0) {
        cur_cache = CURRENT_CACHES.font;
      } else if (response.headers.get('content-type') && 
            response.headers.get('content-type').indexOf("image") >= 0) {
        cur_cache = CURRENT_CACHES.image;
      } else if (response.headers.get('content-type') && 
            response.headers.get('content-type').indexOf("text") >= 0) {
        cur_cache = CURRENT_CACHES.site;
      }
      if (cur_cache) {
        console.log('\tCaching the response to', request.url);
        return caches.open(cur_cache).then(function(cache) {
          cache.put(request, response.clone());
          return response;
        });
      }
    }
    return response;
  })
  .catch(function(error) {
    console.log('Request failed for: ' + request.url, error);
    throw error;
  });
}


Как видно, мы создаем пять кэшей для каждого вида ресурсов. Кэш site — для html файлов. Кэшируются все ресурсы, за исключением тех, у кого в GET query стоит «mode=nocache» — а это у нас запрос к файлу hh.txt со строкой для надписи.
Иногда можно видеть, что ресурс берется с дискового кэша. Это бывает частой проблемой при разработке приложений с Service Worker-ом, поэтому дисковый кэш (кэш браузера) лучше отключать. И не у себя в браузере, а на сервере — например, в
.htaccess
# Cache-Control Headers
<ifModule mod_headers.c>
  <FilesMatch (\.css|\.js|sprites\.png)$>
	Header unset ETag
	Header unset Expires
	Header set Cache-Control "no-cache"
  </FilesMatch>
</IfModule>

Логика работы sw.js простая — «Cache falling back to the network». Сперва запрашиваемый ресурс проверяется в кэше, если он там есть, то берется и возвращается браузеру оттуда. Если нет — получается из сети, возвращается браузеру, а копия ресурса помещается в кэш.

После первого открытия страницы index.html в консоли Chrom-a видны записи об установке и активации Service Worker-а. После второго открытия в хранилище создаются наши кэши и в них помещаются наши ресурсы. Также видно, что при последующих открытиях на веб сервер уходят только запросы к hh.txt, все остальные ресурсы берутся из Service Worker-a.

Скриншот


Хранящиеся локально index.html, hh.css, hh.js, hh.otf, logo.jpg — это и есть тот самый application shell, оболочка статичных ресурсов и данных, выполняющая роль оболочки программы на клиенте. Вся динамическая информация, необходимая для работы сайта, получается javascript запросами на сервер и отображением полученных данных в app shell-e. В нашем случае это запрос к text.txt.

Для того, чтобы называться функционально полноценным PWA, «Hello Habr» не хватает одного — иконки на домашнем экране смартфонов и запуска в полноэкранном режиме.

Для этого необходимо в index.html подключить манифест приложения:
manifest.json
{
  "short_name": "Hello Habr",
  "name": "Hello Habr - PWA example",
  "icons": [
    {
      "src": "logo3.jpg",
      "type": "image/jpg",
      "sizes": "192x192"
    },
    {
      "src": "logo2.jpg",
      "type": "image/jpg",
      "sizes": "512x512"
    }
  ],
  "start_url": "index.html",
  "background_color": "#3367D6",
  "display": "standalone",
  "scope": "/habr-pwa/hello-habr/",
  "theme_color": "#3367D6"
}

Подключается он в index.html:
<link rel="manifest" href="manifest.json">


После этого мобильные браузеры (каждый по-своему) предложат создать ярлык для приложения на домашнем экране. При запуске по ярлыку приложение будет открываться в standalone режиме — без браузерных элементов управления. Более подробней об опциях манифеста — на Google Developers.

Приложение «Hello Habr» в минимальной мере обладает всеми свойствами PWA и является им по сути. Как видно, для того, перевести простой сайт в PWA нужно просто подключить манифест и файл Service Worker-a. Используемый sw.js достаточно универсальный.

В следующий раз переведем в PWA готовый сайт на CMS Joomla (сайт «из коробки» с изначальными демо-данными). Причем, sw.js останется практически тем же.
Tags:
Hubs:
+26
Comments17

Articles