Архитектура работы со страницами
Обзор архитектуры
Система управления страницами построена на современной архитектуре с использованием:
- Pinia Colada - для кеширования и управления состоянием запросов
- Composables - для переиспользуемой логики работы с данными
- Server Routes - для проксирования API запросов
Основные компоненты
1. Composables (app/composables/usePages.ts)
usePageBySlug(slug)
Базовый composable для получения страницы с оптимизированным кешированием
Что делает:
- Получает данные страницы по slug
- Автоматически загружает дочерние страницы для
template === 'page-list' - Использует единый оптимизированный запрос вместо двух отдельных
- Кеширует результат с помощью Pinia Colada
Зачем нужно кеширование:
- 📈 Производительность: повторные запросы не идут на сервер
- ⚡ Скорость: мгновенная загрузка при навигации назад/вперед
- 🌐 Экономия трафика: снижает нагрузку на сервер и трафик пользователя
- 🎯 UX: нет "моргания" контента при переходах между страницами
Преимущества объединенного подхода:
- Атомарность: данные загружаются вместе или не загружаются вообще
- Меньше состояний для отслеживания
- Более простая логика кеширования
- Лучшая производительность
usePageData(routeSlug)
Wrapper для работы со страницами в компонентах маршрутов
Зачем нужен этот wrapper:
- 🔄 Обратная совместимость: сохраняет API для существующих компонентов
- 📱 Роутинг: нормализует slug из параметров маршрута (массив → строка)
- 🎛️ Детализированный контроль: разделяет состояния загрузки страницы и контента
- 🚦 UX оптимизация: позволяет показывать частичный контент
Разделение состояний загрузки:
// Можно показать заголовок страницы сразу
if (!isLoadingPage.value && page.value) {
// Показываем заголовок и основную информацию
}
// А дочерние страницы загружать отдельно
if (isLoadingChildren.value) {
// Показываем скелетон списка
}
2. Server Routes
/server/routes/n-api/pages/slug/[...slug].get.ts
- Обрабатывает многосегментные slug (например,
news/news-1) - Нормализует параметры в строку
- Проксирует запросы к Django API
/server/routes/n-api/pages/index.get.ts
- Обрабатывает запросы списков страниц
- Поддерживает фильтрацию по
parent,template_type - Возвращает пагинированные результаты
3. Конфигурация Pinia Colada (colada.options.ts)
export default {
staleTime: 30 * 1000, // Данные свежие 30 секунд
gcTime: 60 * 1000, // Сборка мусора через 60 секунд
retry: 3, // 3 попытки повторного запроса
refetchOnWindowFocus: true,
refetchOnReconnect: true
}
Алгоритм работы
Загрузка страницы (обновленный алгоритм)
Пошаговый процесс:
- Инициализация
- Компонент вызывает
usePageData(route.params.slug) - Нормализация slug: массив → строка (
['news', 'article'] → 'news/article')
- Компонент вызывает
- Проверка кеша
- Pinia Colada проверяет кеш по ключу
['pages', 'slug', normalizedSlug] - Если данные свежие (< 30 сек) → возвращаем из кеша
- Если данных нет или они устарели → переход к шагу 3
- Pinia Colada проверяет кеш по ключу
- Загрузка данных (единый запрос)
┌─ Запрос к /n-api/pages/slug/${slug} │ (получаем основные данные страницы) │ └─ Если template === 'page-list' && есть id └─ Запрос к /n-api/pages/?parent=${id} (получаем дочерние страницы) - Объединение результата
- Формируется объект:
{ pageData, content } - Сохраняется в кеше с TTL 30 секунд
- Формируется объект:
- Возврат данных
usePageBySlugвозвращает объединенные данныеusePageDataразделяет состояния для UI
Кеширование
Зачем это нужно:
Без кеша:
Пользователь на странице /news → запрос к серверу → ответ (500мс)
Пользователь идет на /about → запрос к серверу → ответ (500мс)
Пользователь возвращается на /news → СНОВА запрос к серверу → ответ (500мс)
С кешем:
Пользователь на странице /news → запрос к серверу → ответ (500мс) + сохранение в кеш
Пользователь идет на /about → запрос к серверу → ответ (500мс) + сохранение в кеш
Пользователь возвращается на /news → данные из кеша (0мс) ⚡
Настройки кеша:
staleTime: 30 секунд— данные считаются "свежими" 30 секундgcTime: 60 секунд— удаляем из памяти через 60 секунд после последнего использованияretry: 3— 3 попытки при ошибке сети- Автообновление при фокусе окна и восстановлении сети
Почему именно эти настройки:
- 30 секунд staleTime: баланс между актуальностью и производительностью
- Слишком мало (5 сек) = много запросов
- Слишком много (5 мин) = устаревшие данные
- 60 секунд gcTime: освобождаем память от неиспользуемых данных
- 3 попытки: достаточно для нестабильной сети, но не слишком много
Использование
В компонентах страниц
<script setup>
const route = useRoute();
const { page, content, isLoading, isLoadingPage, isLoadingChildren, template } = usePageData(() => route.params.slug);
// Показ разных индикаторов загрузки
watchEffect(() => {
if (isLoadingPage.value) {
console.log('Загружаем основную информацию о странице...');
}
if (isLoadingChildren.value) {
console.log('Загружаем дочерние страницы...');
}
});
</script>
<template>
<div>
<!-- Основной контент страницы -->
<PageSkeleton v-if="isLoadingPage" />
<PageHeader v-else-if="page" :page="page" />
<!-- Дочерние страницы (только для page-list) -->
<template v-if="template === 'page-list'">
<ChildrenSkeleton v-if="isLoadingChildren" />
<PageList v-else :pages="content" />
</template>
</div>
</template>
Практические примеры
Пример 1: Простая страница
<script setup>
// Для статической страницы "О нас"
const { page, isLoading } = usePageData('about');
</script>
<template>
<div v-if="isLoading">Загружаем...</div>
<div v-else>
<h1>{{ page?.title }}</h1>
<div v-html="page?.description"></div>
</div>
</template>
Пример 2: Раздел с подстраницами
<script setup>
// Для раздела "Новости" с дочерними статьями
const route = useRoute();
const { page, content, isLoadingPage, isLoadingChildren } = usePageData(() => route.params.slug);
</script>
<template>
<div>
<!-- Заголовок раздела загружается первым -->
<div v-if="isLoadingPage">Загружаем раздел...</div>
<h1 v-else>{{ page?.title }}</h1>
<!-- Список статей может загружаться отдельно -->
<div v-if="isLoadingChildren">Загружаем статьи...</div>
<div v-else>
<ArticleCard
v-for="article in content"
:key="article.id"
:article="article"
/>
</div>
</div>
</template>
Типы страниц
page-list- Страница-раздел с дочерними страницами (новости, каталог)page-detail- Обычная страница с контентом (о нас, контакты)page-landing- Лендинг страница (главная)unique-placeholder- Уникальная страница (404, поиск)
Обработка ошибок
На уровне composables
const { page, error, pageError, childrenError } = usePageData(slug);
// Общая ошибка (любой из запросов)
if (error.value) {
console.error('Ошибка загрузки данных страницы:', error.value);
}
// Специфические ошибки
if (pageError.value) {
// Ошибка загрузки метаданных страницы (404, 500)
// Показываем страницу ошибки
}
if (childrenError.value) {
// Ошибка загрузки дочерних страниц
// Показываем основной контент, но без списка детей
}
Практические советы по обработке ошибок
Стратегия graceful degradation
<template>
<div>
<!-- Основная страница -->
<div v-if="pageError">
<h1>Страница не найдена</h1>
<NuxtLink to="/">На главную</NuxtLink>
</div>
<PageHeader v-else-if="page" :page="page" />
<!-- Дочерние страницы -->
<div v-if="page?.template === 'page-list'">
<div v-if="childrenError" class="error-message">
Не удалось загрузить список. Попробуйте обновить страницу.
</div>
<PageList v-else :pages="content" />
</div>
</div>
</template>
Принцип: показываем то, что удалось загрузить, а ошибки обрабатываем gracefully.
На уровне server routes
catch (e) {
if (e instanceof ApiError) {
setResponseStatus(event, e.status || 500);
return { message: e.message, status: e.status, details: e.data };
}
setResponseStatus(event, 500);
return { message: 'Unexpected error', details: String(e) };
}
Производительность и оптимизации
Почему эта архитектура быстрая
- Кеширование на клиенте
Первый визит: Сервер → Браузер (500мс) Повторный визит: Кеш → Браузер (0мс) - Объединенные запросы
Старый способ: 2 запроса по 300мс = 600мс общее время Новый способ: 1 запрос 400мс = 400мс общее время - Умная загрузка
- Показываем заголовок сразу
- Дочерние страницы подгружаем параллельно
- Пользователь не видит пустой экран
- SSR + клиентское кеширование
- Первая загрузка: данные приходят с сервера (SEO)
- Навигация: данные из кеша (скорость)
Метрики производительности
- TTFB (Time To First Byte): ~100-200мс
- FCP (First Contentful Paint): ~300-500мс
- LCP (Largest Contentful Paint): ~500-800мс
- Кеш-хит: 95%+ для повторных визитов
Будущие улучшения
Мультиязычность
// Планируемое расширение для i18n
const { locale } = useI18n();
const cacheKey = ['pages', 'slug', locale.value, slug];
Авторизация
// Интеграция с JWT токенами
const { token } = useAuth();
const headers = token.value ? { Authorization: `Bearer ${token.value}` } : {};
Блоки контента
// Для страниц типа 'page-detail'
const content = computed(() => {
if (pageData.value?.template === 'page-list') {
return childrenData.value?.results || [];
}
if (pageData.value?.template === 'page-detail') {
// TODO: загрузка блоков контента
return usePageBlocks(pageData.value.id);
}
return [];
});
Prefetching страниц
// Предзагрузка страниц при наведении на ссылки
const prefetchPage = (slug) => {
usePageBySlug(slug); // Запрос пойдет в кеш
};