LEARN X · ЗА 15 МИН

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>
Поддержать проект