Skip to content

Nuxt Render Cache предоставляет мощные composables для программного управления кешированием. Они позволяют интегрировать кеширование в любую часть вашего приложения без использования компонентов.

useRenderCache

Основной composable для рендеринга Vue компонентов с двухуровневым кешированием. Реализует стратегию кеширования с Soft/Hard TTL и автоматическими блокировками.

Базовое использование

typescript
import { useRenderCache } from 'nuxt-render-cache';

const renderCache = useRenderCache({
  cacheKey: 'page:home',
  hardTtl: 300, // 5 минут
  softTtl: 60, // 1 минута
  cacheTags: ['page', 'home'],
});

// Рендеринг компонента с кешированием
const html = await renderCache.render(slots, instance);

Параметры

ПараметрТипОбязательныйПо умолчаниюОписание
cacheKeystring-Уникальный ключ для идентификации кешированного контента. Должен быть уникальным для каждого отдельного куска контента.
hardTtlnumber-TTL в секундах для полного истечения кеша (Hard TTL). После истечения этого времени кеш будет полностью обновлен.
softTtlnumber-TTL в секундах для фонового обновления кеша (Soft TTL). Когда истекает soft TTL, но hard TTL еще актуален, происходит фоновое обновление без блокировки пользователя.
cacheTagsstring[][]Массив тегов для групповой инвалидации кеша. Позволяет очищать связанные данные при изменении контента или структуры.

Примеры использования

Простое кеширование компонента

typescript
import { useRenderCache } from 'nuxt-render-cache';

export const useCachedComponent = () => {
  const renderCache = useRenderCache({
    cacheKey: 'component:hero',
    hardTtl: 600, // 10 минут
    softTtl: 120, // 2 минуты
    cacheTags: ['component', 'hero', 'homepage'],
  });

  const renderHero = async (
    slots: Slots,
    instance: ComponentInternalInstance
  ) => {
    return await renderCache.render(slots, instance);
  };

  return {
    renderHero,
  };
};

Кеширование с динамическими ключами

typescript
import { useRenderCache } from 'nuxt-render-cache';

export const useDynamicCache = (userId: string) => {
  const renderCache = useRenderCache({
    cacheKey: `user:${userId}:profile`,
    hardTtl: 300,
    softTtl: 60,
    cacheTags: ['user', 'profile', `user:${userId}`],
  });

  const renderUserProfile = async (
    slots: Slots,
    instance: ComponentInternalInstance
  ) => {
    return await renderCache.render(slots, instance);
  };

  return {
    renderUserProfile,
  };
};

Кеширование API ответов

typescript
import { useRenderCache } from 'nuxt-render-cache';

export const useApiCache = () => {
  const apiCache = useRenderCache({
    cacheKey: 'api:products',
    hardTtl: 180, // 3 минуты
    softTtl: 30, // 30 секунд
    cacheTags: ['api', 'products'],
  });

  const renderProductList = async (
    slots: Slots,
    instance: ComponentInternalInstance
  ) => {
    return await apiCache.render(slots, instance);
  };

  return {
    renderProductList,
  };
};

Продвинутые примеры

Условное кеширование

typescript
import { useRenderCache } from 'nuxt-render-cache';

export const useConditionalCache = (options: {
  cacheKey: string;
  enabled: boolean;
  hardTtl: number;
  softTtl: number;
}) => {
  const { enabled, ...cacheOptions } = options;

  if (!enabled) {
    // Возвращаем функцию-заглушку без кеширования
    return {
      render: async (slots: Slots, instance: ComponentInternalInstance) => {
        return await renderComponentToString(slots, instance);
      },
    };
  }

  const renderCache = useRenderCache(cacheOptions);

  return {
    render: renderCache.render,
  };
};

Комплексное кеширование с несколькими уровнями

typescript
import { useRenderCache } from 'nuxt-render-cache';

export const useMultiLevelCache = () => {
  // Кеш для основного контента
  const mainCache = useRenderCache({
    cacheKey: 'page:complex:main',
    hardTtl: 600,
    softTtl: 120,
    cacheTags: ['page', 'complex', 'main'],
  });

  // Кеш для боковой панели
  const sidebarCache = useRenderCache({
    cacheKey: 'page:complex:sidebar',
    hardTtl: 1800, // Более длительный TTL для боковой панели
    softTtl: 600,
    cacheTags: ['page', 'complex', 'sidebar'],
  });

  // Кеш для нижнего колонтитула
  const footerCache = useRenderCache({
    cacheKey: 'page:complex:footer',
    hardTtl: 3600, // Очень длительный TTL для футера
    softTtl: 1800,
    cacheTags: ['page', 'complex', 'footer'],
  });

  const renderComplexPage = async (
    mainSlots: Slots,
    sidebarSlots: Slots,
    footerSlots: Slots,
    instance: ComponentInternalInstance
  ) => {
    const [mainHtml, sidebarHtml, footerHtml] = await Promise.all([
      mainCache.render(mainSlots, instance),
      sidebarCache.render(sidebarSlots, instance),
      footerCache.render(footerSlots, instance),
    ]);

    return `
      <div class="complex-page">
        <main>${mainHtml}</main>
        <aside>${sidebarHtml}</aside>
        <footer>${footerHtml}</footer>
      </div>
    `;
  };

  return {
    renderComplexPage,
  };
};

