Хабрахабр

Подводные камни Service Workers

В этом коротеньком очерке я опишу те вещи о 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 провалились…

image

В общем после того как я убрал установку пути в переменной и перенес это в 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); }));
});

P.S. Автор не претендует на оригинальность, однако считает, что если данная статья будет найдена, прочитана и поможет хоть одному несчастному — оно того стоит.

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть