3. Базовый синтаксис
Базовый синтаксис Svelte: шаблоны и выражения 📝
Заголовок раздела «Базовый синтаксис Svelte: шаблоны и выражения 📝»Svelte-шаблон — это расширенный HTML. Ты пишешь обычные теги, добавляешь {выражения} прямо в разметку, и Svelte компилирует всё в оптимальный DOM-код. Никакого virtual DOM, никаких лишних абстракций — всё чисто и прямолинейно! 🎯
Интерполяция: {expression} ✨
Заголовок раздела «Интерполяция: {expression} ✨»В фигурных скобках может быть любое 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 не добавится -->Сокращённые атрибуты: {name} = name={name} 🎯
Заголовок раздела «Сокращённые атрибуты: {name} = name={name} 🎯»Когда имя переменной совпадает с именем атрибута — пиши сокращённо:
<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 при передаче данных в компоненты!
Динамические классы: директива class: 🎨
Заголовок раздела «Динамические классы: директива class: 🎨»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: vs тернарный оператор — что лучше?
Заголовок раздела «class: vs тернарный оператор — что лучше?»<!-- ✅ Директива class: — чисто и декларативно --><div class:active={isActive} class:selected={isSelected}> Элемент</div>
<!-- ❌ Тернарный оператор — громоздко --><div class={isActive ? 'active' : ''}> Элемент</div>
<!-- ❌ Ещё хуже — строковая конкатенация --><div class={'btn' + (isActive ? ' active' : '') + (isSelected ? ' selected' : '')}> Элемент</div>Директива class: — лучший выбор, когда условие добавляет/убирает один класс.
Директива style: 💅
Заголовок раздела «Директива style: 💅»Аналог 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} — вставка сырого HTML 🔓
Заголовок раздела «{@html} — вставка сырого HTML 🔓»Иногда нужно вставить готовый 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>⚠️ Защита от XSS через DOMPurify
Заголовок раздела «⚠️ Защита от XSS через DOMPurify»<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 dompurifynpm install -D @types/dompurifyОпасные примеры — что блокирует DOMPurify
Заголовок раздела «Опасные примеры — что блокирует 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>'{@debug} — точки остановки в шаблоне 🐛
Заголовок раздела «{@debug} — точки остановки в шаблоне 🐛»<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} срабатывает как точка останова — можно исследовать переменные в момент рендера.
{@const} — локальная константа в блоке 📌
Заголовок раздела «{@const} — локальная константа в блоке 📌»Очень полезно внутри {#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}Условный рендеринг: {#if} 🔀
Заголовок раздела «Условный рендеринг: {#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}Тонкости {#if}
Заголовок раздела «Тонкости {#if}»<!-- Svelte НЕ удаляет и не переиспользует DOM при переключении --><!-- Каждый раз — полное создание/удаление DOM-узлов --><!-- Для условного показа/скрытия без удаления — используй style -->
<!-- Показ/скрытие без удаления из DOM: --><div style:display={isVisible ? 'block' : 'none'}> Всегда в DOM, только скрыт</div>
<!-- Удаление из DOM при false: -->{#if isVisible} <div>Удаляется при false</div>{/if}Рендеринг списков: {#each} 📋
Заголовок раздела «Рендеринг списков: {#each} 📋»<script lang="ts"> interface User { id: number; name: string; email: string; active: boolean; }
let users = $state<User[]>([ ]);</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}Ключи в {#each} — зачем?
Заголовок раздела «Ключи в {#each} — зачем?»<!-- ❌ Без ключа: при изменении массива Svelte обновляет по позиции --><!-- Если добавить элемент в начало — все компоненты пересоздаются! -->{#each items as item} <Item {item} />{/each}
<!-- ✅ С ключом: Svelte отслеживает элементы по id --><!-- Добавление в начало — только один новый компонент! -->{#each items as item (item.id)} <Item {item} />{/each}Блоки ожидания: {#await} ⏳
Заголовок раздела «Блоки ожидания: {#await} ⏳»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>Комментарии в Svelte 💬
Заголовок раздела «Комментарии в Svelte 💬»<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:! 🎮