useCache

Низкоуровневый composable для прямой работы с Redis кешем. Предоставляет полный контроль над операциями кеширования без автоматического рендеринга компонентов.

Базовое использование

typescript
import { useCache } from 'nuxt-render-cache';

const cache = useCache();

// Сохранение данных
await cache.set(
  'my-key',
  {
    data: '<div>Cached content</div>',
    timestamp: Date.now(),
    tags: ['example'],
  },
  300
);

// Получение данных
const entry = await cache.get('my-key');

// Проверка блокировки
const lock = await cache.lock('my-key', 30);
if (!lock.isLocked) {
  try {
    // Выполняем работу
    console.log('Lock acquired, performing work');
  } finally {
    await lock.unlock();
  }
}

Методы

МетодВозвращаетОписание
get(key: string)Promise<CacheEntry | null>Получить данные из кеша по ключу. Возвращает объект с данными или null если ключ не найден.
set(key, value, ttl)Promise<string>Сохранить данные в кеш с заданным TTL. Автоматически публикует событие в Redis Pub/Sub.
lock(key, ttl)Promise<{isLocked: boolean, unlock: Function}>Установить блокировку для предотвращения одновременного доступа. Возвращает объект с состоянием блокировки и функцией разблокировки.
expired(key, ttl)Promise<boolean>Проверить истекли ли данные по указанному TTL. Возвращает true если данные истекли или отсутствуют.
waitForCache(key, backupEntry, maxWaitTime)Promise<CacheEntry | null>Ожидание обновления кеша через Redis Pub/Sub. Используется для синхронизации между процессами.
getAllKeys()Promise<string[]>Получить список всех ключей кеша (исключая ключи блокировок).
deleteKey(key)Promise<number>Удалить конкретный ключ из кеша.
deleteByTags(tags)Promise<number>Удалить ключи по тегам. Возвращает количество удаленных ключей.
clearAll()Promise<number>Очистить весь кеш. Возвращает количество удаленных ключей.
getStats()Promise<object>Получить статистику Redis и кеша (количество ключей, информация о Redis).

Примеры использования

Простое кеширование данных

typescript
import { useCache } from 'nuxt-render-cache';

export const useSimpleCache = () => {
  const cache = useCache();

  const getCachedData = async (key: string) => {
    let data = await cache.get(key);

    if (!data) {
      // Данные не найдены, создаем их
      data = {
        data: JSON.stringify({ message: 'Hello from cache!' }),
        timestamp: Date.now(),
        tags: ['example'],
      };

      await cache.set(key, data, 300); // Кешируем на 5 минут
    }

    return JSON.parse(data.data);
  };

  return {
    getCachedData,
  };
};

Управление блокировками

typescript
import { useCache } from 'nuxt-render-cache';

export const useLockManager = () => {
  const cache = useCache();

  const executeWithLock = async (
    lockKey: string,
    operation: () => Promise<any>,
    lockTtl: number = 30
  ) => {
    const lock = await cache.lock(lockKey, lockTtl);

    if (lock.isLocked) {
      throw new Error(`Operation is already in progress for key: ${lockKey}`);
    }

    try {
      console.log(`[LockManager] Lock acquired for key: ${lockKey}`);
      const result = await operation();
      console.log(`[LockManager] Operation completed for key: ${lockKey}`);
      return result;
    } finally {
      await lock.unlock();
      console.log(`[LockManager] Lock released for key: ${lockKey}`);
    }
  };

  return {
    executeWithLock,
  };
};

Инвалидация кеша по тегам

typescript
import { useCache } from 'nuxt-render-cache';

