Загрузка...

Немного про PWA, создаём offline сайт

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

Тем не менее сайт, который использует pwa имеет ряд крутых преимуществ:

  • Открываемый сайт в браузере, может устанавливаться на планшеты и смартфоны. А вместе с этим можно добраться до функции ОС в смартфоне или компе
  • Оффлайн режим благодаря кэшированию контента, так что сёрфить сайт можно и без интернета
  • Выбор стратегии кэширования, управление сетевыми запросами и, как следствие, широкий простор для фантазии разработчика
  • Push уведомления

Браузер Chrome полностью поддерживает API PWA по состоянию на 2019 год.

Такие сайты в браузере запросто устанавливаются в смартфон или планшет пользователя как отдельная программа (такая же как сам браузер) и их можно считать почти полноценными нативными приложениями! PWA — это уже среднее звено между сайтом и приложением.

Для того чтобы сайт в браузере понял новые требования к нему, настраиваем HTTPS и регистрируем конфигурационный файл (service-worker.js) для ServiceWorker. Код, располагающийся в основном шаблоне на всех страницах, в этом случае представляется примерно таким образом:

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);

            if (registration.installing) {
                console.log('Service worker installing');
            } else if (registration.waiting) {
                console.log('Service worker installed');
            } else if (registration.active) {
                console.log('Service worker active');
            }

        }, function (err) {
            // registration failed :(
            console.log('ServiceWorker registration failed: ', err);
        }).catch(function (err) {
            console.log(err)
        });
    });
} else {
    console.log('service worker is not supported');
}

Код проверяет наличие API ServiceWorker в браузере. Если serviceWorker присутствует, то вызывается метод регистрации конфига (navigator.serviceWorker.register( ... )). В конфиге указываются события, которые прослушиваются сервисным работником, перед тем как обратиться к серверу, при первом запуске, при очистке кэша и т.д. Возникает такая ситуация, что можно теперь контролировать процесс обращения к серверу: получается некий прокси (или своебразный middleware), которым можно воспользоваться для различных стратегий работы сайта.

События

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

Install

У себя в блоге я взял за основу кейс автономного сайта, то есть чтобы ресурсы, заметки, страницы и теги были доступны без интернета (offline mode), но если сеть есть, то обращаться к серверу, а не к кэшу (NetworkFirst). Причём если идёт обращение к ресурсу, которого не оказалось в кэше, то добавить его туда, чтобы когда сеть пропадёт, доставать из кэша. Смысл в том что при первом запуске сайта в браузере, формируется кэш, куда кладутся все заметки, скрипты и стили. Это происходит в событии install конфигурационного скрипта. В общем случае код событие install:

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches
            .open(CACHE)
            .then((cache) => {
                fetch(`/give/me/all/urls_for_site`)
                    .then(response => {
                        // ...
                        // handle response AND get urls (rel paths array string[])
                        // urls string[]
                        return cache.addAll(urls);
                    })
            })
    );
});

где urls массив относительных путей ваших страниц, скриптов и стилей

Activate

Это событие полезно для очистки старого кэша, когда код сервис воркера обновляется. Данное действие удаляет файлы, которые принадлежат старому хранилищу. В общем случае вы обязаны очищать мусор в браузере пользователя, но даже если вы этого не сделаете, то браузер сам вычистит все старые файлы кэша при достижении определённого лимита. Например в хроме кэш не больше 150 МБ, дальше он удаляет файлы сам. Примерный код очистки:

self.addEventListener('activate', event => {
    // clear old cache
    const cacheWhitelist = [CACHE];
    event.waitUntil(
        caches.keys()
            .then(keyList => {
                    return Promise.all(
                        keyList.map(key => {
                            if (!cacheWhitelist.includes(key)) {
                                console.log('del cache: ' + key);
                                return caches.delete(key);
                            } else {
                                return Promise.resolve();
                            }
                        })
                    )
                }
            )
    );
});

Fetch

Это событие прослушивает каждый запрос с сайта. Тут можно играться как угодно. Можно, например, если сайт статичный и очень редко обновляемый, положить все его ресурсы в кэш, а потом вообще всегда дёргать кэш (CacheFirst), вместо обращений к серверу. И серверу хорошо (его не дёргают) и пользователям хорошо, всё работает быстро и оффлайн. Но если сайт часто обновляется, то делаем NetworkFirst, то есть всегда обращаемся к серверу, а в случае отсутствия сети, дёргаем из кэша. А можно вообще сделать что-то среднее между этими подходами и еще задействовать пуш уведомления пользователям, чтобы сообщать о важных обновлениях страниц.

Приведу пример NetworkFirst для сайта с обновлением кэша, в случае, например если добавляется новая страница, а в кэше её нет, то добавляем новый ресурс в кэш. Таким образом кэш будет наполняемым динамически. От слов к коду:

self.addEventListener('fetch', function (event) {
    const {request} = event;
    const url = new URL(request.url);
    if (url.origin !== self.location.origin) {
        // dont handle foreign requests
        return fetch(request).catch(err => console.log(err));
    }

    event.respondWith(
        serverThenCacheStrategy(event.request)
            .catch(probablyNotFoundErr => {
                console.log(`serverThenCacheStrategy: ${probablyNotFoundErr}`);
                return fetch(event.request)
                    .then(probablyNotFoundResponse => probablyNotFoundResponse)
                    .catch(reason => console.log(`Another problem: ${reason}`))
            })
    );
});

Вся магия в event.respondWith методе. Что он вернёт, то и покажем клиенту. А покажем:

function serverThenCacheStrategy(request) {
    return fetch(request)
        .then((response) => {
            if (response.ok) {
                // online
                tryAddToCacheResponse(request).catch(reason => Promise.reject(`tryAddToCacheResponse: ${reason}`));
                return response;
            } else {
                // offline
                return getFromCache(request);
            }
        })
        .catch(() => getFromCache(request));
}

Здесь, в случае успешного обращения к серверу, попытаемся добавить response в cache:

function tryAddToCacheResponse(request) {
    return caches
        .open(CACHE)
        .then(cache => resolveCache(cache, request))
        .catch(() => caches.open(CACHE).then(cache => updateStorage(cache, request)));
}

function resolveCache(cache, request) {
    return cache
        .match(request)
        .then(matching => matching || Promise.reject('resolveCache::no match'))
        .catch(reason => Promise.reject(reason));
}

function updateStorage(cache, request) {
    return fetch(request).then(response => {
        cache.put(request, response.clone());
        return response;
    });
}

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

function getFromCache(request) {
    return caches
        .open(CACHE)
        .then((cache) =>
            cache
                .match(request)
                .then((matching) => {
                    return matching || Promise.reject(`no match: ${request.url}`);
                })
                .catch(reason => Promise.reject(reason))
        )
        .catch(reason => Promise.reject(reason));
}

Здесь каждое обращение к серверу, если нет сети, будет перехвачено сервис воркером и если есть такой ресурс в кэше, то вернуть его клиенту.

Полный код моего сервис воркера представлен здесь. Естественно, для каждого сайта, в зависимости от потребностей и стратегий кэширования/обновления, он свой.

После таких не сложных манипуляции сайт станет автономным и будет устанавливаться на рабочий стол смартфона, планшета, компьютера. Вид в мобильном:

mobile

Вид в десктопе:

desktop

Если нажимать на эти кнопки, то сайт из браузера "упакуется" в самостоятельное приложение и добавиться на рабочий стол, как будто вы установили сайт через play market, например. Потом можно запускать этот сайт, не открывая браузер, а выполняя это "приложение" (на самом деле хитро замаскированный браузер). Но для пользователей в конечном итоге выглядит как отдельное приложение. Вот такая интересная магия :). Но оффлайн режим вещь крутая.

Проверить что сайт использует pwa, можно открыв инспектор кода в хроме, например. Перейдите на вкладку аудит и создайте подробный отчёт:

audit pwa chrome

Ну и напоследок надо составить ещё файл manifest.json. Как и service-worker.js для каждого он индивидуальный, но структура его примерно общая, можно подсмотреть мой manifest.json.



Похожие заметки:

Верстка

Услуги » верстка страниц сайта

Открыть здесь

Скрипт динамической ширины

Скрипт для равномерного распределения блоков по ширине родительского контейнера. В качестве контейнера может выступать любой блок как определенной ширины, так и неопределенной, вплоть до body. Что умеет?

  • Нарезать блоки на одинаковую ширину в зависимости от заданного количества колонок
  • Генерировать нужное количество колонок
  • Проставлять clearfix после оканчивающей ряд колонки, чтобы вовремя отменить обтекание
  • Удалять лишние clearfix

Открыть здесь

Удобная разработка с livereload, установка browser-sync

Livereload — «живая» перезагрузка страниц в браузере, при изменении в файлах проекта. Обычно очень удобно при разработке нового проекта, когда постоянно вносятся и тестируются изменения, особенно если дебажим во многих вкладках и браузерах сразу

Открыть здесь


Перед тем как писать комментарии, рекомендую ознакомиться:

Markdown синтаксис »

Оформление кода »

Нужна аватарка »

Комментарии