Привет!
Progressive Web Application (далее PWA) возможно станут новой альтернативой устанавливаемым приложениям разработанным для определенной платформы.
На ресурсе developers.google.com приведено следующее определение 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, JS библиотека для добавления автономной поддержки вэб-приложений
- Webpack
- workbox-webpack-plugin
Надо отметить, что использование workbox-webpack-plugin очень сильно упрощает нашу задачу. В простейшем случае, все сводится к настройке workbox-webpack-plugin.
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 для проверки сетевой запрос или нет (другими словами: что мы загружаем данные или компоненты приложения):
const apiUrl = 'https://jsonplaceholder.typicode.com';
- массив с перечислением файлов, которые необходимо сохранять в кеше. Содержимое этих файлов сохраняется в кеше при первой загрузке приложения и будет использовано обозревателем для последующих загрузок приложения (пока не истечет срок жизни кеша) и для получения данных при отсутствии сети и пустого кеша:
const files = [
'./',
'./app.js',
'./style.css',
'./fallback/posts.json',
'./fallback/users.json',
];
- вспомогательный метод для решения сетевой запрос (обращение к API) или нет
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:
// загружаем 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. Здесь мы должны:
- загрузить модуль с методами для регистрации service worker;
- проверить возможность использовать service worker обозревателем по событию “load” регистрируем service worker вызовом метода registerServiceWorker в модуле reg_sw.js;
- загрузить данные 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
регистрируем servece worker
подписываемся на PUSH уведомления
Часть III:
жизненный цикл service worker
стратегии кеширования
использование workbox-webpack-plugin модуль GenerateSW
Исходники
Полностью с исходным кодом проекта описанного в статье можно ознакомиться на github по ссылке: https://github.com/stokaboka/pwa01
Спасибо за внимание!
Буду рад вашим замечаниям и критике. Эта статья была написана в учебных целях, чтобы разобраться с технологией PWA и Service Worker.
Top comments (0)