export const useCacheInvalidator = () => {
  const cache = useCache();

  const invalidateByTags = async (tags: string[]) => {
    console.log(
      `[CacheInvalidator] Invalidating cache by tags: ${tags.join(', ')}`
    );
    const deletedCount = await cache.deleteByTags(tags);
    console.log(`[CacheInvalidator] Deleted ${deletedCount} cache entries`);
    return deletedCount;
  };

  const invalidateUserCache = async (userId: string) => {
    return await invalidateByTags(['user', `user:${userId}`]);
  };

  const invalidateProductCache = async (productId: string) => {
    return await invalidateByTags(['product', `product:${productId}`]);
  };

  const clearAllCache = async () => {
    console.log('[CacheInvalidator] Clearing all cache');
    const deletedCount = await cache.clearAll();
    console.log(`[CacheInvalidator] Cleared ${deletedCount} cache entries`);
    return deletedCount;
  };

  return {
    invalidateByTags,
    invalidateUserCache,
    invalidateProductCache,
    clearAllCache,
  };
};

Мониторинг кеша

typescript
import { useCache } from 'nuxt-render-cache';

export const useCacheMonitor = () => {
  const cache = useCache();

  const getCacheStats = async () => {
    const stats = await cache.getStats();
    const keys = await cache.getAllKeys();

    return {
      ...stats,
      keysCount: keys.length,
      keysSample: keys.slice(0, 10), // Первые 10 ключей для примера
    };
  };

  const getKeyInfo = async (key: string) => {
    const entry = await cache.get(key);
    const isExpired = await cache.expired(key, 300); // Проверяем TTL 5 минут

    return {
      key,
      exists: !!entry,
      isExpired,
      data: entry ? JSON.parse(entry.data) : null,
      timestamp: entry?.timestamp,
      tags: entry?.tags || [],
    };
  };

  const getTagStats = async () => {
    const keys = await cache.getAllKeys();
    const tagStats: Record<string, number> = {};

    for (const key of keys) {
      const entry = await cache.get(key);
      if (entry?.tags) {
        entry.tags.forEach((tag) => {
          tagStats[tag] = (tagStats[tag] || 0) + 1;
        });
      }
    }

    return tagStats;
  };

  return {
    getCacheStats,
    getKeyInfo,
    getTagStats,
  };
};

Лучшие практики

1. Обработка ошибок

typescript
import { useCache } from 'nuxt-render-cache';

export const useErrorHandling = () => {
  const cache = useCache();

  const safeGet = async (key: string) => {
    try {
      return await cache.get(key);
    } catch (error) {
      console.error(`[Cache] Error getting key ${key}:`, error);
      return null;
    }
  };

  const safeSet = async (key: string, value: any, ttl: number) => {
    try {
      return await cache.set(key, value, ttl);
    } catch (error) {
      console.error(`[Cache] Error setting key ${key}:`, error);
      throw error; // Перебрасываем ошибку для обработки выше
    }
  };

  const safeLock = async (key: string, ttl: number) => {
    try {
      return await cache.lock(key, ttl);
    } catch (error) {
      console.error(`[Cache] Error acquiring lock for key ${key}:`, error);
      return { isLocked: true, unlock: async () => {} }; // Возвращаем "занятую" блокировку
    }
  };

  return {
    safeGet,
    safeSet,
    safeLock,
  };
};

2. Оптимизация производительности

typescript
import { useCache } from 'nuxt-render-cache';

export const useOptimizedCache = () => {
  const cache = useCache();

  // Пакетные операции
  const batchGet = async (keys: string[]) => {
    const promises = keys.map((key) => cache.get(key));
    return await Promise.all(promises);
  };

  const batchSet = async (
    entries: Array<{ key: string; value: any; ttl: number }>
  ) => {
    const promises = entries.map(({ key, value, ttl }) =>
      cache.set(key, value, ttl)
    );
    return await Promise.all(promises);
  };

  // Условное кеширование
  const getOrSet = async (
    key: string,
    factory: () => Promise<any>,
    ttl: number
  ) => {
    let entry = await cache.get(key);

    if (!entry) {
      const data = await factory();
      entry = {
        data: JSON.stringify(data),
        timestamp: Date.now(),
        tags: [],
      };
      await cache.set(key, entry, ttl);
    }

    return JSON.parse(entry.data);
  };

  return {
    batchGet,
    batchSet,
    getOrSet,
  };
};

3. Работа с TTL

typescript
import { useCache } from 'nuxt-render-cache';

