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

3. Базовый синтаксис

Базовый синтаксис Svelte: шаблоны и выражения 📝

Заголовок раздела «Базовый синтаксис Svelte: шаблоны и выражения 📝»

Svelte-шаблон — это расширенный HTML. Ты пишешь обычные теги, добавляешь {выражения} прямо в разметку, и Svelte компилирует всё в оптимальный DOM-код. Никакого virtual DOM, никаких лишних абстракций — всё чисто и прямолинейно! 🎯


В фигурных скобках может быть любое JavaScript-выражение:

<script lang="ts">
let name = $state('Яша');
let age = $state(25);
let items = $state(['Svelte', 'TypeScript', 'Vite']);
</script>
<!-- Переменная -->
<p>Привет, {name}!</p>
<!-- Выражение -->
<p>Через 10 лет тебе будет {age + 10} лет</p>
<!-- Вызов метода -->
<p>{name.toUpperCase()}</p>
<!-- Тернарный оператор -->
<p>{age >= 18 ? 'Взрослый' : 'Молодой'}</p>
<!-- Метод массива -->
<p>Технологии: {items.join(', ')}</p>
<!-- Вычисление -->
<p>π ≈ {Math.PI.toFixed(4)}</p>
<!-- Шаблонная строка -->
<title>{`${name} — личная страница`}</title>
<script lang="ts">
let userId = $state(42);
let isDisabled = $state(false);
let imageUrl = $state('/avatar.jpg');
let altText = $state('Аватар пользователя');
</script>
<!-- В любом атрибуте -->
<a href={"/user/" + userId}>Профиль</a>
<img src={imageUrl} alt={altText} />
<button disabled={isDisabled}>Нажми меня</button>
<!-- Булевы атрибуты: false = атрибут не добавляется, true = добавляется -->
<input type="checkbox" checked={true} />
<input type="text" readonly={false} /> <!-- readonly не добавится -->

Когда имя переменной совпадает с именем атрибута — пиши сокращённо:

<script lang="ts">
let id = $state('my-input');
let value = $state('Привет');
let disabled = $state(false);
let placeholder = $state('Введи текст...');
</script>
<!-- Полная запись -->
<input id={id} value={value} disabled={disabled} placeholder={placeholder} />
<!-- Сокращённая запись — то же самое! -->
<input {id} {value} {disabled} {placeholder} />
<!-- Работает и для компонентов -->
<UserCard {name} {age} {email} />
<!-- Смешанная запись -->
<input {id} type="text" {value} class="my-input" />

Это сильно сокращает boilerplate при передаче данных в компоненты!


Svelte предлагает специальную директиву для условных классов:

<script lang="ts">
let isActive = $state(false);
let isSelected = $state(true);
let hasError = $state(false);
let theme = $state('dark');
</script>
<!-- Синтаксис: class:имя-класса={условие} -->
<div class:active={isActive}>Элемент</div>
<!-- Несколько директив — можно комбинировать -->
<button
class:active={isActive}
class:selected={isSelected}
class:error={hasError}
>
Кнопка
</button>
<!-- Сокращение: если имя переменной = имя класса -->
<div class:active>
<!-- class:active = class:active={active} -->
</div>
<!-- Комбинация с обычным class -->
<div class="btn" class:btn--primary={theme === 'dark'} class:btn--active={isActive}>
Кнопка со статическим и динамическим классом
</div>
<!-- ✅ Директива class: — чисто и декларативно -->
<div class:active={isActive} class:selected={isSelected}>
Элемент
</div>
<!-- ❌ Тернарный оператор — громоздко -->
<div class={isActive ? 'active' : ''}>
Элемент
</div>
<!-- ❌ Ещё хуже — строковая конкатенация -->
<div class={'btn' + (isActive ? ' active' : '') + (isSelected ? ' selected' : '')}>
Элемент
</div>

Директива class: — лучший выбор, когда условие добавляет/убирает один класс.


