Удалённые функции
Удалённые функции — это инструмент для типобезопасного взаимодействия между клиентом и сервером. Их можно вызывать в любой части вашего приложения, но они всегда выполняются на сервере, что позволяет безопасно использовать модули, доступные только на сервере, такие как переменные окружения и клиенты баз данных.
В сочетании с экспериментальной поддержкой Svelte для await это позволяет загружать и обрабатывать данные непосредственно внутри ваших компонентов.
Эта функция на данный момент является экспериментальной, то есть она может содержать ошибки и может быть изменена без предварительного уведомления. Для использования необходимо включить опцию kit.experimental.remoteFunctions в вашем файле svelte.config.js:
export default { kit: { experimental: { remoteFunctions: true } }};Удалённые функции экспортируются из файлов .remote.js или .remote.ts и бывают четырёх типов: query, form, command и prerender. На клиенте экспортированные функции преобразуются в обёртки fetch, которые вызывают свои серверные аналоги через сгенерированную HTTP-точку доступа. Удалённые файлы должны располагаться в директориях lib или routes.
Функция query позволяет получать динамические данные с сервера (для статических данных рекомендуется использовать prerender):
import { query } from '$app/server';import * as db from '$lib/server/database';
export const getPosts = query(async () => { const posts = await db.sql` SELECT title, slug FROM post ORDER BY published_at DESC `;
return posts;});Запрос, возвращаемый из getPosts, работает как Promise, который разрешается в posts:
<script> import { getPosts } from './data.remote';</script>
<h1>Недавние сообщения</h1>
<ul> {#each await getPosts() as { title, slug }} <li><a href="/blog/{slug}">{title}</a></li> {/each}</ul><script lang="ts"> import { getPosts } from './data.remote';</script>
<h1>Недавние сообщения</h1>
<ul> {#each await getPosts() as { title, slug }} <li><a href="/blog/{slug}">{title}</a></li> {/each}</ul>До завершения промиса (или в случае ошибки) будет вызван ближайший <svelte:boundary>.
Хотя рекомендуется использовать await, в качестве альтернативы запрос также имеет свойства loading, error и current:
<script> import { getPosts } from './data.remote';
const query = getPosts();</script>
{#if query.error} <p>упс!</p>{:else if query.loading} <p>загрузка...</p>{:else} <ul> {#each query.current as { title, slug }} <li><a href="/blog/{slug}">{title}</a></li> {/each} </ul>{/if}<script lang="ts"> import { getPosts } from './data.remote';
const query = getPosts();</script>
{#if query.error} <p>упс!</p>{:else if query.loading} <p>загрузка...</p>{:else} <ul> {#each query.current as { title, slug }} <li><a href="/blog/{slug}">{title}</a></li> {/each} </ul>{/if}Аргументы query
Заголовок раздела «Аргументы query»Функции query могут принимать аргументы, например slug отдельной публикации:
<script> import { getPost } from '../data.remote';
let { params } = $props();
const post = $derived(await getPost(params.slug));</script>
<h1>{post.title}</h1><div>{@html post.content}</div><script lang="ts"> import { getPost } from '../data.remote';
let { params } = $props();
const post = $derived(await getPost(params.slug));</script>
<h1>{post.title}</h1><div>{@html post.content}</div>Поскольку getPost предоставляет HTTP-эндпойнт, важно проверять этот аргумент, чтобы убедиться в его корректном типе. Для этого можно использовать любую библиотеку валидации из Standard Schema, такую как Zod или Valibot:
import * as v from 'valibot';import { error } from '@sveltejs/kit';import { query } from '$app/server';import * as db from '$lib/server/database';
export const getPosts = query(async () => { /* ... */ });
export const getPost = query(v.string(), async (slug) => { const [post] = await db.sql` SELECT * FROM post WHERE slug = ${slug} `;
if (!post) error(404, 'Не найдено'); return post;});Как аргументы, так и возвращаемые значения сериализуются с помощью devalue, который поддерживает типы вроде Date и Map (а также пользовательские типы, определённые в вашем хуке transport) в дополнение к JSON.
Обновление запросов
Заголовок раздела «Обновление запросов»Любой запрос можно обновить с помощью метода refresh:
<button onclick={() => getPosts().refresh()}> Проверка новых постов</button>query.batch
Заголовок раздела «query.batch»query.batch работает аналогично query, за исключением того, что он группирует запросы, происходящие в рамках одной макрозадачи. Это решает так называемую проблему n+1: вместо того чтобы каждый запрос приводил к отдельному вызову базы данных (например), одновременные запросы объединяются.
На сервере callback получает массив аргументов, с которыми была вызвана функция. Он должен возвращать функцию вида (input: Input, index: number) => Output. Затем SvelteKit вызывает эту функцию с каждым из входных аргументов, чтобы разрешить отдельные вызовы с их результатами.
import * as v from 'valibot';import { query } from '$app/server';import * as db from '$lib/server/database';
export const getWeather = query.batch(v.string(), async (cityIds) => { const weather = await db.sql` SELECT * FROM weather WHERE city_id = ANY(${cityIds}) `; const lookup = new Map(weather.map(w => [w.city_id, w]));
return (cityId) => lookup.get(cityId);});<script> import CityWeather from './CityWeather.svelte'; import { getWeather } from './weather.remote.js';
let { cities } = $props(); let limit = $state(5);</script>
<h2>Погода</h2>
{#each cities.slice(0, limit) as city} <h3>{city.name}</h3> <CityWeather weather={await getWeather(city.id)} />{/each}
{#if cities.length > limit} <button onclick={() => limit += 5}> Загрузить ещё </button>{/if}Функция form упрощает запись данных на сервер. Она принимает колбэк, который получает текущий FormData…
import * as v from 'valibot';import { error, redirect } from '@sveltejs/kit';import { query, form } from '$app/server';import * as db from '$lib/server/database';import * as auth from '$lib/server/auth';
export const getPosts = query(async () => { /* ... */ });
export const getPost = query(v.string(), async (slug) => { /* ... */ });
export const createPost = form(async (data) => { // Проверяем, что пользователь авторизован const user = await auth.getUser(); if (!user) error(401, 'Не авторизован');
const title = data.get('title'); const content = data.get('content');
// Проверяем валидность данных if (typeof title !== 'string' || typeof content !== 'string') { error(400, 'Название и содержимое обязательны'); }
const slug = title.toLowerCase().replace(/ /g, '-');
// Добавляем запись в базу данных await db.sql` INSERT INTO post (slug, title, content) VALUES (${slug}, ${title}, ${content}) `;
// Перенаправляем на новую страницу redirect(303, `/blog/${slug}`);});…и возвращает объект, который можно использовать через spread-оператор (...) в элементе <form>. Колбэк вызывается при каждой отправке формы.
<script> import { createPost } from '../data.remote';</script>
<h1>Создание новой записи</h1>
<form {...createPost}> <label> <h2>Заголовок</h2> <input name="title" /> </label>
<label> <h2>Текст записи</h2> <textarea name="content"></textarea> </label>
<button>Опубликовать!</button></form><script lang="ts"> import { createPost } from '../data.remote';</script>
<h1>Создание новой записи</h1>
<form {...createPost}> <label> <h2>Заголовок</h2> <input name="title" /> </label>
<label> <h2>Текст записи</h2> <textarea name="content"></textarea> </label>
<button>Опубликовать!</button></form>Объект формы содержит свойства method и action, позволяющие ей работать без JavaScript (то есть отправлять данные с перезагрузкой страницы). Также у неё есть обработчик onsubmit, который прогрессивно улучшает форму при наличии JavaScript, отправляя данные без перезагрузки всей страницы.
Как и в случае с query, если колбэк использует отправленные данные (data), они должны быть валидированы путём передачи Standard Schema в качестве первого аргумента в form.
Форма состоит из набора полей, которые определяются схемой. В случае с createPost у нас есть два поля, title и content, оба из которых являются строками. Чтобы получить атрибуты поля, вызовите его метод .as(...), указав, какой тип ввода использовать. Для большинства типов ввода вы также можете передать второй аргумент — .as(type, value) — чтобы управлять отображаемым значением:
<form {...createPost}> <label> <h2>Заголовок</h2> <input {...createPost.fields.title.as('text')} /> </label>
<label> <h2>Текст записи</h2> <textarea {...createPost.fields.content.as('text')}></textarea> </label>
<button>Опубликовать!</button></form>Эти атрибуты позволяют SvelteKit устанавливать правильный тип ввода, задавать name, который используется для формирования данных (data), передаваемых в обработчик, заполнять значение (value) формы (например, после неудачной отправки, чтобы избавить пользователя от необходимости вводить всё заново) и устанавливать состояние aria-invalid.
Передача второго аргумента в .as(...) полезна при рендеринге формы на основе существующих данных, таких как форма редактирования или несколько экземпляров, созданных с помощью for(...). Полям ввода radio, submit и hidden это значение требуется всегда, а полям checkbox оно необходимо, когда они представляют один из вариантов в поле-массиве. Поля типа file не могут быть заполнены таким способом.
Поля могут быть вложены в объекты и массивы, а их значениями могут быть строки, числа, логические значения или объекты File. Например, если бы ваша схема выглядела так…
const datingProfile = v.object({ name: v.string(), photo: v.file(), info: v.object({ height: v.number(), likesDogs: v.optional(v.boolean(), false) }), attributes: v.array(v.string())});
export const createProfile = form(datingProfile, (data) => { /* ... */ });…ваша форма могла бы выглядеть так:
<script> import { createProfile } from './data.remote';
const { name, photo, info, attributes } = createProfile.fields;</script>
<form {...createProfile} enctype="multipart/form-data"> <label> <input {...name.as('text')} /> Имя </label>
<label> <input {...photo.as('file')} /> Фото </label>
<label> <input {...info.height.as('number')} /> Рост (см) </label>
<label> <input {...info.likesDogs.as('checkbox')} /> Я люблю собак </label>
<h2>Мои лучшие качества</h2> <input {...attributes[0].as('text')} /> <input {...attributes[1].as('text')} /> <input {...attributes[2].as('text')} />
<button>Отправить</button></form>Поскольку наша форма содержит поле ввода file, мы добавили атрибут enctype="multipart/form-data". Значения для info.height и info.likesDogs автоматически приводятся к числу и логическому значению соответственно.
В случае с полями ввода radio и checkbox, относящимися к одному и тому же полю, значение value должно быть указано в качестве второго аргумента в .as(...):
export const operatingSystems = /** @type {const} */ (['windows', 'mac', 'linux']);export const languages = /** @type {const} */ (['html', 'css', 'js']);
export const survey = form( v.object({ operatingSystem: v.picklist(operatingSystems), languages: v.optional(v.array(v.picklist(languages)), []), }), (data) => { /* ... */ },);export const operatingSystems = ['windows', 'mac', 'linux'] as const;export const languages = ['html', 'css', 'js'] as const;
export const survey = form( v.object({ operatingSystem: v.picklist(operatingSystems), languages: v.optional(v.array(v.picklist(languages)), []), }), (data) => { /* ... */ },);<form {...survey}> <h2>Какую ОС вы используете?</h2>
{#each operatingSystems as os} <label> <input {...survey.fields.operatingSystem.as('radio', os)}> {os} </label> {/each}
<h2>На каких языках вы программируете?</h2>
{#each languages as language} <label> <input {...survey.fields.languages.as('checkbox', language)}> {language} </label> {/each}
<button>Отправить</button></form>В качестве альтернативы вы можете использовать select и select multiple:
<form {...survey}> <h2>Какую ОС вы используете?</h2>
<select {...survey.fields.operatingSystem.as('select')}> {#each operatingSystems as os} <option>{os}</option> {/each} </select>
<h2>На каких языках вы программируете?</h2>
<select {...survey.fields.languages.as('select multiple')}> {#each languages as language} <option>{language}</option> {/each} </select>
<button>Отправить</button></form>Программная валидация
Заголовок раздела «Программная валидация»В дополнение к декларативной валидации по схеме, вы можете программно помечать поля как невалидные внутри обработчика формы, используя хелпер invalid из @sveltejs/kit. Это полезно в тех случаях, когда вы не можете знать, валидны ли данные, пока не попытаетесь выполнить какое-либо действие.
- Он выбрасывает исключение (throw) так же, как
redirectилиerror - Он принимает несколько аргументов, которые могут быть строками (для проблем, относящихся к форме в целом — они будут отображаться только в
fields.allIssues()) или ошибками, соответствующими Standard Schema (для тех, что относятся к конкретному полю). Используйте параметрissueдля типобезопасного создания таких ошибок:
import * as v from 'valibot';import { invalid } from '@sveltejs/kit';import { form } from '$app/server';import * as db from '$lib/server/database';
export const buyHotcakes = form( v.object({ qty: v.pipe( v.number(), v.minValue(1, 'вы должны купить хотя бы один пирожок') ) }), async (data, issue) => { try { await db.buy(data.qty); } catch (e) { if (e.code === 'OUT_OF_STOCK') { invalid( issue.qty(`у нас не хватает пирожков`) ); } } });Валидация
Заголовок раздела «Валидация»Если отправленные данные не проходят проверку по схеме, колбэк не будет выполнен. Вместо этого метод issues() каждого невалидного поля вернет массив объектов { message: string }, а атрибут aria-invalid (возвращаемый из as(...)) будет установлен в true:
<form {...createPost}> <label> <h2>Заголовок</h2>
{#each createPost.fields.title.issues() as issue} <p class="issue">{issue.message}</p> {/each}
<input {...createPost.fields.title.as('text')} /> </label>
<label> <h2>Текст записи</h2>
{#each createPost.fields.content.issues() as issue} <p class="issue">{issue.message}</p> {/each}
<textarea {...createPost.fields.content.as('text')}></textarea> </label>
<button>Опубликовать!</button></form>Вам не нужно ждать отправки формы, чтобы провалидировать данные — вы можете вызывать validate() программно, например, в обработчике oninput (который будет проверять данные при каждом нажатии клавиши) или в обработчике onchange:
<form {...createPost} oninput={() => createPost.validate()}> <!-- --></form>По умолчанию ошибки будут игнорироваться, если они относятся к элементам управления формы, с которыми еще не взаимодействовали. Чтобы провалидировать все поля ввода, вызовите validate({ includeUntouched: true }).
Для валидации на стороне клиента вы можете указать preflight-схему, которая заполнит issues() и предотвратит отправку данных на сервер, если данные не прошли валидацию:
<script> import * as v from 'valibot'; import { createPost } from '../data.remote';
const schema = v.object({ title: v.pipe(v.string(), v.nonEmpty()), content: v.pipe(v.string(), v.nonEmpty()) });</script>
<h1>Создание новой записи</h1>
<form {...+++createPost.preflight(schema)+++}> <!-- --></form>Чтобы получить список всех ошибок, а не только тех, которые относятся к одному конкретному полю, вы можете использовать метод fields.allIssues():
{#each createPost.fields.allIssues() as issue} <p>{issue.message}</p>{/each}Получение и установка значений
Заголовок раздела «Получение и установка значений»У каждого поля есть метод value(), который отражает его текущее значение. По мере того как пользователь взаимодействует с формой, оно обновляется автоматически:
<form {...createPost}> <!-- --></form>
<div class="preview"> <h2>{createPost.fields.title.value()}</h2> <div>{@html render(createPost.fields.content.value())}</div></div>В качестве альтернативы, createPost.fields.value() вернет объект { title, content }.
Вы можете обновить поле (или набор полей) с помощью метода set(...):
<script> import { createPost } from '../data.remote';
// это... createPost.fields.set({ title: 'Моя новая запись в блоге', content: 'Lorem ipsum dolor sit amet...' });
// ...эквивалентно этому: createPost.fields.title.set('Моя новая запись в блоге'); createPost.fields.content.set('Lorem ipsum dolor sit amet');</script>Обработка конфиденциальных данных
Заголовок раздела «Обработка конфиденциальных данных»В случае отправки формы без прогрессивного улучшения (то есть когда JavaScript по какой-либо причине недоступен) метод value() также заполняется, если отправленные данные оказались невалидными. Это сделано для того, чтобы пользователю не пришлось заполнять всю форму заново с нуля.
Вы можете предотвратить отправку конфиденциальных данных (таких как пароли и номера кредитных карт) обратно пользователю, используя имя, начинающееся с нижнего подчеркивания:
<form {...register}> <label> Имя пользователя <input {...register.fields.username.as('text')} /> </label>
<label> Пароль <input {...register.fields._password.as('password')} /> </label>
<button>Зарегистрироваться!</button></form>В данном примере, если данные не проходят валидацию, при перезагрузке страницы будет заполнено только первое поле <input>.
Однопроходные мутации
Заголовок раздела «Однопроходные мутации»По умолчанию после успешной отправки формы автоматически обновляются все запросы на странице (вместе с любыми функциями load). Это гарантирует актуальность данных, но неэффективно: многие запросы останутся неизменными, а для получения обновлённых данных потребуется повторный запрос к серверу.
Вместо этого мы можем указать, какие именно запросы нужно обновить после конкретной отправки формы. Это называется однопроходной мутацией, и есть два способа её реализовать. Первый — обновить запрос на сервере, внутри обработчика формы:
export const getPosts = query(async () => { /* ... */ });
export const getPost = query(v.string(), async (slug) => { /* ... */ });
export const createPost = form(async (data) => { // логика формы...
// Обновляем `getPosts()` на сервере и отправляем // данные вместе с результатом `createPost` await getPosts().refresh();
// Перенаправляем на новую страницу redirect(303, `/blog/${slug}`);});Второй способ — управлять однопроходной мутацией с клиента, что мы рассмотрим в разделе про enhance.
Возвращаемые значения и перенаправления
Заголовок раздела «Возвращаемые значения и перенаправления»В примере выше используется redirect(...), который перенаправляет пользователя на новую страницу. Альтернативно, колбэк может возвращать данные, которые станут доступны как createPost.result:
export const createPost = form(async (data) => { // ...
return { success: true };});<script> import { createPost } from '../data.remote';</script>
<h1>Создание новой записи</h1>
<form {...createPost}><!-- ... --></form>
{#if createPost.result?.success} <p>Запись успешно опубликована!</p>{/if}<script lang="ts"> import { createPost } from '../data.remote';</script>
<h1>Создание новой записи</h1>
<form {...createPost}><!-- ... --></form>
{#if createPost.result?.success} <p>Запись успешно опубликована!</p>{/if}Это значение временное — оно исчезнет при повторной отправке формы, переходе на другую страницу или обновлении страницы.
Если во время отправки произойдёт ошибка, будет отображена ближайшая страница +error.svelte.
enhance
Заголовок раздела «enhance»Мы можем настроить поведение при отправке формы с помощью метода enhance:
<script> import { createPost } from '../data.remote'; import { showToast } from '$lib/toast';</script>
<h1>Создание новой записи</h1>
<form {...createPost.enhance(async ({ form, data, submit }) => { try { await submit(); form.reset();
showToast('Запись успешно опубликована!'); } catch (error) { showToast('Ошибка! Что-то пошло не так'); }})}> <input name="title" /> <textarea name="content"></textarea> <button>Опубликовать</button></form><script lang="ts"> import { createPost } from '../data.remote'; import { showToast } from '$lib/toast';</script>
<h1>Создание новой записи</h1>
<form {...createPost.enhance(async ({ form, data, submit }) => { try { await submit(); form.reset();
showToast('Запись успешно опубликована!'); } catch (error) { showToast('Ошибка! Что-то пошло не так'); }})}> <input name="title" /> <textarea name="content"></textarea> <button>Опубликовать</button></form>Колбэк получает элемент form, содержащиеся в нём data и функцию submit.
Для реализации однопроходных мутаций на клиенте используйте submit().updates(...). Например, если на этой странице используется запрос getPosts(), мы можем обновить его следующим образом:
await submit().updates(getPosts());Мы также можем переопределить текущие данные во время выполнения отправки формы:
await submit().updates( getPosts().withOverride((posts) => [newPost, ...posts]));Переопределённые данные применяются немедленно и сбрасываются после завершения (или ошибки) отправки формы.
Несколько экземпляров формы
Заголовок раздела «Несколько экземпляров формы»Некоторые формы могут повторяться как часть списка. В этом случае вы можете создавать отдельные экземпляры функции формы с помощью for(id), чтобы обеспечить их изоляцию.
Если каждый экземпляр должен отображать разные значения, передайте их в качестве второго аргумента в .as(...):
<script> import { getTodos, modifyTodo } from '../data.remote';</script>
<h1>Список дел</h1>
{#each await getTodos() as todo} {@const modify = modifyTodo.for(todo.id)} <form {...modify}> <input {...modify.fields.description.as('text', todo.description)} /> <button disabled={!!modify.pending}>Сохранить изменения</button> </form>{/each}Несколько кнопок отправки
Заголовок раздела «Несколько кнопок отправки»В форме <form> можно использовать несколько кнопок отправки. Например, вы можете иметь одну форму, которая позволяет либо войти в систему, либо зарегистрироваться — в зависимости от того, какая кнопка была нажата.
Чтобы это реализовать, добавьте в свою схему поле для значения кнопки и используйте as('submit', value) для его привязки:
<script> import { loginOrRegister } from '$lib/auth';</script>
<form {...loginOrRegister}> <label> Ваш логин <input {...loginOrRegister.fields.username.as('text')} /> </label>
<label> Ваш пароль <input {...loginOrRegister.fields._password.as('password')} /> </label>
<button {...loginOrRegister.fields.action.as('submit', 'login')}>Войти</button> <button {...loginOrRegister.fields.action.as('submit', 'register')}>Зарегистрироваться</button></form><script lang="ts"> import { loginOrRegister } from '$lib/auth';</script>
<form {...loginOrRegister}> <label> Ваш логин <input {...loginOrRegister.fields.username.as('text')} /> </label>
<label> Ваш пароль <input {...loginOrRegister.fields._password.as('password')} /> </label>
<button {...loginOrRegister.fields.action.as('submit', 'login')}>Войти</button> <button {...loginOrRegister.fields.action.as('submit', 'register')}>Зарегистрироваться</button></form>В обработчике формы вы можете проверить, какая именно кнопка была нажата:
import * as v from 'valibot';import { form } from '$app/server';
export const loginOrRegister = form( v.object({ username: v.string(), _password: v.string(), action: v.picklist(['login', 'register']) }), async ({ username, _password, action }) => { if (action === 'login') { // обработка входа } else { // обработка регистрации } });command
Заголовок раздела «command»Функция command, аналогично form, позволяет записывать данные на сервер. В отличие от form, она не привязана к элементу и может вызываться из любого места.
Как и в случае с query, если функция принимает аргумент, его следует валидировать, передав Standard Schema в качестве первого аргумента command.
import * as v from 'valibot';import { query, command } from '$app/server';import * as db from '$lib/server/database';
export const getLikes = query(v.string(), async (id) => { const [row] = await db.sql` SELECT likes FROM item WHERE id = ${id} `;
return row.likes;});
export const addLike = command(v.string(), async (id) => { await db.sql` UPDATE item SET likes = likes + 1 WHERE id = ${id} `;});Теперь можно просто вызвать addLike, например, из обработчика событий:
<script> import { getLikes, addLike } from './likes.remote'; import { showToast } from '$lib/toast';
let { item } = $props();</script>
<button onclick={async () => { try { await addLike(item.id); } catch (error) { showToast('Произошла ошибка!'); } }}> Лайкнуть</button>
<p>Лайков: {await getLikes(item.id)}</p><script lang="ts"> import { getLikes, addLike } from './likes.remote'; import { showToast } from '$lib/toast';
let { item } = $props();</script>
<button onclick={async () => { try { await addLike(item.id); } catch (error) { showToast('Произошла ошибка!'); } }}> Лайкнуть</button>
<p>Лайков: {await getLikes(item.id)}</p>Однопроходные мутации
Заголовок раздела «Однопроходные мутации»Как и с формами, все запросы на странице (такие как getLikes(item.id) в примере выше) автоматически обновляются после успешного выполнения команды. Но мы можем сделать это более эффективным, указав SvelteKit, какие именно запросы будут затронуты командой, либо внутри самой команды…
export const getLikes = query(v.string(), async (id) => { /* ... */ });
export const addLike = command(v.string(), async (id) => { await db.sql` UPDATE item SET likes = likes + 1 WHERE id = ${id} `;
getLikes(id).refresh();});…либо при её вызове:
try { await addLike(item.id).updates(getLikes(item.id));} catch (error) { showToast('Что-то пошло не так!');}Как и ранее, мы можем использовать withOverride для оптимистичных обновлений:
try { await addLike(item.id).updates( getLikes(item.id).withOverride((n) => n + 1) );} catch (error) { showToast('Что-то пошло не так!');}prerender
Заголовок раздела «prerender»Функция prerender аналогична query, но вызывается во время сборки для предварительного рендеринга результата. Используйте её для данных, которые меняются не чаще одного раза при повторном развёртывании.
import { prerender } from '$app/server';import * as db from '$lib/server/database';
export const getPosts = prerender(async () => { const posts = await db.sql` SELECT title, slug FROM post ORDER BY published_at DESC `;
return posts;});Вы можете использовать функции prerender на страницах, которые в остальном динамические, что позволяет частично предварительно рендерить данные. Это обеспечивает очень быструю навигацию, так как предварительно отрендеренные данные могут храниться на CDN вместе с другими статическими ресурсами.
В браузере предварительно отрендеренные данные сохраняются с помощью API Cache. Этот кеш сохраняется при перезагрузке страницы и очищается при первом посещении новой версии вашего приложения.
Аргументы prerender
Заголовок раздела «Аргументы prerender»Как и в запросах, функции prerender могут принимать аргументы, которые следует проверять с помощью Standard Schema:
import * as v from 'valibot';import { error } from '@sveltejs/kit';import { prerender } from '$app/server';import * as db from '$lib/server/database';
export const getPosts = prerender(async () => { /* ... */ });
export const getPost = prerender(v.string(), async (slug) => { const [post] = await db.sql` SELECT * FROM post WHERE slug = ${slug} `;
if (!post) error(404, 'Не найдено'); return post;});Все вызовы getPost(...), обнаруженные краулером SvelteKit при предварительном рендеринге страниц, будут сохранены автоматически. Однако вы также можете явно указать, с какими значениями следует вызывать функцию, используя параметр inputs:
export const getPost = prerender( v.string(), async (slug) => { /* ... */ }, { inputs: () => [ 'first-post', 'second-post', 'third-post' ] });export const getPost = prerender( v.string(), async (slug) => { /* ... */ }, { inputs: () => [ 'first-post', 'second-post', 'third-post' ] });По умолчанию функции prerender исключаются из серверного бандла, что означает невозможность их вызова с аргументами, которые не были предварительно отрендерены. Вы можете изменить это поведение, установив dynamic: true:
export const getPost = prerender( v.string(), async (slug) => { /* ... */ }, { dynamic: true, inputs: () => [ 'first-post', 'second-post', 'third-post' ] });export const getPost = prerender( v.string(), async (slug) => { /* ... */ }, { dynamic: true, inputs: () => [ 'first-post', 'second-post', 'third-post' ] });Обработка ошибок валидации
Заголовок раздела «Обработка ошибок валидации»Если вы не передаёте невалидные данные в удалённые функции, есть только две причины, по которым аргумент command, query или prerender может не пройти валидацию:
- Сигнатура функции изменилась между развёртываниями, и некоторые пользователи используют старую версию приложения
- Кто-то пытается атаковать ваш сайт, подбирая данные к вашим эндпойнтам
Во втором случае мы не хотим помогать атакующему, поэтому SvelteKit сгенерирует стандартный ответ 400 Bad Request. Вы можете кастомизировать сообщение, реализовав серверный хук handleValidationError, который, как и handleError, должен возвращать тип App.Error (по умолчанию это { message: string }):
/** @type {import('@sveltejs/kit').HandleValidationError} */export function handleValidationError({ event, issues }) { return { message: 'Хорошая попытка, хакер!' };}import type { HandleValidationError } from '@sveltejs/kit';
export const handleValidationError: HandleValidationError = ({ event, issues }) => { return { message: 'Хорошая попытка, хакер!' };};Если вы понимаете, что делаете, и хотите отключить валидацию, вы можете передать строку 'unchecked' вместо схемы:
import { query } from '$app/server';
export const getStuff = query('unchecked', async ({ id }: { id: string }) => { // фактическая структура данных может отличаться от ожидаемой TypeScript, // так как злоумышленники могут вызывать эту функцию с другими аргументами});Использование getRequestEvent
Заголовок раздела «Использование getRequestEvent»Внутри query, form и command вы можете использовать getRequestEvent для получения текущего объекта RequestEvent. Это упрощает создание абстракций для работы с файлами куки, например:
import { getRequestEvent, query } from '$app/server';import { findUser } from '$lib/server/database';
export const getProfile = query(async () => { const user = await getUser();
return { name: user.name, avatar: user.avatar };});
// эта функция может вызываться из разных мест:function getUser() { const { cookies, locals } = getRequestEvent();
locals.userPromise ??= findUser(cookies.get('session_id')); return await locals.userPromise;}Обратите внимание, что некоторые свойства RequestEvent отличаются в удалённых функциях. Отсутствуют params и route.id, нельзя устанавливать заголовки (кроме записи куки, и то только внутри функций form и command), а url.pathname всегда равен / (поскольку фактический путь запроса клиента является внутренней деталью реализации).
Перенаправления
Заголовок раздела «Перенаправления»Внутри функций query, form и prerender можно использовать функцию redirect(...). В функциях command это нельзя делать, так как перенаправления там следует избегать. (Если это абсолютно необходимо, можно вернуть объект { redirect: location } и обработать его на клиенте.)