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);Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
cacheKey | string | ✅ | - | Уникальный ключ для идентификации кешированного контента. Должен быть уникальным для каждого отдельного куска контента. |
hardTtl | number | ✅ | - | TTL в секундах для полного истечения кеша (Hard TTL). После истечения этого времени кеш будет полностью обновлен. |
softTtl | number | ✅ | - | TTL в секундах для фонового обновления кеша (Soft TTL). Когда истекает soft TTL, но hard TTL еще актуален, происходит фоновое обновление без блокировки пользователя. |
cacheTags | string[] | ❌ | [] | Массив тегов для групповой инвалидации кеша. Позволяет очищать связанные данные при изменении контента или структуры. |
Примеры использования
Простое кеширование компонента
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,
};
};