Аналог class: но для инлайновых стилей:

<script lang="ts">
let color = $state('#ff3e00');
let fontSize = $state(16);
let isVisible = $state(true);
let bgColor = $state('#1e293b');
</script>
<!-- Синтаксис: style:css-property={value} -->
<p style:color={color}>Текст в цвете</p>
<!-- Для значений с единицами — через шаблонную строку -->
<p style:font-size="{fontSize}px">Размер текста</p>
<!-- Несколько директив -->
<div
style:color={color}
style:font-size="{fontSize}px"
style:background-color={bgColor}
style:display={isVisible ? 'block' : 'none'}
>
Стилизованный элемент
</div>
<!-- Kebab-case и camelCase — оба работают -->
<div style:backgroundColor={bgColor}>Фон</div>
<div style:background-color={bgColor}>Тоже фон</div>
<!-- style: перезапишет style="" атрибут для конкретного свойства -->
<div style="color: blue; font-size: 20px" style:color={color}>
<!-- color будет из переменной, font-size — из атрибута -->
</div>

Иногда нужно вставить готовый HTML: из CMS, Markdown-конвертера, rich text редактора.

<script lang="ts">
// ✅ Безопасный HTML из надёжного источника
let safeContent = $state(
'<strong>Жирный</strong> и <em>курсивный</em> текст с <code>кодом</code>'
);
// ✅ HTML сгенерированный на сервере или прошедший санитизацию
let blogPost = $state('<p>Первый абзац...</p><p>Второй абзац...</p>');
</script>
<!-- Вставка HTML — НЕ санитизируется Svelte! -->
{@html safeContent}
<!-- В элементе -->
<div class="content">{@html blogPost}</div>
<script lang="ts">
import DOMPurify from 'dompurify';
// Данные из ненадёжного источника (пользовательский ввод)
let userInput = $state('');
// ✅ ВСЕГДА санитизируй перед {@html}!
let sanitized = $derived(DOMPurify.sanitize(userInput));
</script>
<div>{@html sanitized}</div>
<!-- ❌ НИКОГДА не делай так с пользовательским вводом! -->
<!-- <div>{@html userInput}</div> -->
Окно терминала
npm install dompurify
npm install -D @types/dompurify
// ❌ XSS через img onerror
'<img src=x onerror="alert(\'XSS\')">'
// DOMPurify → '<img src="x">'
// ❌ Инжекция скрипта
'<script>document.cookie = "stolen"</script>'
// DOMPurify → ''
// ❌ Обработчик события
'<a href="#" onclick="stealCookies()">Клик</a>'
// DOMPurify → '<a href="#">Клик</a>'

<script lang="ts">
let user = $state({ name: 'Яша', role: 'admin' });
let items = $state([1, 2, 3]);
</script>
<!-- Приостанавливает DevTools и логирует значения -->
<!-- Работает только в dev-режиме, в prod игнорируется -->
{@debug user}
<!-- Несколько переменных -->
{@debug user, items}
<!-- Без аргументов — аналог debugger; -->
{@debug}
<p>{user.name}</p>

Когда DevTools открыты, {@debug} срабатывает как точка останова — можно исследовать переменные в момент рендера.


