Svelte
Экспресс-тур по Svelte за 15 минут: компонент, реактивность ($:/руны), bind, события, #if/#each, props, слоты, сторы, onMount, переходы.
Svelte — это компилятор UI: он превращает компоненты в маленький императивный JavaScript, который точечно меняет DOM. Никакого virtual DOM и рантайм-фреймворка в бандле — почти всё происходит на этапе сборки. Ниже весь язык одним прохождением через закомментированный код.
Что такое Svelte
Не библиотека рантайма, а компилятор. Компоненты живут в файлах .svelte и собираются обычно через Vite (шаблон npm create vite@latest -- --template svelte).
<!-- App.svelte -->
<!--
Svelte НЕ использует virtual DOM.
Компилятор анализирует код и генерирует точечные
обновления DOM — поэтому бандл маленький и быстрый.
Один .svelte-файл = один компонент и состоит из трёх частей:
1) <script> — логика на JavaScript
2) разметка — HTML с расширениями Svelte
3) <style> — CSS, по умолчанию ИЗОЛИРОВАННЫЙ для компонента
-->
<h1>Привет, Svelte!</h1>
Структура компонента
Три блока в любом порядке. <style> по умолчанию scoped — стиль не «протекает» наружу.
<script>
// Обычный JavaScript. Переменные верхнего уровня
// автоматически доступны в разметке ниже.
let name = 'мир';
let className = 'box';
</script>
<!-- Разметка: {выражение} вставляет значение -->
<div class={className}>
Привет, {name}! Сумма: {2 + 2}
</div>
<style>
/* Этот стиль применится ТОЛЬКО к div внутри
этого компонента (scoped по умолчанию). */
.box {
padding: 8px;
border: 1px solid gray;
}
</style>
Реактивность (Svelte 4)
Присваивание переменной = сигнал обновить DOM. Знак $: объявляет реактивное выражение.
<script>
let count = 0;
// Реактивность срабатывает на ПРИСВАИВАНИИ.
// count = count + 1 -> DOM обновится.
function inc() {
count += 1;
}
// $: — реактивное объявление. Пересчитывается,
// когда меняется любая зависимость справа (count).
$: doubled = count * 2;
// $: с блоком — реактивный побочный эффект.
$: if (count >= 10) {
console.log('Дошли до десяти!');
}
// Важно: мутации НЕ триггерят реактивность.
// arr.push(x) не обновит DOM — нужно присваивание:
let arr = [1, 2];
function add() {
arr = [...arr, arr.length + 1]; // переприсвоили
}
</script>
<button on:click={inc}>count = {count}</button>
<p>Удвоенное: {doubled}</p>
Реактивность (Svelte 5, руны)
В Svelte 5 появились руны — функции-сигналы $state, $derived, $effect. Реактивность теперь явная и работает не только в .svelte, но и в .svelte.js.
<script>
// $state — реактивное состояние (аналог let + реактивность)
let count = $state(0);
// $derived — вычисляемое значение (аналог $: x = ...)
let doubled = $derived(count * 2);
// $effect — побочный эффект при изменении зависимостей
$effect(() => {
console.log('count стал', count);
});
function inc() {
count += 1; // присваивание по-прежнему триггер
}
</script>
<!-- В Svelte 5 события пишут как onclick (без двоеточия) -->
<button onclick={inc}>{count} / {doubled}</button>
Привязка данных (bind)
Двусторонняя связь поля формы и переменной через bind:.
<script>
let text = '';
let agree = false;
let color = 'red';
</script>
<!-- bind:value — текст input синхронизирован с text -->
<input bind:value={text} placeholder="Введите имя" />
<p>Вы ввели: {text}</p>
<!-- bind:checked — для чекбоксов -->
<label>
<input type="checkbox" bind:checked={agree} />
Согласен
</label>
<!-- bind работает и с select, radio, textarea -->
<select bind:value={color}>
<option value="red">Красный</option>
<option value="green">Зелёный</option>
</select>
<p>Цвет: {color}, согласие: {agree}</p>
События
Директива on:событие (Svelte 4) вешает обработчик. Доступны модификаторы: once, preventDefault, stopPropagation.
<script>
let last = '—';
function handleClick(event) {
// event — обычный DOM-событие
last = 'клик по ' + event.target.tagName;
}
function onKey(event) {
last = 'клавиша ' + event.key;
}
</script>
<button on:click={handleClick}>Кликни</button>
<!-- Инлайн-обработчик стрелкой -->
<button on:click={() => last = 'инлайн'}>Инлайн</button>
<!-- Модификаторы через | -->
<form on:submit|preventDefault={() => last = 'submit'}>
<input on:keydown={onKey} />
</form>
<p>Последнее: {last}</p>
Условия и циклы
Логика блоков пишется прямо в разметке: {#if}, {#each}, {#await}.
<script>
let user = { name: 'Аня', loggedIn: true };
let fruits = ['яблоко', 'груша', 'слива'];
</script>
<!-- Условие с :else if / :else -->
{#if user.loggedIn}
<p>Привет, {user.name}!</p>
{:else if user.name}
<p>Войдите, {user.name}</p>
{:else}
<p>Гость</p>
{/if}
<!-- Цикл. Второй параметр (i) — индекс.
(fruit) в конце — КЛЮЧ для эффективного обновления списка.
{:else} внутри #each срабатывает, если массив пуст. -->
<ul>
{#each fruits as fruit, i (fruit)}
<li>{i + 1}. {fruit}</li>
{:else}
<li>Список пуст</li>
{/each}
</ul>
Props (входные параметры)
В Svelte 4 пропсы объявляют через export let. Так компонент принимает данные от родителя.
<!-- Child.svelte -->
<script>
// export let делает переменную ВХОДНЫМ пропсом
export let title; // обязательный
export let count = 0; // со значением по умолчанию
</script>
<h3>{title}: {count}</h3>
<!-- ───────────────────────────── -->
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
let n = 5;
</script>
<!-- Передаём пропсы как атрибуты -->
<Child title="Очки" count={n} />
<!-- Сокращение, если имя совпадает: {n} вместо n={n} -->
<Child title="Жизни" {n} />
<!-- В Svelte 5 пропсы получают через руну $props():
let { title, count = 0 } = $props(); -->
Слоты
Слот — место, куда родитель вставляет свою разметку (как children).
<!-- Card.svelte -->
<script>
export let title = 'Карточка';
</script>
<div class="card">
<header>{title}</header>
<!-- Безымянный слот: сюда попадёт содержимое тега -->
<slot>Контент по умолчанию</slot>
<!-- Именованный слот -->
<footer>
<slot name="footer">Нет подвала</slot>
</footer>
</div>
<!-- ───────────────────────────── -->
<!-- Использование -->
<Card title="Профиль">
<p>Это уйдёт в безымянный слот</p>
<span slot="footer">Подвал</span>
</Card>
Реактивные блоки и вычисления
Несколько $: образуют граф зависимостей — Svelte сам пересчитывает их в правильном порядке.
<script>
let price = 100;
let qty = 2;
// Цепочка реактивных вычислений.
// Svelte отслеживает зависимости и обновляет по порядку:
$: subtotal = price * qty; // зависит от price, qty
$: tax = subtotal * 0.2; // зависит от subtotal
$: total = subtotal + tax; // зависит от обоих
// Реактивный блок может группировать операторы
$: {
if (total > 1000) {
console.log('Дорогой заказ');
}
}
</script>
<input type="number" bind:value={qty} />
<p>Итого с НДС: {total} руб.</p>
Сторы (writable)
Сторы — реактивное состояние вне компонентов, для общих данных. Префикс $ автоматически подписывается и отписывается.
<!-- stores.js -->
import { writable, derived } from 'svelte/store';
// writable — изменяемый стор с начальным значением
export const count = writable(0);
// derived — производный стор
export const doubled = derived(count, ($c) => $c * 2);
<!-- ───────────────────────────── -->
<!-- Component.svelte -->
<script>
import { count, doubled } from './stores.js';
// $count — авто-подписка: читает значение и
// обновляет компонент при изменениях.
function inc() {
count.update((n) => n + 1); // обновить через функцию
// или count.set(5) — задать напрямую
}
</script>
<button on:click={inc}>
{$count} (x2 = {$doubled})
</button>
Жизненный цикл (onMount, onDestroy)
Хуки из svelte: код при появлении и удалении компонента.
<script>
import { onMount, onDestroy } from 'svelte';
let seconds = 0;
let timer;
// onMount — после первой вставки в DOM
// (идеально для запросов и таймеров).
onMount(() => {
timer = setInterval(() => seconds += 1, 1000);
// Можно вернуть функцию очистки — она = onDestroy
return () => clearInterval(timer);
});
// onDestroy — перед удалением компонента
onDestroy(() => {
console.log('Компонент удалён');
});
</script>
<p>Прошло секунд: {seconds}</p>
Переходы и анимации
Директива transition: анимирует появление и исчезновение элемента. Готовые эффекты лежат в svelte/transition.
<script>
import { fade, fly } from 'svelte/transition';
let show = true;
</script>
<button on:click={() => show = !show}>Переключить</button>
{#if show}
<!-- transition: применяется и на вход, и на выход -->
<p transition:fade>Плавное затухание</p>
<!-- in:/out: — разные эффекты, с параметрами -->
<p in:fly={{ y: 20 }} out:fade>Влёт и затух</p>
{/if}
Типичный компонент: счётчик-todo
Собираем пройденное в один рабочий компонент.
<script>
let task = ''; // привязка к input
let todos = []; // список задач
function add() {
const t = task.trim();
if (!t) return;
// Присваивание (не push!) — чтобы сработала реактивность
todos = [...todos, { id: Date.now(), text: t, done: false }];
task = '';
}
function toggle(id) {
todos = todos.map((td) =>
td.id === id ? { ...td, done: !td.done } : td
);
}
function remove(id) {
todos = todos.filter((td) => td.id !== id);
}
// Реактивный счётчик невыполненных
$: left = todos.filter((td) => !td.done).length;
</script>
<form on:submit|preventDefault={add}>
<input bind:value={task} placeholder="Новая задача" />
<button>Добавить</button>
</form>
<p>Осталось: {left} из {todos.length}</p>
<ul>
{#each todos as td (td.id)}
<li>
<label>
<input type="checkbox" checked={td.done}
on:change={() => toggle(td.id)} />
{td.text}
</label>
<button on:click={() => remove(td.id)}>✕</button>
</li>
{/each}
</ul>