Service Workers — это виртуальный прокси между браузером и сетью. Наконец-то они решают проблемы, с которыми фронтенд-разработчики боролись годами — в первую очередь, как правильно кэшировать активы веб-сайта и сделать их доступными, когда устройство пользователя находится в автономном режиме.
Они выполняются в потоке, отдельном от основного кода JavaScript нашей страницы, и не имеют никакого доступа к структуре DOM. Это представляет подход, отличный от традиционного веб-программирования — API не блокирует и может отправлять и получать сообщения между различными контекстами. Вы можете дать Service Worker что-то для работы и получить результат, когда он будет готов, используя подход, основанный на Promise.
Они могут делать намного больше, чем «просто» предлагать автономные возможности, включая обработку уведомлений, выполнение сложных вычислений в отдельном потоке и т. д. Сервисные работники обладают достаточной силой, поскольку они могут контролировать сетевые запросы, изменять их, обслуживать пользовательские ответы, полученные из кэш или полностью синтезировать ответы.
Безопасность
Поскольку они настолько мощны, сервис-воркеры могут выполняться только в защищенных контекстах (имеется в виду HTTPS). Если вы хотите сначала поэкспериментировать, прежде чем запускать свой код в производство, вы всегда можете протестировать на локальном хосте или настроить страницы GitHub — оба поддерживают HTTPS.
Сначала офлайн
Паттерн «сначала оффлайн» или «сначала кешировать» — самая популярная стратегия предоставления контента пользователю. Если ресурс кэширован и доступен в автономном режиме, сначала верните его, прежде чем пытаться загрузить с сервера. Если его еще нет в кэше, загрузите его и кэшируйте для будущего использования.
«Прогресс» в PWA
При правильном внедрении в качестве прогрессивного улучшения сервис-воркеры могут принести пользу пользователям, у которых есть современные браузеры, поддерживающие API, предоставляя поддержку в автономном режиме, но ничего не сломают для тех, кто использует устаревшие браузеры.
Сервисные работники в приложении js13kPWA
Достаточно теории — давайте посмотрим исходный код!
Регистрация сервис-воркера
Мы начнем с просмотра кода, который регистрирует новый Service Worker в файле app.js:
ПРИМЕЧАНИЕ. Мы используем синтаксис стрелочных функций es6 в реализации Service Worker.
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('./pwa-examples/js13kpwa/sw.js');
};
Если API сервис-воркера поддерживается в браузере, он регистрируется на сайте с помощью метода ServiceWorkerContainer.register()
. Его содержимое находится в файле sw.js и может быть выполнено после успешной регистрации. Это единственный фрагмент кода Service Worker, который находится внутри файла app.js; все остальное, относящееся к Service Worker, записывается в самом файле sw.js.
Жизненный цикл сервис-воркера
После завершения регистрации файл sw.js автоматически загружается, затем устанавливается и, наконец, активируется.
Установка
API позволяет нам добавлять прослушиватели событий для ключевых событий, которые нас интересуют — первое — это событие install
:
self.addEventListener('install', (e) => {
console.log('[Service Worker] Install');
});
В прослушивателе install
мы можем инициализировать кеш и добавлять в него файлы для автономного использования. Наше приложение js13kPWA делает именно это.
Сначала создается переменная для хранения имени кеша, файлы оболочки приложения перечислены в одном массиве.
var cacheName = 'js13kPWA-v1';
var appShellFiles = [
'/pwa-examples/js13kpwa/',
'/pwa-examples/js13kpwa/index.html',
'/pwa-examples/js13kpwa/app.js',
'/pwa-examples/js13kpwa/style.css',
'/pwa-examples/js13kpwa/fonts/graduate.eot',
'/pwa-examples/js13kpwa/fonts/graduate.ttf',
'/pwa-examples/js13kpwa/fonts/graduate.woff',
'/pwa-examples/js13kpwa/favicon.ico',
'/pwa-examples/js13kpwa/img/js13kgames.png',
'/pwa-examples/js13kpwa/img/bg.png',
'/pwa-examples/js13kpwa/icons/icon-32.png',
'/pwa-examples/js13kpwa/icons/icon-64.png',
'/pwa-examples/js13kpwa/icons/icon-96.png',
'/pwa-examples/js13kpwa/icons/icon-128.png',
'/pwa-examples/js13kpwa/icons/icon-168.png',
'/pwa-examples/js13kpwa/icons/icon-192.png',
'/pwa-examples/js13kpwa/icons/icon-256.png',
'/pwa-examples/js13kpwa/icons/icon-512.png'
];
Далее во втором массиве генерируются ссылки на изображения для загрузки вместе с контентом из файла data/games.js. После этого оба массива объединяются с помощью функции Array.prototype.concat()
.
var gamesImages = [];
for(var i=0; i<games.length; i++) {
gamesImages.push('data/img/'+games[i].slug+'.jpg');
}
var contentToCache = appShellFiles.concat(gamesImages);
Затем мы можем управлять самим событием install
:
self.addEventListener('install', (e) => {
console.log('[Service Worker] Install');
e.waitUntil(
caches.open(cacheName).then((cache) => {
console.log('[Service Worker] Caching all: app shell and content');
return cache.addAll(contentToCache);
})
);
});
Здесь нужно объяснить две вещи: что делает ExtendableEvent.waitUntil
и что представляет собой объект caches
.
Service Worker не устанавливается до тех пор, пока не будет выполнен код внутри waitUntil
. Возвращает обещание — такой подход нужен, потому что установка может занять некоторое время, поэтому нам нужно дождаться ее завершения.
caches
— это специальный CacheStorage
объект, доступный в рамках данного Service Worker для включения возможности сохранения данных — сохранение в веб-хранилище не будет работать, потому что веб-хранилище синхронно. Вместо этого с Service Workers мы используем Cache API.
Здесь мы открываем кеш с заданным именем, затем добавляем в кеш все файлы, которые использует наше приложение, чтобы они были доступны при следующей загрузке (идентифицируется по URL-адресу запроса).
Активация
Существует также событие activate
, которое используется так же, как install
. Это событие обычно используется для удаления любых файлов, которые больше не нужны, и очистки после приложения в целом. Нам не нужно делать это в нашем приложении, поэтому мы его пропустим.
Ответ на получение
В нашем распоряжении также есть событие fetch
, которое срабатывает каждый раз, когда HTTP-запрос отправляется из нашего приложения. Это очень полезно, так как позволяет нам перехватывать запросы и отвечать на них пользовательскими ответами. Вот простой пример использования:
self.addEventListener('fetch', (e) => {
console.log('[Service Worker] Fetched resource '+e.request.url);
});
Ответом может быть что угодно: запрошенный файл, его кешированная копия или фрагмент кода JavaScript, который будет делать что-то конкретное — возможности безграничны.
В нашем примерном приложении мы обслуживаем контент из кеша, а не из сети, пока ресурс действительно находится в кеше. Мы делаем это независимо от того, находится ли приложение в сети или в автономном режиме. Если файла нет в кеше, приложение сначала добавляет его туда, а затем обслуживает:
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then((r) => {
console.log('[Service Worker] Fetching resource: '+e.request.url);
return r || fetch(e.request).then((response) => {
return caches.open(cacheName).then((cache) => {
console.log('[Service Worker] Caching new resource: '+e.request.url);
cache.put(e.request, response.clone());
return response;
});
});
})
);
});
Здесь мы отвечаем на событие выборки функцией, которая пытается найти ресурс в кеше и возвращает ответ, если он там есть. Если нет, мы используем другой запрос на выборку, чтобы получить его из сети, затем сохраняем ответ в кеше, чтобы он был доступен там при следующем запросе.
Метод FetchEvent.respondWith
берет на себя управление — это часть, которая функционирует как прокси-сервер между приложением и сетью. Это позволяет нам отвечать на каждый отдельный запрос любым ответом, который мы хотим: подготовленным Service Worker, взятым из кеша, измененным при необходимости.
Вот и все! Наше приложение кеширует свои ресурсы при установке и обслуживает их с помощью выборки из кеша, поэтому оно работает, даже если пользователь находится в автономном режиме. Он также кэширует новый контент всякий раз, когда он добавляется.
Обновления
Остается еще один вопрос: как обновить Service Worker, когда доступна новая версия приложения, содержащая новые активы? Ключом к этому является номер версии в имени кеша:
var cacheName = 'js13kPWA-v1';
Когда это обновится до версии 2, мы сможем добавить все наши файлы (включая наши новые файлы) в новый кеш:
contentToCache.push('/pwa-examples/js13kpwa/icons/icon-32.png');
// ...
self.addEventListener('install', (e) => { e.waitUntil( caches.open('js13kPWA-v2').then((cache) => { return cache.addAll(contentToCache); }) ); });
В фоновом режиме устанавливается новый сервис-воркер, а предыдущий (v1) работает корректно до тех пор, пока не исчезнут использующие его страницы — тогда новый сервис-воркер активируется и берет на себя управление страницей от старого.
Очистка кеша
Помните событие activate
, которое мы пропустили? Его можно использовать для очистки старого кеша, который нам больше не нужен:
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(keyList.map((key) => {
if(key !== cacheName) {
return caches.delete(key);
}
}));
})
);
});
Это гарантирует, что в кеше будут только те файлы, которые нам нужны, поэтому мы не оставим после себя мусор; доступное место в кеше в браузере ограничено, поэтому рекомендуется почистить за собой.
Другие варианты использования
Обслуживание файлов из кеша — не единственная функция, которую предлагает Service Worker. Если вам нужно выполнить тяжелые вычисления, вы можете выгрузить их из основного потока и выполнить их в рабочем потоке и получить результаты, как только они будут доступны. С точки зрения производительности вы можете предварительно выбирать ресурсы, которые не нужны сейчас, но могут понадобиться в ближайшем будущем, поэтому приложение будет работать быстрее, когда вам действительно понадобятся эти ресурсы.
Резюме
В этой статье мы кратко рассмотрели, как заставить PWA работать в автономном режиме с помощью сервис-воркеров. Узнайте больше о концепциях, лежащих в основе Service Worker API, и о том, как его использовать более подробно.
Чтобы узнать о различиях между асинхронным и синхронным Javascript, посетите: https://medium.com/@ankitkamboj18/is-javascript-synchronous-or-asynchronous-what-the-hell-is-a-promise-302ee008dfcd
для получения дополнительной информации следуйте: https://medium.com/@ankitkamboj18