Очень полезно внутри {#each} для промежуточных вычислений:

<script lang="ts">
interface Product {
name: string;
price: number;
quantity: number;
discount: number;
}
let cart = $state<Product[]>([
{ name: 'Ноутбук', price: 80000, quantity: 1, discount: 0.1 },
{ name: 'Мышь', price: 2500, quantity: 2, discount: 0 },
{ name: 'Клавиатура', price: 5000, quantity: 1, discount: 0.15 },
]);
</script>
{#each cart as item}
{@const discountedPrice = item.price * (1 - item.discount)}
{@const total = discountedPrice * item.quantity}
{@const savings = (item.price - discountedPrice) * item.quantity}
<div class="cart-item">
<span>{item.name}</span>
<span>{discountedPrice.toFixed(0)} ₽ × {item.quantity}</span>
<strong>= {total.toFixed(0)} ₽</strong>
{#if savings > 0}
<small>Скидка: {savings.toFixed(0)} ₽</small>
{/if}
</div>
{/each}
<!-- @const работает и внутри #if -->
{#if user}
{@const fullName = user.firstName + ' ' + user.lastName}
{@const initials = fullName.split(' ').map(n => n[0]).join('')}
<p>{fullName} ({initials})</p>
{/if}

<script lang="ts">
let isLoggedIn = $state(false);
let role = $state<'admin' | 'user' | 'guest'>('guest');
let score = $state(75);
let user = $state<{ name: string } | null>(null);
</script>
<!-- Базовый if -->
{#if isLoggedIn}
<p>Добро пожаловать!</p>
{/if}
<!-- if/else -->
{#if isLoggedIn}
<button>Выйти</button>
{:else}
<button>Войти</button>
{/if}
<!-- if/else if/else — сколько угодно ветвей -->
{#if role === 'admin'}
<AdminPanel />
{:else if role === 'user'}
<UserDashboard />
{:else}
<GuestPage />
{/if}
<!-- Вложенные if -->
{#if user}
{#if user.name}
<p>Привет, {user.name}!</p>
{:else}
<p>Привет, незнакомец!</p>
{/if}
{/if}
<!-- Проверка на null/undefined -->
{#if user !== null}
<UserCard data={user} />
{/if}
<!-- Svelte НЕ удаляет и не переиспользует DOM при переключении -->
<!-- Каждый раз — полное создание/удаление DOM-узлов -->
<!-- Для условного показа/скрытия без удаления — используй style -->
<!-- Показ/скрытие без удаления из DOM: -->
<div style:display={isVisible ? 'block' : 'none'}>
Всегда в DOM, только скрыт
</div>
<!-- Удаление из DOM при false: -->
{#if isVisible}
<div>Удаляется при false</div>
{/if}

<script lang="ts">
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
let users = $state<User[]>([
{ id: 1, name: 'Яша', email: '[email protected]', active: true },
{ id: 2, name: 'Маша', email: '[email protected]', active: false },
{ id: 3, name: 'Петя', email: '[email protected]', active: true },
]);
</script>
<!-- Базовый each -->
{#each users as user}
<div>{user.name}</div>
{/each}
<!-- С индексом -->
{#each users as user, index}
<div>{index + 1}. {user.name}</div>
{/each}
<!-- С ключом (key) — ВАЖНО для производительности и анимаций! -->
{#each users as user (user.id)}
<UserCard {user} />
{/each}
<!-- Деструктуризация -->
{#each users as { id, name, email, active } (id)}
<tr class:inactive={!active}>
<td>{id}</td>
<td>{name}</td>
<td>{email}</td>
</tr>
{/each}
<!-- :else — когда массив пуст -->
{#each users as user (user.id)}
<UserCard {user} />
{:else}
<p>Пользователей нет</p>
{/each}
<!-- ❌ Без ключа: при изменении массива Svelte обновляет по позиции -->
<!-- Если добавить элемент в начало — все компоненты пересоздаются! -->
{#each items as item}
<Item {item} />
{/each}
<!-- ✅ С ключом: Svelte отслеживает элементы по id -->
<!-- Добавление в начало — только один новый компонент! -->
{#each items as item (item.id)}
<Item {item} />
{/each}

Svelte умеет работать с промисами прямо в шаблоне:

<script lang="ts">
interface User {
id: number;
name: string;
email: string;
}
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) throw new Error('Пользователь не найден');
return response.json();
}
let userPromise = $state(fetchUser(1));
</script>
<!-- Полный синтаксис -->
{#await userPromise}
<!-- Показывается пока промис pending -->
<div class="loading">⏳ Загружаем данные...</div>
{:then user}
<!-- Показывается когда промис resolved -->
<div class="user-card">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
{:catch error}
<!-- Показывается когда промис rejected -->
<div class="error">❌ Ошибка: {error.message}</div>
{/await}
<!-- Упрощённый синтаксис — без pending состояния -->
{#await fetchUser(2) then user}
<p>{user.name}</p>
{/await}
<!-- С обработкой ошибок -->
{#await fetchUser(3) then user}
<UserCard {user} />
{:catch error}
<ErrorMessage message={error.message} />
{/await}
<script lang="ts">
let userId = $state(1);
// Каждый раз когда userId меняется — промис пересоздаётся
let userPromise = $derived(fetchUser(userId));
</script>
{#await userPromise}
<Spinner />
{:then user}
<UserProfile {user} />
{:catch error}
<ErrorBanner {error} />
{/await}
<div>
<button onclick={() => userId--} disabled={userId <= 1}>← Предыдущий</button>
<button onclick={() => userId++}>Следующий →</button>
</div>

<script lang="ts">
// Обычные JavaScript комментарии — не попадают в HTML
let count = $state(0); // инлайн комментарий
/*
Многострочный JS комментарий
Тоже не попадает в HTML
*/
</script>
<!-- HTML комментарий — виден в DevTools, но не на экране -->
<!-- ⚠️ HTML комментарии ПОПАДАЮТ в итоговый HTML! -->
<!-- Не пиши в них чувствительную информацию -->
<!-- Комментирование кода Svelte — работает -->
<!-- {#if condition}
<div>Закомментированный блок</div>
{/if} -->
<style>
/* CSS комментарий — не попадает в HTML (стили scoped) */
p {
color: red; /* инлайн CSS комментарий */
}
</style>

<!-- Компоненты без содержимого (слотов) можно самозакрывать -->
<UserAvatar src={avatarUrl} alt="Аватар" />
<Divider />
<Spinner size={24} />
<!-- Эквивалентно -->
<UserAvatar src={avatarUrl} alt="Аватар"></UserAvatar>
<!-- HTML элементы самозакрывать НЕЛЬЗЯ (только void элементы) -->
<br /> <!-- ✅ void element -->
<input /> <!-- ✅ void element -->
<img /> <!-- ✅ void element -->
<div /> <!-- ❌ нельзя — Svelte/браузер воспримет неожиданно -->

<!-- Рекомендуемый порядок секций в .svelte файле -->
<!-- 1. module script (если нужен) -->
<script context="module">
export const componentName = 'MyComponent';
</script>
<!-- 2. Основной script -->
<script lang="ts">
// 2.1 Импорты
import { onMount } from 'svelte';
import ChildComponent from './ChildComponent.svelte';
// 2.2 Props
let { title, count = $bindable(0) } = $props();
// 2.3 Локальное состояние
let isLoading = $state(false);
// 2.4 Вычисляемые значения
let doubled = $derived(count * 2);
// 2.5 Эффекты
$effect(() => { /* ... */ });
// 2.6 Функции и обработчики
function handleClick() { /* ... */ }
</script>
<!-- 3. Шаблон -->
<main>
<!-- Контент -->
</main>
<!-- 4. Стили (всегда последними) -->
<style>
main { /* ... */ }
</style>

Ключевые элементы синтаксиса Svelte:

  • {expression} — любое JS-выражение в шаблоне или атрибуте
  • {name} — сокращение для name={name} в атрибутах
  • class:name={bool} — условное добавление CSS-класса
  • style:prop={value} — инлайн стиль через директиву
  • {@html} — вставка сырого HTML (осторожно, XSS!)
  • {@debug} — точка останова в DevTools
  • {@const} — локальная константа внутри блока
  • {#if}...{:else if}...{:else}...{/if} — условный рендеринг
  • {#each arr as item, i (key)}...{:else}...{/each} — списки
  • {#await promise}...{:then}...{:catch}...{/await} — асинхронность

Следующий шаг — события и двусторонняя привязка через bind:! 🎮