Типизация кастомных стратегий

Workbox предоставляет мощный и гибкий механизм для создания сервис-воркеров, а также управления кэшированием и стратегиями получения ресурсов. Одним из ключевых аспектов является возможность определения кастомных стратегий, которые выходят за рамки стандартных стратегий (CacheFirst, NetworkFirst, StaleWhileRevalidate и т.д.). Для крупных проектов и проектов на TypeScript важна строгая типизация этих стратегий, чтобы обеспечить корректность кода и удобство автодополнения.

Основы кастомных стратегий

Кастомная стратегия в Workbox строится на наследовании от базового класса Strategy:

import { Strategy } from 'workbox-strategies';

class CustomStrategy extends Strategy {
  async _handle(request, handler) {
    // request – объект Request
    // handler – объект StrategyHandler
    const response = await handler.fetch(request);
    return response;
  }
}
  • Метод _handle является центральным элементом кастомной стратегии.
  • Параметр request — это объект класса Request, который представляет HTTP-запрос.
  • Параметр handler предоставляет доступ к стандартным методам стратегии (fetch, cacheMatch, cachePut), что позволяет комбинировать кэширование и сетевые запросы.

Интерфейсы и типы в TypeScript

Для строгой типизации кастомных стратегий можно определить интерфейс параметров стратегии и типизацию метода _handle. В Workbox базовые типы экспортируются из пакета workbox-core и workbox-strategies.

import { Strategy, StrategyHandler } from 'workbox-strategies';

interface CustomStrategyOptions {
  cacheName?: string;
  maxAgeSeconds?: number;
}

class TypedCustomStrategy extends Strategy {
  private options: CustomStrategyOptions;

  constructor(options: CustomStrategyOptions) {
    super();
    this.options = options;
  }

  protected async _handle(
    request: Request,
    handler: StrategyHandler
  ): Promise<Response> {
    const cachedResponse = await handler.cacheMatch(request);
    if (cachedResponse) {
      return cachedResponse;
    }
    const response = await handler.fetch(request);
    await handler.cachePut(request, response.clone());
    return response;
  }
}
  • Использование интерфейсов позволяет контролировать, какие параметры можно передавать в стратегию.
  • Метод _handle возвращает Promise<Response>, что соответствует стандарту работы с асинхронными сетевыми операциями.
  • handler.cacheMatch(request) проверяет наличие ресурса в кэше, а handler.cachePut(request, response) добавляет его в кэш.

Обеспечение совместимости с Workbox Routing

Для интеграции кастомной стратегии с роутингом Workbox важно, чтобы стратегия соответствовала интерфейсу RouteHandler. Пример регистрации маршрута с кастомной стратегией:

import { registerRoute } from 'workbox-routing';

const strategy = new TypedCustomStrategy({ cacheName: 'custom-cache', maxAgeSeconds: 3600 });

registerRoute(
  ({ request }) => request.destination === 'script',
  strategy
);
  • Любая стратегия, наследуемая от Strategy, автоматически реализует интерфейс RouteHandler.
  • Это позволяет использовать кастомные стратегии в маршрутах точно так же, как стандартные (CacheFirst, NetworkFirst).

Расширение типизации для дополнительных методов

Если стратегия использует дополнительные методы, например, фильтрацию по заголовкам или динамическое управление кэшем, полезно расширять интерфейсы:

interface AdvancedStrategyOptions extends CustomStrategyOptions {
  filterHeaders?: string[];
}

class AdvancedCustomStrategy extends Strategy {
  private options: AdvancedStrategyOptions;

  constructor(options: AdvancedStrategyOptions) {
    super();
    this.options = options;
  }

  protected async _handle(request: Request, handler: StrategyHandler): Promise<Response> {
    let response = await handler.fetch(request);
    
    if (this.options.filterHeaders) {
      const filteredResponse = new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: this.filterResponseHeaders(response.headers),
      });
      response = filteredResponse;
    }

    await handler.cachePut(request, response.clone());
    return response;
  }

  private filterResponseHeaders(headers: Headers): Headers {
    const filtered = new Headers();
    this.options.filterHeaders?.forEach(header => {
      const value = headers.get(header);
      if (value) filtered.set(header, value);
    });
    return filtered;
  }
}
  • Метод filterResponseHeaders возвращает объект Headers, сохраняя строгую типизацию.
  • Такой подход гарантирует, что разработчик не добавит произвольные свойства, нарушающие контракт стратегии.

Паттерны типизации для больших проектов

  1. Определение интерфейсов для опций стратегии — позволяет централизованно управлять параметрами.
  2. Использование StrategyHandler с корректными generic-типами — повышает безопасность работы с кэшем и сетевыми запросами.
  3. Разделение логики фильтрации и обработки ответа — упрощает тестирование и повторное использование методов.
  4. Наследование от базового класса Strategy — сохраняет совместимость с остальной инфраструктурой Workbox.

Эти подходы позволяют строить масштабируемые и безопасные сервис-воркеры, где каждая кастомная стратегия полностью типизирована и легко интегрируется с существующими маршрутами и стандартными стратегиями.