export const useTtlManager = () => {
  const cache = useCache();

  const setWithCustomTtl = async (
    key: string,
    value: any,
    ttlConfig: {
      soft: number;
      hard: number;
    }
  ) => {
    const entry = {
      data: JSON.stringify(value),
      timestamp: Date.now(),
      tags: [],
      ttlConfig, // Сохраняем конфигурацию TTL
    };

    return await cache.set(key, entry, ttlConfig.hard);
  };

  const getWithTtlCheck = async (key: string) => {
    const entry = await cache.get(key);

    if (!entry) {
      return { data: null, status: 'miss' };
    }

    const ttlConfig = entry.ttlConfig;
    if (!ttlConfig) {
      return { data: JSON.parse(entry.data), status: 'hit' };
    }

    const age = Date.now() - entry.timestamp;
    const softExpired = age > ttlConfig.soft * 1000;
    const hardExpired = age > ttlConfig.hard * 1000;

    if (hardExpired) {
      return { data: null, status: 'expired' };
    }

    return {
      data: JSON.parse(entry.data),
      status: softExpired ? 'stale' : 'fresh',
      age,
      softTtl: ttlConfig.soft,
      hardTtl: ttlConfig.hard,
    };
  };

  return {
    setWithCustomTtl,
    getWithTtlCheck,
  };
};

Отладка и мониторинг

Логирование операций

typescript
import { useCache } from 'nuxt-render-cache';

export const useCacheLogger = () => {
  const cache = useCache();

  const loggedGet = async (key: string) => {
    console.log(`[CacheLogger] Getting key: ${key}`);
    const startTime = Date.now();

    try {
      const result = await cache.get(key);
      const duration = Date.now() - startTime;

      console.log(
        `[CacheLogger] Key ${key} ${
          result ? 'found' : 'not found'
        } (${duration}ms)`
      );

      return result;
    } catch (error) {
      console.error(`[CacheLogger] Error getting key ${key}:`, error);
      throw error;
    }
  };

  const loggedSet = async (key: string, value: any, ttl: number) => {
    console.log(`[CacheLogger] Setting key: ${key} with TTL: ${ttl}s`);

    try {
      const result = await cache.set(key, value, ttl);
      console.log(`[CacheLogger] Key ${key} set successfully`);
      return result;
    } catch (error) {
      console.error(`[CacheLogger] Error setting key ${key}:`, error);
      throw error;
    }
  };

  return {
    loggedGet,
    loggedSet,
    // Делегируем остальные методы без логирования
    lock: cache.lock.bind(cache),
    expired: cache.expired.bind(cache),
    deleteKey: cache.deleteKey.bind(cache),
    clearAll: cache.clearAll.bind(cache),
  };
};

Распространенные проблемы

Проблема: Гонки условий (Race conditions)

typescript
import { useCache } from 'nuxt-render-cache';

export const useRaceConditionSafe = () => {
  const cache = useCache();

  const safeUpdate = async (
    key: string,
    updateFn: (currentValue: any) => any,
    ttl: number
  ) => {
    const lockKey = `update:${key}`;
    const lock = await cache.lock(lockKey, 30);

    if (lock.isLocked) {
      throw new Error('Update already in progress');
    }

    try {
      const currentEntry = await cache.get(key);
      const currentValue = currentEntry ? JSON.parse(currentEntry.data) : null;

      const newValue = await updateFn(currentValue);

      const newEntry = {
        data: JSON.stringify(newValue),
        timestamp: Date.now(),
        tags: currentEntry?.tags || [],
      };

      await cache.set(key, newEntry, ttl);
      return newValue;
    } finally {
      await lock.unlock();
    }
  };

  return {
    safeUpdate,
  };
};

Проблема: Утечки памяти

typescript
import { useCache } from 'nuxt-render-cache';

export const useMemorySafe = () => {
  const cache = useCache();

  // Очистка старых ключей
  const cleanupExpired = async (pattern: string = '*') => {
    const keys = await cache.getAllKeys();
    let cleanedCount = 0;

    for (const key of keys) {
      if (key.includes(pattern)) {
        const isExpired = await cache.expired(key, 3600); // 1 час
        if (isExpired) {
          await cache.deleteKey(key);
          cleanedCount++;
        }
      }
    }

    console.log(`[MemorySafe] Cleaned ${cleanedCount} expired keys`);
    return cleanedCount;
  };

  // Очистка по размеру
  const cleanupBySize = async (maxKeys: number = 1000) => {
    const keys = await cache.getAllKeys();

    if (keys.length > maxKeys) {
      const keysToDelete = keys.slice(0, keys.length - maxKeys);
      let deletedCount = 0;

      for (const key of keysToDelete) {
        await cache.deleteKey(key);
        deletedCount++;
      }

      console.log(
        `[MemorySafe] Deleted ${deletedCount} keys to maintain size limit`
      );
      return deletedCount;
    }

    return 0;
  };

  return {
    cleanupExpired,
    cleanupBySize,
  };
};

Создано с ❤️ для Nuxt сообщества