Сервис‑воркеры сегодня — не экзотика, а рабочий инструмент: offline‑режим, агрессивное кэширование и «живые» обновления. Под это уже давно есть отличный, проверенный Workbox, и я сам с ним работал.
Но по мере того, как я переносил один и тот же сервис‑воркер из проекта в проект, у меня всё чаще возникало ощущение, что мне нужен инструмент с другим профилем:
поменьше магии и слоёв абстракции;
попроще вход в код и в «как тут всё устроено»;
конструктор из маленьких плагинов, которые легко прочитать глазами и отладить.
Поэтому я написал @budarin/pluggable-serviceworker — не вместо Workbox, а как более лёгкий, типизированный и прозрачный рантайм для тех случаев, когда хочется полной управляемости и минимально возможной когнитивной нагрузки.
Почему не просто Workbox?
Сразу отвечу на ожидаемый вопрос: «А почему ты не взял Workbox, он же уже всё умеет?»
Я Workbox уважаю и использовал, но вживую столкнулся с несколькими моментами:
Сложность API и архитектуры
Workbox решает очень много задач и тащит за собой свою модель роутинга, стратегий, плагинов. Это мощно, но:чтобы уверенно в нём ориентироваться, нужно в голове держать довольно большой «словарь» понятий;
разобраться, как именно проходит запрос через все прослойки, — не всегда просто.
сложно что-то изменить: моё issue с предложением опциональной параллельной загрузки ресурсов висит уже несколько лет, хотя под ним подписалось большое количество разработчиков
Высокая когнитивная нагрузка при кастомизации
Когда нужно не стандартная стратегия кэширования, а «чуть‑чуть не так», начинаются компромиссы:либо подстраиваться под существующие стратегии;
либо уходить в свои обёртки вокруг Workbox и начинать помнить уже два уровня абстракции.
Сложность написания собственных плагинов
Строго говоря, это возможно, но для многих команд «написать свой плагин под Workbox» — это уже что‑то из разряда внутреннего фреймворка: не каждый разработчик готов в это погружаться.
Мне захотелось минималистичного контракта:
один тип
ServiceWorkerPluginс привычными хуками (install,activate,fetch,message,sync,push,periodicsync, а такжеbackgroundfetchsuccess/backgroundfetchfail/backgroundfetchabort/backgroundfetchclickдля Background Fetch API);предсказуемый порядок выполнения;
нормальное логирование и единый
onError;возможность собрать свой сервис‑воркер из очень простых кирпичиков (примитивов и пресетов), или вообще написать плагин «в лоб» за пару десятков строк.
Workbox отлично подходит, когда вы окей с его моделью и объёмом абстракций.
@budarin/pluggable-serviceworker — это вариант, когда хочется:
максимально простого модульного сервис‑воркера,
маленького рантайма без тяжёлых зависимостей,
и кода, который легко понять любому фронтендеру, даже не «гуру по сервис‑воркерам».
Дальше покажу, как библиотека выглядит вживую — на двух примерах: с нуля и из готовых блоков.
Пример 1. Свой сервис‑воркер с нуля: один плагин на установку и fetch
Напишем минималистичный, но полезный сервис‑воркер:
сервис-воркер устанавливается при первом посещении и вступает в силу только при перезагрузке/повторном посещении страницы
при установке — кладём статику в кеш;
при запросе ресурса — отдаём из кеша; если ничего нет — даём запросу пойти в сеть как обычно.
// precacheAndServePlugin.ts import type { Plugin } from '@budarin/pluggable-serviceworker'; export function precacheAndServePlugin(config: { cacheName: string; assets: string[]; }): Plugin { const { cacheName, assets } = config; return { name: 'precache-and-serve', install: async () => { const cache = await caches.open(cacheName); await cache.addAll(assets); }, fetch: async (event, logger) => { const cache = await caches.open(cacheName); const asset = await cache.match(event.request); if (!asset) { logger.debug( '[SW] cache miss', new URL(event.request.url).pathname ); } // если ресурс не найден в кэше - отдаем undefined - это сигнал библиотеке чтобы вызвать следующий плагин // если все плагины вернули undefined - библиотека сама запросит ресурс с сервера return asset ?? undefined; }, }; }
Код сервис‑воркера:
// sw.ts import { initServiceWorker } from '@budarin/pluggable-serviceworker'; import { precacheAndServePlugin } from './precacheAndServePlugin'; const options = { version: '1.0.0', onError: (error, event, type) => { console.error('[SW error]', type, error, event); }, }; initServiceWorker( [ precacheAndServePlugin({ cacheName: 'my-app-v1', assets: ['/', '/styles.css', '/main.js'], }), ], options );
Никаких явных self.addEventListener('install' | 'fetch') — только декларативное описание того, что должен делать плагин. Вход в код для нового человека — максимально простой.
Пример 2. Конструктор из готовых блоков: offline‑first + stale‑while‑revalidate(SWR) для запроса '/api/posts'
Теперь соберём что‑то посерьёзнее, но всё так же читаемое: страницы, скрипты и стили отдаём из кеша (приложение грузится быстро и работает офлайн), ленту постов — тоже из кеша, но в фоне подтягиваем свежие данные. Сервис‑воркер активируется сразу, без перезагрузки вкладки.
// sw.ts import { initServiceWorker, Plugin } from '@budarin/pluggable-serviceworker'; import { precache, serveFromCache, skipWaiting, claim, pruneStaleCache, staleWhileRevalidate, } from '@budarin/pluggable-serviceworker/plugins'; const staticCache = 'static-v1'; const postsCache = 'posts-v1'; const assets = ['/', '/main.js', '/styles.css']; const options = { version: '1.0.0', onError: (error, event, type) => { console.error('[SW error]', type, error, event); }, }; // код нашего плагина function postsSwrPlugin(config: { cacheName: string; pathPattern?: RegExp; }): Plugin { const { cacheName, pathPattern = /\/api\/posts(\/|$)/ } = config; const swr = staleWhileRevalidate({ cacheName }); return { name: 'posts-swr', async fetch(event, logger) { const url = new URL(event.request.url); if (!pathPattern.test(url.pathname)) { return undefined; } logger.info('[SW] posts SWR for', url.pathname); // вызываем код существующего плагина SWR return swr.fetch!(event, logger); }, }; } initServiceWorker( [ // install: будут вызываны параллельно precache({ assets, cacheName: staticCache }), skipWaiting(), // activate claim(), // fetch: цепочка вызовов пока кто-нибудь не вернет ресурс serveFromCache({ cacheName: staticCache }), postsSwrPlugin({ cacheName: postsCache }), // иначе запрос уйдет в сеть ], options );
Смотрим на массив плагинов — и видим весь сценарий:
какие кеши есть;
как активируется SW;
какие URL‑ы попадают под SWR‑логику;
что происходит, если ни один
fetch‑плагин не ответил - пакет сам сходит за ресурсом на сервер.
Регистрация на клиенте: обход бага с claim() и обновления
С клиентской стороны можно использовать готовые утилиты:
// main.ts import { isServiceWorkerSupported, registerServiceWorkerWithClaimWorkaround, onNewServiceWorkerVersion, onServiceWorkerMessage, pingServiceWorker, } from '@budarin/pluggable-serviceworker/client'; if (isServiceWorkerSupported()) { (async () => { // обходим баг Chrome const reg = await registerServiceWorkerWithClaimWorkaround('/sw.js'); const unsubscribeUpdate = onNewServiceWorkerVersion(reg, () => { // здесь можно показать баннер "Доступна новая версия приложения" console.log('New Service Worker version is ready!'); }); const unsubscribeMsg = onServiceWorkerMessage( 'SW_MSG_NEW_VERSION_READY', () => { // здесь – баннер "Новая версия установлена, перезагрузите страницу" console.log('New assets cached, you can reload the page.'); } ); window.addEventListener('focus', async () => { await pingServiceWorker(); }); // позже, когда подписки больше не нужны: unsubscribeUpdate(); unsubscribeMsg(); })(); }
Во время разработки был обнаружен и зарегистрирован баг в Google Chrome - во время 1-й установки сервис-воркера с вызовом claim() сервис-воркер активируется, но страница остается без контроллера до перезагрузки. issue-482903583
Что делает данный пакет особенным
Коротко:
Плагинная архитектура
Обычные объекты с хуками
install/activate/fetch/ …Каждый плагин делает свою задачу (кеш, авторизация, нотификации, SWR для API и т.д.).
Прозрачный порядок выполнения
Под капотом: под каждый тип событий (
install,activate,fetch,message,sync,periodicsync,push, события Background Fetch) создаётся свой массив обработчиков. Плагины сортируются поorder(по возрастанию, по умолчанию 0); в этом порядке обработчики попадают в массивы. Подписка на событие добавляется только если есть хотя бы один плагин для этого события. При наступлении события в сервис‑воркере вызываются обработчики из нужного массива.Порядок важен для
fetchиpush(последовательное выполнение). Для остальных событий (install,activate,message,sync,periodicsync, события Background Fetch) обработчики выполняются параллельно. Используйтеorderв конфиге плагина для управления порядком выполнения.install/activate/message/sync/periodicsync— обработчики выполняются параллельно;fetch— последовательно, с понятной логикой прерывания;push— все обработчики выполняются последовательно.
Централизованный
onErrorи нормальный логгер
Один обработчик ошибок для всего SW и единыйloggerctrace/debug/info/warn/errorво всех плагинах.Готовые примитивы, пресеты и типовые SW
Можно:писать свои плагины с нуля;
или собирать SW из готовых блоков:
плагины:
precache,cacheFirst,networkFirst,staleWhileRevalidate,serveFromCache,restoreAssetToCache,skipWaiting,claim,reloadClients,claimAndReloadClients,precacheMissing,precacheWithNotificationпресет:
offlineFirstготовые SW:
activateAndUpdateOnNextVisitSW,immediatelyActivateAndUpdateSW,immediatelyActivateUpdateOnSignalSW
Клиентские утилиты
Регистрация SW с обходом багаclaim(), подписка на новую версию, сообщения от SW,pingдля «пробуждения» и прочее.
Установка
npm install @budarin/pluggable-serviceworker # или pnpm add @budarin/pluggable-serviceworker
Где посмотреть живьём
Репозиторий: github.com/budarin/pluggable-serviceworker
Пакет в npm: @budarin/pluggable-serviceworker
В репозитории есть демо‑приложение на React + Vite в папке demo/:
пресет
offlineFirst;типовой сервис‑воркер
activateOnSignal;пример клиентского кода с
registerServiceWorkerWithClaimWorkaround,onNewServiceWorkerVersion,onServiceWorkerMessage,pingServiceWorkerи т.д.
Это нормальное живое приложение, а не «сферический пример в вакууме» — можно клонировать, запустить и посмотреть, как оно ведёт себя в браузере.
Когда @budarin/pluggable-serviceworker заходит лучше всего
Я не пытаюсь сказать «забудьте про Workbox». У него огромный функционал и своя ниша. Но есть сценарии, где хочется именно такого формата, как в этом пакете:
Нужен простой старт
Хочется, чтобы джун/мидл за вечер собрал рабочий SW и понимал, что там происходит, а не тонул в слоях абстракций.Проектов несколько, инфраструктура одна
Вы хотите переиспользовать одни и те же плагины (precache,auth, SWR‑кеш для API, нотификации) между разными фронтендами без копипасты и без втаскивания всего Workbox.Нужен полный контроль
Важно точно знать, в каком порядке и что выполняется вinstall/activate/fetch/push, и иметь одинonError, который видит все типы ошибок — от падения внутри плагина доunhandledrejectionв SW.Не хочется тянуть тяжёлый рантайм
Проект чувствителен к размеру бандла и количеству зависимостей, а задача — «просто собрать несколько плагинов и жить спокойно».
Если вы уже живёте с Workbox и он вас всем устраивает — отлично, продолжайте. Если же вы регулярно ловите себя на мысли «хочу сделать чуть‑чуть по‑своему, но лезть в кишки Workbox не хочется» — возможно, мой пакет как раз тот более лёгкий инструмент, которого не хватало между «писать всё руками» и «тянуть огромный фреймворк».
Итоги и что дальше
Мне хотелось, чтобы сервис‑воркер в проекте выглядел как набор явных, маленьких модулей, а не как один большой файл с десятком обработчиков событий и размытыми границами ответственности. Из этого желания и вырос @budarin/pluggable-serviceworker:
плагинный контракт, который можно объяснить за 5 минут;
предсказуемый порядок выполнения хуков;
централизованный
onErrorи единый логгер;куча готовых примитивов и пресетов, но без навязанной «мегастратегии».
Если вам близок такой подход — попробуйте собрать свой первый SW из одного‑двух плагинов, а потом постепенно достроить до нужного вам монстра.
Буду рад любому фидбеку: ишью в репозитории, идеям новых примитивов/пресетов, историям. Токсичному фидбэку рад не буду - не пишите ! 😊
