Progressive Web Application:: Reliable. Part I
Tags: javascript, pwa, serwiceworker, workbox
Published: 2018-11-19 07:23:13

---
title: Progressive Web Application:: Reliable. Part I
published: true
description: Progressive Web Application:: Reliable. Part I
tags: #js #pwa #serwiceworker #workbox
---

Привет!

Progressive Web Application (далее PWA) возможно станут новой альтернативой устанавливаемым приложениям разработанным для определенной платформы.

На ресурсе [developers.google.com](https://developers.google.com/web/progressive-web-apps/) приведено следующее определение PWA:
* __Reliable __  Load instantly and never show the downasaur, even in uncertain network conditions.
* __Fast- Respond__ quickly to user interactions with silky smooth animations and no janky scrolling.
* __Egaging- Feel__ like a natural app on the device, with an immersive user experience.

Пока все выглядит довольно заманчиво:

* Один код для разных платформ
* Обозреватель интернета в качестве виртуальной машины, который есть практически везде
* Богатые возможности для создания UI
* Возможность использовать приложение без доступа к глобальной сети
* На устройстве PWA “выглядит” как нативное приложение
* И если с браузерами и UI все понятно: фактически мы делаем одно-страничное приложение, то что делать с оффлайн режимом и установкой на устройство?

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

> Основная цель - разобраться, что скрывается под понятием Reliable,  быстрая загрузка приложения и данных, возможность работать в отсутствие сети.

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

https://developers.google.com/web/progressive-web-apps/
https://developers.google.com/web/tools/workbox/
https://blog.logrocket.com/building-a-progressive-web-app-pwa-no-react-no-angular-no-vue-aefdded3b5e
https://vaadin.com/pwa/build/production-pwa-with-webpack-and-workbox

В сети уже имеется огромное множество статей на тему PWA, каждый может найти то, что больше подходит именно ему.

##Итак, начнем!##
Основное отличие PWA от Single Page Application (SPA) это установка на устройство, работа без доступа к сети или с частой потерей соединения и загрузка из кеша файлов приложения и данных.

Это возможно при использовании ServiceWorker (далее SW)  -  это JavaScript (далее JS) код, выполняющийся в отдельном потоке от потока страницы и выполняющий роль посредника (proxy) между вэб-приложением и сетью.

На данный момент SW поддерживает

* Push Notifications
* Background Sync

> Но на самом деле кеширование файлов в обозревателе с помощью SW вовсе не значит, что обязательно нужно делать именно PWA приложение.

> Мы можем использовать возможности кеширования файлов с помощью SW для локального хранения шрифтов, изображений и других компонентов web-приложения или web-страницы для ускорения загрузки.

###Постановка задачи###

Разработать простейшее PWA приложение и разработать простой service worker обеспечивающий online и offline работу нашего приложения.

###Решение###

Наше приложение будет простым, но подходящим для дальнейшего развития в более сложное решение.

Для этого мы будем использовать:

* [Google Workbox](https://developers.google.com/web/tools/workbox/), JS библиотека для добавления автономной поддержки вэб-приложений
* [Webpack](https://webpack.js.org/)
* [workbox-webpack-plugin](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin)

Надо отметить, что использование workbox-webpack-plugin очень сильно упрощает нашу задачу. В простейшем случае, все сводится к настройке workbox-webpack-plugin.

Workbox-webpack-plugin предоставляет [два способа](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin) внедрения в проект SW:

* __GenerateSW __ -  генерирует файл SW на основе минимальных настроек workbox-webpack-plugin. Простая интеграция SW в приложение. Но надо отметить, что за простотой базовых настроек по умолчанию кроется мощная система конфигурации SW, но это отдельная тема
* __InjectManifest__ -  свой или сторонний service worker, полный контроль над процессом настройками

Мы будем использовать оба варианта: GenerateSW используем мощь Workbox servce worker через настройки workbox-webpack-plugin; InjectManifest -  напишем свой простейший SW для понимания как это работает.

###Использование InjectManifest###
(Создадим свой SW (sw.js).)[https://github.com/stokaboka/pwa01/blob/master/src/sw.js]

Мы будем использовать стратегию _Network First_ -  запросы отправляются в сеть, если сеть недоступна пытаемся получить данные для запроса из кеша, если в кеше нет данных -  возвращаем данные-заглушки.

Создаем вспомогательные переменные:

* ссылка на API для проверки сетевой запрос или нет (другими словами: что мы загружаем данные или компоненты приложения):


```javascript
    const apiUrl = 'https://jsonplaceholder.typicode.com';
```


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


```javascript
    const files = [
        './',
        './app.js',
        './style.css',
        './fallback/posts.json',
        './fallback/users.json',
    ];
```


* вспомогательный метод для решения сетевой запрос (обращение к API) или нет


```javascript
function isApiCall(req) {
    return req.url.startsWith(apiUrl);
}

```


При первой загрузке приложения происходит процесс установки и инициализация кеша. Мы перехватываем событие “install”, инициализируем кеш с тегом “files” и сохраняем нужные нам файлы в кеше для последующего использования:


```
self.addEventListener('install', async e => {
    // получаем ссылку на кеш
    const cache = await caches.open('files');
    // сохраняем в кеше файлы приложения и заглушки для данных
    cache.addAll(files);
});
```


Создаем основные методы для обработки запросов нашего приложения, их будет три:

1) запрос в сеть, метод getFromNetwork

```
async function getFromNetwork(req) {
    // ссылка на кеш с тэгом "data"
    const cache = await caches.open('data');
    
    try {
        // выполняем запрос в сеть
        const res = await fetch(req);
        // сохраняем результат в кеш
        cache.put(req, res.clone());
        return res;
    } catch (e) {
        // упс, что-то пошло не так, сеть не работает!!!
        // извлекаем результат запроса из кеша
        const res = await cache.match(req);
        // возвращаем результат запроса если он найден в кеше
        // возвращаем данные-заглушки 
        // если в кеше нет результатов запроса
        return res || getFallback(req);
    }
}
```

2) поиск результата запроса в кеше, метод getFromCache
```
async function getFromCache(req) {
    // запрос в кеш
    const res = await caches.match(req);
    
    if (!res) {
        // в кеше нет данных для запроса
        // отправляем запрос в сеть
        // return fetch(req);
        return getFromNetwork(req)
    }
    
    return res;
}
```

3) данные-заглушки на случай отсутствия сети и отсутствия данных в кеше, метод getFallback. Эти файлы были сохранены в кеше при первой загрузке приложения
```
async function getFallback(req) {
    const path = req.url.substr(apiUrl.length);
    
    if (path.startsWith('/posts')) {
        return caches.match('./fallback/posts.json');
    } else if (path.startsWith('/users')) {
        return caches.match('./fallback/users.json');
    }
}
```

И теперь самый главный метод нашего _service worker_ -  перехват сетевых запросов (вызовы метода fetch обозревателя) из нашего web-приложения:

```
self.addEventListener('fetch', async e => {
    // извлекаем запрос из события
    const req = e.request;
    // запрос соответствует нашему api url - обращаемся в сеть
    // прочие запросы (html, css, js, json и любые другие файлы)
    // - пытаемся получить результаты из кеша
    // эти файлы являются частями нашего приложения и
    // сохраняются при первой загрузке
    const res = isApiCall(req) ?
        getFromNetwork(req) : getFromCache(req);
    // подсовываем событию "fetch" результат сформированный нами
    // в вызовах getFromNetwork или getFromCache
    // этот результат будет использован в нашем приложении
    await e.respondWith(res);
});
```


Мы написали свой первый service worker для PWA приложения!!!

Полный код файла можно посмотреть по ссылке: https://github.com/stokaboka/pwa01/blob/master/src/sw.js

##Настройка _workbox-webpack-plugin_ в режиме _InjectManifest_##
###Использование пользовательского или стороннего service worker.###

В нашем случае service worker  -  это наш файл sw.js

Файл конфигурации webpack [webpack.manifest.js](https://github.com/stokaboka/pwa01/blob/master/build-utils/presets/webpack.manifest.js):


```
// загружаем workbox-webpack-plugin
const {InjectManifest} = require('workbox-webpack-plugin');

// настраиваем workbox-webpack-plugin в режиме InjectManifest
module.exports = () => ({
    plugins: [
        new InjectManifest({
            // файл service worker написанный нами
            swSrc: './src/sw.js',
            // под именем service-worker.js наш файл попадет в сборку
            swDest: 'service-worker.js'
        })
    ]
});
```


Файл для подключения модуля workbox-webpack-plugin и его настройке в режиме __InjectManifest__:
https://github.com/stokaboka/pwa01/blob/master/build-utils/presets/webpack.manifest.js

Этот файл  -  часть конфигурации webpack которая относится к настройке workbox-webpack-plugin и service worker.

> Собственно webpack.config.js доступен по ссылке: https://github.com/stokaboka/pwa01/blob/master/webpack.config.js
Но здесь мы рассматриваем только вопросы связанные с использованием SW.


###Использование SW в web-приложении###

Результатом работы _workbox-webpack-plugin_ будет два сформированных плагином файла:

* service-worker.js, копия нашего файла sw.js с импортом файла манифеста ( precache-manifest…. ) и пакета workbox-sw.js
* precache-manifest.########################.js, файл манифеста, где ######################## случайный набор символов

Эти файлы копируются в папку сборки. Остается только подключить service worker в наше PWA приложение.

Создаем главный файл приложения (точку входа) app.js. Здесь мы должны:

1. загрузить модуль с методами для регистрации service worker;
2. проверить возможность использовать service worker обозревателем
по событию “load” регистрируем service worker вызовом метода _registerServiceWorker_ в модуле _reg_sw.js_;
3. загрузить данные PWA приложения и отобразить эти данные вызовом метода _loadMoreEntries_ в модуле _api.js_ (это относится к работе приложения и подробно не рассматривается);


```
// загружаем модуль с методами регистрации service worker
import { registerServiceWorker } from './reg_sw'
// методы загрузки данных
import { loadMoreEntries } from "./api";

// имя файла service worker
// этот файл сформирован на основе 
// нашего sw.js workbox-webpack-plugin - ом
// имя файла задано в файле настройке webpack.manifest.js
let serviceWorkerName = '/service-worker.js';

// проверяем возможность обозревателем использовать service worker
if ('serviceWorker' in navigator) {
    // ловим событие "load" - окончание загрузки всех компонентов
    window.addEventListener('load', async () => {
        // регистрируем service worker
        await registerServiceWorker(serviceWorkerName);
        // загружаем данные для работы PWA приложения
        loadMoreEntries();
    });
}
```



Обработаем событие _beforeinstallprompt_ предложение обозревателем установить PWA:


```
// ловим событие "beforeinstallprompt" - предложение установить
// PWA приложение в системе (при возможности)
// установка возможна только при загрузке по HTTPS
// с сертификатом web-сервера или при работе из песочницы localhost
window.addEventListener('beforeinstallprompt', e => {
    e.preventDefault();
    e.prompt();
});
```



##Заключение.##
Итак, мы разработали тестовое PWA с использованием: (Workbox)[https://developers.google.com/web/tools/workbox/], (workbox-webpack-plugin)[https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin]. Мы разработали свой собственный service worker с простейшей логикой, способный кешировать файлы приложения и загруженных данных. Для использования нашего _service worker_ мы использовали режим _InjectManifest_.

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

###Продолжение следует:###

__[Часть II](https://dev.to/stokaboka/progressive-web-applicationreliable-part-ii-c3p)__
регистрируем servece worker
подписываемся на PUSH уведомления


__Часть III:__
жизненный цикл service worker
стратегии кеширования
использование workbox-webpack-plugin модуль GenerateSW


###Исходники###
Полностью с исходным кодом проекта описанного в статье можно ознакомиться на github по ссылке: https://github.com/stokaboka/pwa01

Спасибо за внимание!

Буду рад вашим замечаниям и критике. Эта статья была написана в учебных целях, чтобы разобраться с технологией PWA и Service Worker.