Перейти к содержимому

23. SvelteKit: load() функции

📡 SvelteKit Load Functions: Загрузка данных как профи

Заголовок раздела «📡 SvelteKit Load Functions: Загрузка данных как профи»

Привет! 👋 Загрузка данных — это сердце любого веб-приложения. SvelteKit предлагает элегантное решение: load функции, которые запускаются перед рендерингом страницы и передают данные в компонент через специальный data prop.

Думай о load функции как о waiter в ресторане 🍽️: гость (браузер) делает заказ (запрос страницы), официант (load) идёт на кухню (сервер/база данных), приносит блюдо (данные), и только тогда гость видит свою еду (страница рендерится).


Это ключевое разделение в 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

src/routes/blog/+page.ts
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.
};
};
src/routes/blog/+page.svelte
<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>

src/routes/blog/[slug]/+page.server.ts
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»
src/routes/blog/[slug]/+page.server.ts
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 тоже перезапускается.


src/routes/dashboard/+page.ts
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 };
};
src/routes/dashboard/+page.svelte
<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>

SvelteKit поддерживает стриминг данных — ты можешь вернуть Promise из load функции, и страница начнёт рендериться до того, как все данные загружены:

src/routes/product/[id]/+page.server.ts
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 при ошибке
};
};
src/routes/product/[id]/+page.svelte
<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}

src/routes/users/[id]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
// PageServerLoad автоматически типизирует params из имени файла!
// Для /users/[id]/ params.id всегда string
export 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 -->
<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}

src/routes/static-page/+page.ts
// Предрендеривать страницу при сборке (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 };
};

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 };
};