@budarin/pluggable-serviceworker
v1.2.1
Published
Extensible via plugins service worker
Downloads
382
Maintainers
Readme
@budarin/pluggable-serviceworker
🔌 Расширяемый через плагины Service Worker
Библиотека для создания модульных и расширяемых Service Worker'ов с помощью системы плагинов.
🚀 Почему этот пакет облегчает разработку?
Разработка Service Worker'ов традиционно сложна из-за необходимости вручную управлять множественными обработчиками событий, обработкой ошибок и порядком выполнения. Этот пакет решает эти проблемы:
🔌 Модульная архитектура
- Плагинная система позволяет разбивать функциональность на независимые модули
- Каждый плагин отвечает за свою задачу (кеширование, аутентификация, уведомления)
- Легко добавлять/удалять функциональность без изменения основного кода
- Не нужно думать об инфраструктурном коде в обработчиках событий - пишите простой код не думая о сложностях кода самого сервисворкера
🎯 Управление порядком выполнения
- Предсказуемый порядок - плагины без
orderвыполняются первыми, затем по возрастаниюorder - Гибкость - можно контролировать последовательность инициализации
- Масштабируемость - легко добавлять новые плагины в нужном месте
⚡ Оптимизированная логика выполнения
- Параллельно для
install,activate,message,sync- независимые задачи выполняются одновременно - Последовательно для
fetch,push- первый успешный результат прерывает цепочку - Производительность - правильный выбор стратегии для каждого типа события
🛡️ Централизованная обработка ошибок
- Единый обработчик для всех типов ошибок
- Типизированные ошибки - знаешь, что именно сломалось
- Изоляция - ошибка в одном плагине не ломает остальные
- Автоматическая обработка глобальных событий ошибок
📝 Удобное логирование
- Настраиваемый логгер с разными уровнями
- Контекстная информация в логах
- Отладка становится намного проще
📦 Установка
npm install @budarin/pluggable-serviceworkerили
pnpm add @budarin/pluggable-serviceworker🚀 Быстрый старт
Базовое использование
// sw.js
import {
initServiceWorker,
type ServiceWorkerPlugin,
type SwContext,
} from '@budarin/pluggable-serviceworker';
// Контекст: список ассетов для precache и имя кеша
interface PrecacheAndServeContext extends SwContext {
assets: string[];
cacheName: string;
}
const precacheAndServePlugin: ServiceWorkerPlugin<PrecacheAndServeContext> = {
name: 'precache-and-serve',
install: async (_event, context) => {
const cache = await caches.open(context.cacheName);
await cache.addAll(context.assets);
},
fetch: async (event, context) => {
const cache = await caches.open(context.cacheName);
return cache.match(event.request) ?? undefined;
},
};
// TypeScript проверит, что в options есть assets и cacheName
initServiceWorker([precacheAndServePlugin], {
logger: console,
assets: ['/', '/styles.css', '/script.js'],
cacheName: 'my-cache-v1',
});Важно: для fetch плагину не нужно самому вызывать fetch(event.request), если все плагины вернули undefined - фреймворк сам выполняет запрос в сеть. Во все обработчики плагинов вторым аргументом передаётся контекст — те же данные, что вы передали в initServiceWorker.
Демо
В папке demo/ — приложение React + Vite с пресетом offlineFirst и типовым сервис-воркером activateOnSignal. Запуск из корня: pnpm install && pnpm build, затем cd demo && pnpm install && pnpm run dev. Подробности и ссылки на публичные песочницы (StackBlitz, CodeSandbox) — в demo/README.md.
Open in StackBlitz · Open in CodeSandbox
initServiceWorker(plugins, options)
initServiceWorker — точка входа: она регистрирует обработчики событий Service Worker (install, activate, fetch, …) и прогоняет их через список плагинов.
plugins: массив плагиновoptions: один общий объект с настройками/данными, который будет доступен плагинам как контекст
Пример:
initServiceWorker([myPlugin], {
logger: console,
// ... тут будут поля контекста, которые требуют плагины ...
});⚙️ Конфигурация и контекст (options)
Функция initServiceWorker принимает второй параметр options типа ServiceWorkerInitOptions (контекст для плагинов + onError для библиотеки). В обработчики плагинов вторым аргументом передаётся контекст — часть этого объекта без onError (тип контекста — SwContext и ваши поля; при типизированных плагинах — пересечение требуемых ими полей).
interface SwContext {
logger?: Logger; // по умолчанию console
// сюда можно добавлять свои поля: version, assets, cacheName и т.д.
}
// В initServiceWorker передаётся ServiceWorkerInitOptions = SwContext + onError:
interface ServiceWorkerInitOptions extends SwContext {
onError?: (error, event, errorType?) => void; // только для библиотеки, в плагины не передаётся
}В тип контекста, который видят плагины, входит только SwContext и ваши поля; onError в этот тип не входит и используется только библиотекой.
Формируйте объект options в своём сервис-воркере (контекст для плагинов + при необходимости onError) и передавайте его в initServiceWorker. В плагины передаётся тот же объект как контекст — плагины получают доступ к полям контекста, а onError остаётся внутренним делом библиотеки.
Поля конфигурации
logger?: Logger (опциональное)
Объект для логирования с методами info, warn, error, debug. По умолчанию используется console. Может быть передан любой объект, реализующий интерфейс Logger.
interface Logger {
trace: (...data: unknown[]) => void;
debug: (...data: unknown[]) => void;
info: (...data: unknown[]) => void;
warn: (...data: unknown[]) => void;
error: (...data: unknown[]) => void;
}Пример:
const logger = console; // Использование стандартного console
const options = {
logger,
// или
logger: {
trace: (...data) => customLog('TRACE', ...data),
debug: (...data) => customLog('DEBUG', ...data),
info: (...data) => customLog('INFO', ...data),
warn: (...data) => customLog('WARN', ...data),
error: (...data) => customLog('ERROR', ...data),
},
};onError?: (error, event, errorType) => void (опциональное)
Единый обработчик для всех типов ошибок в Service Worker. Дефолтного обработчика ошибок нет - если onError не передан, ошибки будут проигнорированы (не обработаны).
Параметры:
error: Error | any- объект ошибкиevent: Event- событие, в контексте которого произошла ошибкаerrorType?: ServiceWorkerErrorType- тип ошибки (см. раздел "Обработка ошибок")
Важно: Если onError не указан, ошибки в плагинах и глобальные ошибки будут проигнорированы. Для production-окружения рекомендуется всегда указывать onError для логирования и мониторинга ошибок.
Пример минимальной конфигурации:
// Без onError - ошибки будут проигнорированы
initServiceWorker([cachePlugin], {});
// С onError - ошибки будут обработаны
initServiceWorker([cachePlugin], {
logger: console,
onError: (error, event, errorType) => {
console.error('Service Worker error:', error, errorType);
},
});Обработка ошибок
Библиотека позволяет описать единый обработчик для всех типов ошибок в Service Worker и выполнить обработку индивидуально каждого типа ошибки. Она сама подписывается на глобальные события error, messageerror, unhandledrejection, rejectionhandled; ошибка в одном плагине не останавливает выполнение остальных. Если внутри onError произойдёт исключение, оно логируется через options.logger.
import {
initServiceWorker,
ServiceWorkerErrorType,
} from '@budarin/pluggable-serviceworker';
const logger = console; // или свой объект с методами info, warn, error, debug
const options = {
logger,
onError: (error, event, errorType) => {
logger.info(`Ошибка типа "${errorType}":`, error);
switch (errorType) {
case ServiceWorkerErrorType.ERROR:
// JavaScript ошибки
logger.error('JavaScript error:', error);
break;
case ServiceWorkerErrorType.MESSAGE_ERROR:
// Ошибки сообщений
logger.error('Message error:', error);
break;
case ServiceWorkerErrorType.UNHANDLED_REJECTION:
// Необработанные Promise rejection
logger.error('Unhandled promise rejection:', error);
break;
case ServiceWorkerErrorType.REJECTION_HANDLED:
// Обработанные Promise rejection
logger.info('Promise rejection handled:', error);
break;
case ServiceWorkerErrorType.PLUGIN_ERROR:
// Ошибки в плагинах
logger.error('Plugin error:', error);
break;
default:
// Неизвестные типы ошибок
logger.error('Unknown error type:', error);
// можно даже так - отправка ошибки в аналитику
fetch('/api/errors', {
method: 'POST',
body: JSON.stringify({
error: error.message,
eventType: event.type,
url: event.request?.url,
timestamp: Date.now(),
}),
}).catch(() => {
// Игнорируем ошибки отправки логов
});
}
},
};
initServiceWorker(
[
/* ваши плагины */
],
options
);🔌 Интерфейс плагина
Каждый плагин реализует интерфейс ServiceWorkerPlugin<C>, где C extends SwContext — тип контекста, который плагин ожидает. Во все обработчики вторым аргументом передаётся контекст (те же данные, что в options при инициализации, без onError).
interface ServiceWorkerPlugin<C extends SwContext = SwContext> {
name: string;
order?: number;
install?: (event: ExtendableEvent, context?: C) => Promise<void> | void;
activate?: (event: ExtendableEvent, context?: C) => Promise<void> | void;
fetch?: (
event: FetchEvent,
context?: C
) => Promise<Response | undefined> | Response | undefined;
message?: (event: SwMessageEvent, context?: C) => void;
sync?: (event: SyncEvent, context?: C) => Promise<void> | void;
push?: (
event: PushEvent,
context?: C
) =>
| Promise<PushNotificationPayload | void>
| PushNotificationPayload
| void;
periodicsync?: (
event: PeriodicSyncEvent,
context?: C
) => Promise<void> | void;
}Плагин может объявить требуемый контекст через дженерик: ServiceWorkerPlugin<SwContext & { assets: string[]; cacheName: string }>. Тогда TypeScript потребует передать в initServiceWorker объект options с полями assets и cacheName (при вызове с литералом массива плагинов тип options выводится автоматически).
📝 Описание методов
| Метод | Событие | Возвращает | Описание |
| -------------- | -------------- | --------------------------------- | -------------------------------------------------------------- |
| install | install | void | Инициализация плагина при установке SW |
| activate | activate | void | Активация плагина при обновлении SW |
| fetch | fetch | Response \| undefined | Обработка сетевых запросов |
| message | message | void | Обработка сообщений от основного потока |
| sync | sync | void | Синхронизация данных в фоне |
| push | push | PushNotificationPayload \| void | Данные для уведомления; библиотека вызывает showNotification |
| periodicsync | periodicsync | void | Периодические фоновые задачи |
🎯 Особенности обработчиков
- Во все методы вторым аргументом передаётся контекст (данные из объекта, переданного в
initServiceWorker, безonError). Параметр можно не использовать, если плагину контекст не нужен. fetch: ВозвращаетResponseдля завершения цепочки илиundefinedдля передачи следующему плагину. Если все плагины вернулиundefined, фреймворк вызываетfetch(event.request).push: Как и fetch — возвращаетPushNotificationPayload(объект для Notification API) илиundefined. Тип экспортируется из пакета. Первый плагин, вернувший объект сtitle, «выигрывает»: библиотека вызываетself.registration.showNotification(title, options). Если все вернулиundefined, уведомление не показывается.- Остальные обработчики (
install,activate,message,sync,periodicsync): возвращаемое значение не используется; фреймворк вызывает метод каждого плагина по очереди, цепочка не прерывается. - Все обработчики опциональны — реализуйте только нужные события.
🔄 Обновление Service Worker (skipWaiting / clients.claim)
Библиотека не вызывает skipWaiting() и clients.claim() — это поведение задаётся индивидуально в каждом проекте и оставлено на усмотрение плагинов. При необходимости вызывайте их в своих обработчиках install и activate (в библиотеке реализованы данные примитивы - смотри ниже):
const updatePlugin = {
name: 'update-plugin',
install: (event) => {
self.skipWaiting();
},
activate: (event) => {
event.waitUntil(self.clients.claim());
},
};🎯 Порядок выполнения
Плагины выполняются в следующем порядке:
- Сначала ВСЕ плагины без
order- в том порядке, в котором они были добавлены - Затем плагины с
order- в порядке возрастания значенийorder
Пример:
const plugins = [
{ name: 'first' }, // без order - выполняется первым
{ name: 'fourth', order: 2 },
{ name: 'second' }, // без order - выполняется вторым
{ name: 'third', order: 1 },
{ name: 'fifth' }, // без order - выполняется третьим
];
// Порядок выполнения: first → second → fifth → third → fourthПреимущества новой системы:
- 🎯 Предсказуемость - плагины без
orderвсегда выполняются первыми - 🔧 Простота - не нужно знать, какие номера уже заняты
- 📈 Масштабируемость - легко добавлять новые плагины в нужном порядке
⚡ Логика выполнения обработчиков
Разные типы событий Service Worker обрабатываются по-разному в зависимости от их специфики:
🔄 Параллельное выполнение
События: install, activate, message, sync, periodicsync
Все обработчики выполняются одновременно с помощью Promise.all():
// Все плагины инициализируются параллельно
const installPlugin1 = {
name: 'cache-assets',
install: async () => {
/* кеширование ресурсов приложения*/
},
};
const installPlugin2 = {
name: 'cache-ext',
install: async () => {
/* кэширование вспомогательных ресурсов */
},
};
// Оба install обработчика выполнятся одновременноПочему параллельно:
- install/activate: Все плагины должны инициализироваться независимо
- message: Все плагины должны получить сообщение одновременно
- sync: Разные задачи синхронизации независимы (синхронизация данных + кеша)
- periodicsync: Периодические задачи независимы друг от друга
➡️ Последовательное выполнение
События: fetch, push
Обработчики выполняются по очереди до первого успешного результата:
Fetch - с прерыванием цепочки
const authPlugin = {
name: 'auth',
// Без order - выполняется первым
fetch: async (event) => {
if (needsAuth(event.request)) {
return new Response('Unauthorized', { status: 401 }); // Прерывает цепочку
}
return undefined; // Передает следующему плагину
},
};Почему последовательно:
- fetch: Нужен только один ответ, первый успешный прерывает цепочку. Если никто не вернул ответ — выполняется
fetch(event.request) - push: Плагин может вернуть данные для уведомления типа
PushNotificationPayload|undefined. Библиотека один раз вызываетshowNotificationпо первому вернувшемуся payload; цепочка прерывается — одно уведомление, без конфликтов.
📋 Сводная таблица
| Событие | Выполнение | Прерывание | Причина |
| -------------- | --------------- | ---------- | ------------------------------------------------------ |
| install | Параллельно | Нет | Независимая инициализация |
| activate | Параллельно | Нет | Независимая активация |
| fetch | Последовательно | Да | Нужен один ответ |
| message | Параллельно | Нет | Все получают сообщение |
| sync | Параллельно | Нет | Независимые задачи |
| periodicsync | Параллельно | Нет | Независимые периодические задачи |
| push | Последовательно | Да | Один показ уведомления (библиотека по первому payload) |
Примитивы, пресеты и типовые сервис-воркеры
Примитивы (плагины)
Один примитив — одна операция. Импорт: @budarin/pluggable-serviceworker/plugins.
| Название | Событие | Описание |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| precache | install | Кеширует список context.assets в кеш context.cacheName. |
| skipWaiting | install | Вызывает skipWaiting(). |
| claim | activate | Вызывает clients.claim(). |
| claimOnMessage | message | При сообщении с event.data.type === context.claimMessageType (по умолчанию 'SW_ACTIVATE') вызывает skipWaiting(). clients.claim() вызывается плагином claim в activate. |
| serveFromCache | fetch | Отдаёт из кеша; при промахе — undefined. |
| restoreAssetToCache | fetch | Для URL из context.assets: сначала из кеша; если в кеше нет — запрос с сервера, в кеш, ответ браузеру. |
| cacheFirst | fetch | Кеш → при промахе сеть, ответ в кеш. |
| networkFirst | fetch | Сеть → при ошибке/офлайне из кеша. |
| staleWhileRevalidate | fetch | Отдаёт из кеша, в фоне обновляет кеш из сети. |
Контекст для кеширующих примитивов: OfflineFirstContext (assets, cacheName, опционально claimMessageType). Импортируйте тип из основного пакета.
Пресеты
Комбинации примитивов (стратегии кеширования). Импорт: @budarin/pluggable-serviceworker/presets.
| Название | Состав | Назначение | | ---------------- | ------------------------- | ------------------------------------ | | offlineFirst | precache + serveFromCache | Статика из кеша, при промахе — сеть. |
Стратегии networkFirst, staleWhileRevalidate и др. доступны как примитивы — собирайте свой кастомный сервис-воркер из примитивов и пресетов.
Типовые сервис-воркеры (из коробки)
Готовые точки входа по моменту активации (все с кешированием offline-first). Импорт: @budarin/pluggable-serviceworker/sw.
| Название | Описание | | ------------------------------------ | --------------------------------------------------------------------------------------------------- | | activateOnNextVisitServiceWorker | Кеширующий SW, активируется при следующем визите страницы. | | activateImmediatelyServiceWorker | Кеширующий SW, активируется сразу (skipWaiting + claim). | | activateOnSignalServiceWorker | Кеширующий SW, активируется по сигналу со страницы (сообщение с типом из options.claimMessageType). |
Пример использования типового SW:
// sw.js — точка входа вашего сервис-воркера
import { activateOnNextVisitServiceWorker } from '@budarin/pluggable-serviceworker/sw';
activateOnNextVisitServiceWorker({
assets: ['/', '/styles.css', '/script.js'],
cacheName: 'my-cache-v1',
logger: console,
onError: (err, event, type) => console.error(type, err),
});На странице регистрируйте этот файл: navigator.serviceWorker.register('/sw.js') (или путь, по которому сборка отдаёт ваш sw.js).
Режим разработки
Если нужно, чтобы в режиме разработки ни один из плагинов ничего не кэшировал и не пытался что-либо отдавать из кэша - в плагине и сервисворкере можно использовть import.meta.env.DEV для Vite для условного использования кэширования.
📄 Лицензия
MIT © Vadim Budarin
