В этом коротеньком очерке я опишу те вещи о service workers, о которых я бы хотел прочесть год или хотя бы полгода назад и тем самым избежать очень долгого и мучительного периода отладки приложения.
Для тех, кто вообще не в курсе о чем речь, то очень вкратце — service worker это скрипт, который выполняется браузером в фоне, отдельно от веб-страницы и способен выполнять функции для которых не требуется взаимодействие со страницей или пользователем.
Полную документацию найти не сложно, но вот ссылка.
Еще мне очень пригодился вот этот материал и мне очень жаль что я невнимательно с ним ознакомился когда только начал знакомство с ServiceWorker API.
На практике Service Worker API позволяет делать такую магическую вещь, как кеширование файлов онлайн веб-приложения на локальное устройство пользователя и затем работать полностью в оффлайне, если нужно.
В будущем планируется добавить такие классные вещи как синхронизация кеша в фоне, то есть даже если пользователь не находится сейчас на вашем сайте, сервис-воркер все равно сможет запуститься и скачать обновления например. А также доступ к PushApi из фона опять же (то есть при получении обновления отправить вам пуш-уведомление).
Как это работает.
- Веб-страница регистрирует service worker
- Service worker устанавливается, активируется и начинает в фоне что-то делать. Например “слушать” события ‘fetch’ и при необходимости их изменять или отменять совсем
Используя Channel Messaging API веб-страница может отправлять сообщения service worker и получать от него ответы (и наоборот).
Service Worker НЕ может иметь доступа к DOM и данным веб-страницы, кроме как посредством сообщений.
Чего я не знал до недавнего времени, так это того, что даже если пользователь сейчас на вашем сайте, это никак не гарантирует того, что service worker будет работать все время. И ни в коем случае нельзя полагаться на global scope воркера.
То есть вот такая схема работы привела меня к очень плачевным последствиям и длительному процессу отладки приложения (внимание, плохой код, НЕ использовать ни в коем случае):
регистрация/установка service worker
Index.html
var regSW = require("./register-worker.js");
var sharedData = {filesDir: localDir};
regSW.registerServiceWorker(sharedData);
register-worker.js
var registerServiceWorker = function(sharedData){
navigator.serviceWorker.register('service-worker.js', { scope: './' })
.then(navigator.serviceWorker.ready)
.then(function () {
console.log('service worker registered');
sendMessageToServiceWorker(sharedData).then(function(data) {
console.log('service worker respond with message:', data);
})
.catch(function(error) {
console.error('send message fails with error:', error);
});
})
.catch(function (error) {
console.error('error when registering service worker', error, arguments)
});
};
var sendMessageToServiceWorker = function(data){
return new Promise(function(resolve, reject) {
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
if (event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
navigator.serviceWorker.controller.postMessage(data,
[messageChannel.port2]);
});
};
Код воркера, с прослушкой fetch и подменой ответа
service-worker.js
self.addEventListener('message', function(event) {
self.filesDir = event.data.filesDir;
event.ports[0].postMessage({'received': event.data});
});
self.addEventListener('fetch', function fetcher(event) {
let url = event.request.url;
if (url.indexOf("s3") > -1) {
//redirect to local stored file
url = "file://" + self.filesDir + self.getPath(url);
let responseInit = {
status: 302,
statusText: 'Found',
headers: {
Location: url
}
};
let redirectResponse = new Response('', responseInit);
event.respondWith(redirectResponse);
}
});
Что тут произошло:
- Мы зарегистрировали воркер и отправили ему в сообщении по какому локальному пути нужно искать закешированные ранее файлы.
- В сервис воркере мы получили сообщение и сохранили путь в global scope воркера в переменную self.filesDir.
- Воркер слушает событие fetch и на все что содержит в пути “s3” отвечает редиректом на локальный файл.
Оговорюсь что код сильно упрощен (конечно я не подменяю все что содержит s3 в пути, я не настолько ленив), но главное он показывает.
И все бы ничего, если бы не факт, что по истечению случайного количества времени (3-10 минут) работы приложения, service worker начинал перенаправлять запросы “в никуда”, а точнее в что-то типа «file://undefined/images/image1.png»
То есть спустя какое-то время переменная self.filesDir попросту удаляется и мы получаем тонну 404 file not found вместо картинок.
Естественно ни один уважающий себя программист не будет тестировать приложение целых 5 минут. Поэтому баг обнаруживает в лучшем случае тестер. А обычно даже клиент. Потому что сами знаете, эти тестеры… И вообще, тестирование никто не оплачивал, скажите спасибо что при старте не крашится.
В общем чтобы долго не затягивать, проблема в том, что если service worker не используется [какое-то время], то браузер его прибивает (извините не придумал более уместного перевода для слова terminate) и затем при следующем обращении стартует снова. Соответственно, новая копия воркера знать не знает о чем там его мертвый предшественник общался с веб-страницей и у него нет никаких сведений о том, откуда брать файлы.
Поэтому, если Вам нужно что-то сохранить — делайте это в перманентном хранилище, а именно в IndexedDB.
Еще одна заметка — говорят воркер не может использовать синхронные API, поэтому localStorage API не удастся использовать. Но я лично не пробовал.
Кстати, отладка в моем случае затянулась еще и потому что даже когда я тестировал долго-долго (минут 7) в надежде воспроизвести все-таки баг, у меня не получалось, так как при открытом окне Developer Tools коварный chrome не убивает воркер. Хоть и сообщает об этом лаконичным сообщением в логах “Service Worker termination by a timeout was canceled because DevTools is attached”
Собственно тут до меня и дошло, почему мои многократные попытки выяснить почему ServiceWorker у меня работает иначе чем у клиента на production провалились…
В общем после того как я убрал установку пути в переменной и перенес это в indexedDB мои несчастья закончились и мне снова начал нравиться ServiceWorker API.
А вот собственно рабочий пример кода, который можно взять и использовать в отличие от предыдущего:
регистрация/установка service worker
index.html
var regSW = require("./register-worker.js");
idxDB.setObject('filesDir', filesDir);
regSW.registerServiceWorker();
register-worker.js
var registerServiceWorker = function(){
navigator.serviceWorker.register('service-worker.js', { scope: './' })
.then(navigator.serviceWorker.ready)
.then(function () {
console.log('service worker registered');
})
.catch(function (error) {
console.error('error when registering service worker', error, arguments)
});
};
Код воркера, с прослушкой fetch и подменой ответа
service-worker.js
self.getLocalDir = function() {
let DB_NAME = 'localCache';
let DB_VERSION = 1;
let STORE = 'cache';
let KEY_NAME = 'filesDir';
return new Promise(function(resolve, reject) {
var open = indexedDB.open(DB_NAME, DB_VERSION);
open.onerror = function(event) {
reject('error while opening indexdb cache');
};
open.onsuccess = function(event) {
let db = event.target.result, result;
result = db.transaction([STORE])
.objectStore(STORE)
.get(KEY_NAME);
result.onsuccess = function(event) {
if (!event.target.result) {
reject('filesDir not set');
} else {
resolve(JSON.parse(event.target.result.value));
}
};
result.onerror = function(event) {
reject('error while getting playthroughDir');
};
}
});
};
self.addEventListener('fetch', function fetcher(event) {
let url = event.request.url;
if (url.indexOf("s3") > -1) {
//redirect to local stored file
event.respondWith(getLocalDir().then(function(filesDir){
url = "file://" + filesDir + self.getPath(url);
var responseInit = {
status: 302,
statusText: 'Found',
headers: {
Location: url
}
};
return new Response('', responseInit);
}));
});