23. SvelteKit: load() функции
📡 SvelteKit Load Functions: Загрузка данных как профи
Заголовок раздела «📡 SvelteKit Load Functions: Загрузка данных как профи»Привет! 👋 Загрузка данных — это сердце любого веб-приложения. SvelteKit предлагает элегантное решение: load функции, которые запускаются перед рендерингом страницы и передают данные в компонент через специальный data prop.
Думай о load функции как о waiter в ресторане 🍽️: гость (браузер) делает заказ (запрос страницы), официант (load) идёт на кухню (сервер/база данных), приносит блюдо (данные), и только тогда гость видит свою еду (страница рендерится).
🎯 Два вида load функций: Universal vs Server
Заголовок раздела «🎯 Два вида load функций: Universal vs Server»Это ключевое разделение в SvelteKit, которое нужно понять сразу:
+page.ts ← Universal load (запускается и на сервере, и на клиенте)+page.server.ts ← Server load (ТОЛЬКО на сервере, никогда на клиенте)
+layout.ts ← Universal load для layout+layout.server.ts ← Server load для layoutКогда что использовать:
+page.ts (Universal): ✅ Публичные API (не нужен секретный ключ) ✅ Данные, которые нужны при клиентской навигации ✅ Fetch с внешних API ❌ Прямой доступ к базе данных ❌ Секретные переменные окружения
+page.server.ts (Server only): ✅ База данных (Prisma, Drizzle, etc.) ✅ Секретные API ключи ✅ Проверка сессии/авторизации ✅ Cookies ✅ Form Actions (ТОЛЬКО здесь!) ❌ Не запускается при клиентской навигации без SSR📖 +page.ts: Universal Load
Заголовок раздела «📖 +page.ts: Universal Load»import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch, url, params, data }) => { // fetch здесь — специальный SvelteKit fetch: // - На сервере: делает прямые запросы (нет сетевого overhead) // - На клиенте: обычный браузерный fetch // - Поддерживает относительные URL // - Автоматически передаёт cookies
const page = Number(url.searchParams.get('page') ?? '1'); const limit = 10;
const response = await fetch(`/api/posts?page=${page}&limit=${limit}`);
if (!response.ok) { throw new Error('Не удалось загрузить посты'); }
const { posts, total } = await response.json();
return { posts, total, page, limit, // Всё это будет доступно в +page.svelte через data.posts, data.total, etc. };};<script lang="ts"> import type { PageData } from './$types';
// data — автоматически типизирован из load функции! let { data }: { data: PageData } = $props();
// TypeScript знает что data.posts — массив постов, data.total — число</script>
<h1>Блог</h1><p>Всего постов: {data.total}</p>
{#each data.posts as post} <article> <h2>{post.title}</h2> <p>{post.excerpt}</p> <a href="/blog/{post.slug}">Читать →</a> </article>{/each}
<!-- Пагинация --><nav> {#if data.page > 1} <a href="?page={data.page - 1}">← Назад</a> {/if} <span>Страница {data.page}</span> {#if data.page * data.limit < data.total} <a href="?page={data.page + 1}">Далее →</a> {/if}</nav>🖥️ +page.server.ts: Server Load
Заголовок раздела «🖥️ +page.server.ts: Server Load»import type { PageServerLoad } from './$types';import { error, redirect } from '@sveltejs/kit';import { db } from '$lib/server/database';
export const load: PageServerLoad = async ({ params, // Параметры маршрута locals, // Данные из hooks (user session, etc.) cookies, // Куки request, // Request объект url, // URL объект setHeaders, // Установить HTTP заголовки}) => { const { slug } = params;
// Прямой доступ к базе данных — только на сервере! const post = await db.post.findUnique({ where: { slug }, include: { author: true, tags: true, comments: { orderBy: { createdAt: 'desc' }, take: 10, }, }, });
if (!post) { error(404, { message: `Пост "${slug}" не найден` }); }
if (!post.published && !locals.user?.isAdmin) { error(403, { message: 'Нет доступа к черновику' }); }
// Кэшируем страницу на CDN на 1 час (только публичные посты): if (post.published) { setHeaders({ 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400', }); }
// Обновляем счётчик просмотров асинхронно (не ждём результата): db.post.update({ where: { id: post.id }, data: { views: { increment: 1 } }, }).catch(console.error); // Не блокируем загрузку страницы
return { post, // Заметь: locals.user НЕ передаём автоматически — только то что нужно компоненту isOwner: locals.user?.id === post.authorId, };};🏗️ +layout.ts и +layout.server.ts: данные для всех страниц
Заголовок раздела «🏗️ +layout.ts и +layout.server.ts: данные для всех страниц»// src/routes/+layout.server.ts — данные для ВСЕХ страниц приложенияimport type { LayoutServerLoad } from './$types';import { db } from '$lib/server/database';
export const load: LayoutServerLoad = async ({ locals, cookies }) => { // Эти данные будут доступны ВЕЗДЕ через $page.data или data prop layout
const user = locals.user ?? null;
// Загружаем данные для навигации один раз: const [categories, notifications] = await Promise.all([ db.category.findMany({ orderBy: { name: 'asc' } }), user ? db.notification.count({ where: { userId: user.id, read: false } }) : 0, ]);
return { user, categories, notificationsCount: notifications, theme: cookies.get('theme') ?? 'dark', };};<!-- src/routes/+layout.svelte — используем layout данные --><script lang="ts"> import type { LayoutData } from './$types';
let { children, data }: { children: any; data: LayoutData } = $props(); // data.user, data.categories, data.notificationsCount — всё здесь!</script>
<nav> {#if data.user} <span>Привет, {data.user.name}!</span> <span>🔔 {data.notificationsCount}</span> {:else} <a href="/login">Войти</a> {/if}
{#each data.categories as cat} <a href="/category/{cat.slug}">{cat.name}</a> {/each}</nav>
{@render children()}🔗 parent(): получаем данные родительского layout
Заголовок раздела «🔗 parent(): получаем данные родительского layout»import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent, params }) => { // parent() даёт нам данные из +layout.server.ts и +layout.ts const { user, categories } = await parent();
// Теперь можем использовать user для проверки доступа const post = await db.post.findUnique({ where: { slug: params.slug }, });
// Комбинируем данные: return { post, relatedPosts: await getRelatedPosts(post.id, categories), };};Важно: parent() создаёт зависимость от родительского layout. Если родительский layout инвалидируется — дочерний load тоже перезапускается.
⚡ depends() и invalidate(): контроль кэширования
Заголовок раздела «⚡ depends() и invalidate(): контроль кэширования»import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch, depends }) => { // depends() — регистрируем зависимости для invalidation depends('app:notifications'); // Кастомный идентификатор depends('app:user-stats');
const [notifications, stats] = await Promise.all([ fetch('/api/notifications').then(r => r.json()), fetch('/api/stats').then(r => r.json()), ]);
return { notifications, stats };};<script lang="ts"> import { invalidate, invalidateAll } from '$app/navigation'; import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
// Инвалидировать только уведомления: async function refreshNotifications() { await invalidate('app:notifications'); // Перезапустит load для этой зависимости }
// Инвалидировать по URL: async function refreshStats() { await invalidate('/api/stats'); // Перезапустит load, который fetch этот URL }
// Инвалидировать всё на странице: async function refreshAll() { await invalidateAll(); // Перезапустит все load функции текущей страницы }
// Автоматическое обновление каждые 30 секунд: $effect(() => { const interval = setInterval(() => { invalidate('app:notifications'); }, 30000);
return () => clearInterval(interval); });</script>
<div> <h1>Dashboard</h1> <p>Уведомления: {data.notifications.length}</p> <button onclick={refreshNotifications}>🔔 Обновить</button> <button onclick={refreshAll}>🔄 Обновить всё</button></div>🌊 Стриминг с Promise
Заголовок раздела «🌊 Стриминг с Promise»SvelteKit поддерживает стриминг данных — ты можешь вернуть Promise из load функции, и страница начнёт рендериться до того, как все данные загружены:
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => { const productId = params.id;
// Критически важные данные — ждём немедленно: const product = await db.product.findUnique({ where: { id: productId }, });
if (!product) error(404, 'Товар не найден');
return { product, // ← Ждём сразу (блокирует рендер)
// Возвращаем Promise-ы — они стримятся! reviews: db.review.findMany({ where: { productId }, orderBy: { createdAt: 'desc' }, }), // ← Не ждём, стримим
recommendations: getAIRecommendations(productId) .catch(() => []), // ← Стримим, с fallback при ошибке };};<script lang="ts"> import type { PageData } from './$types';
let { data }: { data: PageData } = $props();</script>
<!-- Основные данные уже доступны: --><h1>{data.product.name}</h1><p>{data.product.price} ₽</p>
<!-- Стримируемые данные — используем {#await}: -->{#await data.reviews} <div class="skeleton">Загружаем отзывы...</div>{:then reviews} {#each reviews as review} <div class="review"> <strong>{review.author}</strong> <p>{review.text}</p> </div> {/each}{:catch error} <p>Не удалось загрузить отзывы</p>{/await}
{#await data.recommendations} <div class="skeleton">Подбираем рекомендации...</div>{:then recommendations} <h3>Похожие товары</h3> {#each recommendations as rec} <a href="/product/{rec.id}">{rec.name}</a> {/each}{/await}🔒 TypeScript: PageLoad и PageServerLoad
Заголовок раздела «🔒 TypeScript: PageLoad и PageServerLoad»import type { PageServerLoad } from './$types';import { error } from '@sveltejs/kit';
// PageServerLoad автоматически типизирует params из имени файла!// Для /users/[id]/ params.id всегда stringexport const load = (async ({ params, locals }) => { const user = await db.user.findUnique({ where: { id: params.id }, // params.id: string (автоматически!) });
if (!user) error(404, 'Пользователь не найден');
return { user }; // TypeScript выведет тип возврата автоматически}) satisfies PageServerLoad;
// src/routes/users/[id]/+page.svelte// PageData автоматически = { user: User }// Явная типизация если нужно:import type { PageServerLoad, Actions } from './$types';
interface Post { id: string; title: string; content: string; author: { name: string; avatar: string };}
export const load = (async ({ params }) => { const post: Post = await getPost(params.slug); return { post };}) satisfies PageServerLoad;
// Это даст PageData = { post: Post } в +page.svelte📊 $page.data: доступ к данным без prop drilling
Заголовок раздела «📊 $page.data: доступ к данным без prop drilling»<!-- Любой компонент может получить данные страницы через $page --><script lang="ts"> import { page } from '$app/stores';
// $page.data содержит все merged данные от всех load функций: // - +layout.server.ts data // - +layout.ts data // - +page.server.ts data // - +page.ts data
// Получаем user из layout data без prop drilling: let user = $derived($page.data.user);</script>
{#if $page.data.user} <span>Привет, {$page.data.user.name}!</span>{/if}⚙️ Конфигурация: prerender, ssr, csr
Заголовок раздела «⚙️ Конфигурация: prerender, ssr, csr»// Предрендеривать страницу при сборке (SSG):export const prerender = true;
// Отключить SSR — только клиентский рендеринг:export const ssr = false;
// Отключить клиентский рендеринг — чистый HTML:export const csr = false;
// Загрузка данных для статической страницы:export const load = async () => { // Эта функция запустится один раз при сборке const data = await fetchBuildTimeData(); return { data };};🚨 Обработка ошибок в load
Заголовок раздела «🚨 Обработка ошибок в load»import { error, redirect, fail } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params, locals }) => { // error() — возвращает HTTP ошибку if (!locals.user) { redirect(307, '/login'); // Редирект }
const resource = await db.find(params.id);
if (!resource) { error(404, { message: 'Ресурс не найден', code: 'NOT_FOUND', // Дополнительные данные ошибки }); }
if (resource.ownerId !== locals.user.id) { error(403, 'Нет доступа'); }
return { resource };};