Svelte и SvelteKit — Полное руководство

Svelte — это современный компилятор для создания пользовательских интерфейсов. В отличие от React и Vue, Svelte не использует Virtual DOM — он компилирует компоненты в эффективный императивный JavaScript-код во время сборки.


Философия Svelte

Чем отличается от React/Vue

ХарактеристикаSvelteReactVue
ПодходКомпиляторRuntime библиотекаRuntime фреймворк
Virtual DOMНетДаДа
Bundle sizeМинимальный~40KB~35KB
РеактивностьCompile-timeuseState/useReducerref/reactive
BoilerplateМинимальныйСреднийСредний
СтилиScoped по умолчаниюCSS-in-JS/ModulesScoped/Modules

Преимущества Svelte

  1. Меньше кода — более лаконичный синтаксис
  2. Нет Virtual DOM — прямые манипуляции с DOM
  3. Меньший bundle — компилируется только нужный код
  4. Встроенная реактивность — без хуков и оберток
  5. Встроенные анимации — transitions и animations из коробки
  6. Scoped CSS — стили изолированы по умолчанию

Как работает под капотом

┌─────────────────┐     ┌──────────────┐     ┌─────────────────┐
│  .svelte файлы  │ ──► │  Компилятор  │ ──► │  Vanilla JS     │
│                 │     │   Svelte     │     │  (оптимизир.)   │
└─────────────────┘     └──────────────┘     └─────────────────┘

Компилятор анализирует:
1. <script> — JS/TS логика
2. <style> — CSS (автоматически scoped)
3. Разметка — HTML-подобный синтаксис

И генерирует:
- create() — создание DOM
- update() — обновление при изменении данных
- destroy() — очистка

Установка и настройка

Создание проекта Svelte

# С использованием Vite (рекомендуется)
npm create vite@latest my-svelte-app -- --template svelte
# или с TypeScript
npm create vite@latest my-svelte-app -- --template svelte-ts
 
cd my-svelte-app
npm install
npm run dev

Создание проекта SvelteKit

# Официальный способ
npm create svelte@latest my-sveltekit-app
 
cd my-sveltekit-app
npm install
npm run dev
 
# Сборка
npm run build
 
# Превью продакшен сборки
npm run preview

Структура проекта SvelteKit

my-sveltekit-app/
├── src/
│   ├── lib/              # Библиотечные компоненты и утилиты
│   │   ├── components/   # Компоненты
│   │   ├── stores/       # Svelte stores
│   │   └── utils/        # Утилиты
│   ├── routes/           # Страницы (file-based routing)
│   │   ├── +page.svelte  # Главная страница
│   │   ├── +layout.svelte # Корневой layout
│   │   ├── +error.svelte # Страница ошибки
│   │   ├── about/
│   │   │   └── +page.svelte
│   │   └── blog/
│   │       ├── +page.svelte        # /blog
│   │       ├── +page.server.ts     # Server-side логика
│   │       └── [slug]/
│   │           ├── +page.svelte    # /blog/[slug]
│   │           └── +page.ts        # Load функция
│   ├── app.html          # HTML шаблон
│   ├── app.css           # Глобальные стили
│   └── hooks.server.ts   # Server hooks
├── static/               # Статические файлы
├── svelte.config.js      # Конфигурация Svelte
├── vite.config.ts        # Конфигурация Vite
└── tsconfig.json

Основы Svelte

Анатомия компонента

<!-- UserCard.svelte -->
<script lang="ts">
  // Импорты
  import { onMount, onDestroy } from 'svelte';
  import type { User } from '$lib/types';
 
  // Props (входные параметры)
  export let user: User;
  export let showActions: boolean = true;
 
  // Локальное состояние
  let isExpanded = false;
  let clickCount = 0;
 
  // Реактивные вычисления (computed)
  $: fullName = `${user.firstName} ${user.lastName}`;
  $: isActive = user.status === 'active';
 
  // Реактивные statements (выполняются при изменении зависимостей)
  $: {
    console.log('User changed:', user.name);
    clickCount = 0; // Сброс при смене пользователя
  }
 
  $: if (clickCount > 10) {
    console.log('Too many clicks!');
  }
 
  // Методы
  function toggleExpand() {
    isExpanded = !isExpanded;
  }
 
  function handleClick() {
    clickCount += 1;
  }
 
  // Lifecycle hooks
  onMount(() => {
    console.log('Component mounted');
 
    // Возвращаемая функция вызовется при unmount
    return () => {
      console.log('Cleanup on mount return');
    };
  });
 
  onDestroy(() => {
    console.log('Component destroyed');
  });
</script>
 
<!-- Разметка -->
<div class="user-card" class:expanded={isExpanded} class:active={isActive}>
  <img src={user.avatar} alt={fullName} />
 
  <div class="info">
    <h3>{fullName}</h3>
    <p>{user.email}</p>
    <span>Clicks: {clickCount}</span>
  </div>
 
  <button on:click={toggleExpand}>
    {isExpanded ? 'Collapse' : 'Expand'}
  </button>
 
  {#if showActions}
    <div class="actions">
      <button on:click={handleClick}>Click me</button>
    </div>
  {/if}
</div>
 
<!-- Стили (автоматически scoped) -->
<style>
  .user-card {
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 8px;
  }
 
  .user-card.expanded {
    background: #f5f5f5;
  }
 
  .user-card.active {
    border-color: green;
  }
 
  h3 {
    margin: 0;
    color: #333;
  }
 
  /* Глобальные стили */
  :global(.some-global-class) {
    color: red;
  }
</style>

Использование компонента

<!-- App.svelte -->
<script lang="ts">
  import UserCard from './UserCard.svelte';
  import type { User } from '$lib/types';
 
  let users: User[] = [
    { id: 1, firstName: 'John', lastName: 'Doe', email: 'john@example.com', status: 'active' },
    { id: 2, firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com', status: 'inactive' }
  ];
 
  let selectedUser: User | null = null;
 
  function handleSelect(user: User) {
    selectedUser = user;
  }
</script>
 
<main>
  {#each users as user (user.id)}
    <UserCard
      {user}
      showActions={user.status === 'active'}
      on:click={() => handleSelect(user)}
    />
  {/each}
 
  {#if selectedUser}
    <p>Selected: {selectedUser.firstName}</p>
  {/if}
</main>

Реактивность

Реактивные присваивания

<script>
  let count = 0;
  let items = ['apple', 'banana'];
 
  // Простое присваивание — реактивно
  function increment() {
    count += 1; // Триггерит обновление
  }
 
  // Для массивов и объектов нужно присваивание
  function addItem() {
    // ❌ Не работает — мутация без присваивания
    // items.push('cherry');
 
    // ✅ Работает — новый массив
    items = [...items, 'cherry'];
 
    // ✅ Или так
    items.push('cherry');
    items = items; // Триггерим обновление
  }
 
  // Для объектов
  let user = { name: 'John', age: 25 };
 
  function updateUser() {
    // ✅ Spread для обновления
    user = { ...user, age: 26 };
 
    // ✅ Или присваивание после мутации
    user.age = 26;
    user = user;
  }
</script>

Реактивные декларации ($:)

<script>
  let width = 10;
  let height = 20;
 
  // Реактивное вычисляемое значение
  $: area = width * height;
 
  // Реактивный statement (выполняется при изменении зависимостей)
  $: console.log(`Area is now ${area}`);
 
  // Блок с несколькими statements
  $: {
    const perimeter = 2 * (width + height);
    console.log('Perimeter:', perimeter);
  }
 
  // Условные реактивные statements
  $: if (area > 100) {
    console.log('Large area!');
  }
 
  // Деструктуризация
  let obj = { x: 1, y: 2 };
  $: ({ x, y } = obj);
 
  // Зависимость от нескольких переменных
  $: combined = `${width}x${height} = ${area}`;
</script>
 
<input type="number" bind:value={width} />
<input type="number" bind:value={height} />
<p>Area: {area}</p>

Сравнение с React/Vue

<!-- Svelte -->
<script>
  let count = 0;
  $: doubled = count * 2;
</script>
 
<button on:click={() => count++}>
  {count} (doubled: {doubled})
</button>
// React
function Counter() {
  const [count, setCount] = useState(0);
  const doubled = useMemo(() => count * 2, [count]);
 
  return (
    <button onClick={() => setCount(c => c + 1)}>
      {count} (doubled: {doubled})
    </button>
  );
}
<!-- Vue 3 -->
<script setup>
import { ref, computed } from 'vue';
 
const count = ref(0);
const doubled = computed(() => count.value * 2);
</script>
 
<template>
  <button @click="count++">
    {{ count }} (doubled: {{ doubled }})
  </button>
</template>

Шаблонный синтаксис

Условный рендеринг

<script>
  let loggedIn = false;
  let user = { role: 'admin' };
  let status = 'loading'; // 'loading' | 'success' | 'error'
</script>
 
<!-- if -->
{#if loggedIn}
  <p>Welcome back!</p>
{/if}
 
<!-- if/else -->
{#if loggedIn}
  <p>Welcome back!</p>
{:else}
  <p>Please log in</p>
{/if}
 
<!-- if/else if/else -->
{#if user.role === 'admin'}
  <AdminPanel />
{:else if user.role === 'user'}
  <UserPanel />
{:else}
  <GuestPanel />
{/if}
 
<!-- Вложенные условия -->
{#if loggedIn}
  {#if user.role === 'admin'}
    <AdminTools />
  {/if}
{/if}
 
<!-- Switch-подобная логика -->
{#if status === 'loading'}
  <Spinner />
{:else if status === 'error'}
  <Error />
{:else if status === 'success'}
  <Content />
{/if}

Циклы

<script>
  let items = [
    { id: 1, name: 'Apple', color: 'red' },
    { id: 2, name: 'Banana', color: 'yellow' },
    { id: 3, name: 'Cherry', color: 'red' }
  ];
 
  let numbers = [1, 2, 3, 4, 5];
</script>
 
<!-- Простой цикл -->
{#each items as item}
  <div>{item.name}</div>
{/each}
 
<!-- С индексом -->
{#each items as item, index}
  <div>{index + 1}. {item.name}</div>
{/each}
 
<!-- С ключом (важно для анимаций и оптимизации) -->
{#each items as item (item.id)}
  <div>{item.name}</div>
{/each}
 
<!-- Деструктуризация -->
{#each items as { id, name, color } (id)}
  <div style="color: {color}">{name}</div>
{/each}
 
<!-- С else для пустого списка -->
{#each items as item (item.id)}
  <div>{item.name}</div>
{:else}
  <p>No items found</p>
{/each}
 
<!-- Вложенные циклы -->
{#each categories as category}
  <h2>{category.name}</h2>
  {#each category.items as item (item.id)}
    <p>{item.name}</p>
  {/each}
{/each}

Await блоки

<script>
  async function fetchUser(id) {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error('Failed to fetch');
    return response.json();
  }
 
  let userId = 1;
  $: userPromise = fetchUser(userId);
</script>
 
<!-- Полный синтаксис -->
{#await userPromise}
  <p>Loading user...</p>
{:then user}
  <p>Hello, {user.name}!</p>
{:catch error}
  <p class="error">{error.message}</p>
{/await}
 
<!-- Без состояния загрузки -->
{#await userPromise then user}
  <p>Hello, {user.name}!</p>
{/await}
 
<!-- Только обработка ошибок -->
{#await userPromise}
  <p>Loading...</p>
{:then user}
  <p>{user.name}</p>
{:catch}
  <p>Something went wrong</p>
{/await}

Key блоки

<script>
  import Timer from './Timer.svelte';
  let key = 0;
 
  function resetTimer() {
    key += 1; // Пересоздаёт компонент
  }
</script>
 
<!-- Компонент пересоздаётся при изменении key -->
{#key key}
  <Timer />
{/key}
 
<button on:click={resetTimer}>Reset Timer</button>
 
<!-- Полезно для анимаций при смене данных -->
{#key user.id}
  <div in:fade>
    <UserProfile {user} />
  </div>
{/key}

Привязки данных (Bindings)

Привязка значений

<script>
  let name = '';
  let age = 25;
  let agreed = false;
  let color = '#ff0000';
  let selected = 'a';
  let multiSelected = [];
  let volume = 50;
</script>
 
<!-- Text input -->
<input type="text" bind:value={name} />
 
<!-- Number input -->
<input type="number" bind:value={age} />
 
<!-- Checkbox -->
<input type="checkbox" bind:checked={agreed} />
 
<!-- Color picker -->
<input type="color" bind:value={color} />
 
<!-- Range slider -->
<input type="range" min="0" max="100" bind:value={volume} />
 
<!-- Select -->
<select bind:value={selected}>
  <option value="a">Option A</option>
  <option value="b">Option B</option>
  <option value="c">Option C</option>
</select>
 
<!-- Multi-select -->
<select multiple bind:value={multiSelected}>
  <option value="a">A</option>
  <option value="b">B</option>
  <option value="c">C</option>
</select>
 
<!-- Radio buttons -->
<label>
  <input type="radio" bind:group={selected} value="a" />
  Option A
</label>
<label>
  <input type="radio" bind:group={selected} value="b" />
  Option B
</label>
 
<!-- Checkbox group -->
<label>
  <input type="checkbox" bind:group={multiSelected} value="a" />
  A
</label>
<label>
  <input type="checkbox" bind:group={multiSelected} value="b" />
  B
</label>
 
<!-- Textarea -->
<textarea bind:value={content}></textarea>
 
<!-- Contenteditable -->
<div contenteditable bind:innerHTML={html}></div>
<div contenteditable bind:textContent={text}></div>

Привязка к DOM элементам

<script>
  import { onMount } from 'svelte';
 
  let inputElement;
  let divElement;
  let canvasElement;
 
  onMount(() => {
    inputElement.focus();
 
    // Работа с canvas
    const ctx = canvasElement.getContext('2d');
    ctx.fillRect(0, 0, 100, 100);
  });
</script>
 
<!-- bind:this для получения ссылки на DOM элемент -->
<input bind:this={inputElement} />
<div bind:this={divElement}>Content</div>
<canvas bind:this={canvasElement}></canvas>

Привязка размеров

<script>
  let width;
  let height;
  let top;
  let left;
</script>
 
<!-- Привязка размеров (readonly) -->
<div
  bind:clientWidth={width}
  bind:clientHeight={height}
  bind:offsetWidth
  bind:offsetHeight
>
  Size: {width}x{height}
</div>
 
<!-- Привязка позиции скролла -->
<div
  bind:scrollX
  bind:scrollY
>
  <!-- content -->
</div>
 
<!-- Или для window -->
<svelte:window
  bind:scrollY
  bind:innerWidth
  bind:innerHeight
  bind:outerWidth
  bind:outerHeight
/>

Привязка к компонентам

<!-- Counter.svelte -->
<script>
  export let count = 0;
 
  export function reset() {
    count = 0;
  }
</script>
 
<button on:click={() => count++}>{count}</button>
<!-- App.svelte -->
<script>
  import Counter from './Counter.svelte';
 
  let counterValue;
  let counterComponent;
</script>
 
<!-- Двусторонняя привязка к prop -->
<Counter bind:count={counterValue} />
 
<!-- Привязка к экземпляру компонента -->
<Counter bind:this={counterComponent} />
 
<button on:click={() => counterComponent.reset()}>
  Reset
</button>
 
<p>Counter value: {counterValue}</p>

События

DOM события

<script>
  function handleClick(event) {
    console.log('Clicked!', event);
  }
 
  function handleInput(event) {
    console.log('Input:', event.target.value);
  }
 
  function handleKeydown(event) {
    if (event.key === 'Enter') {
      console.log('Enter pressed');
    }
  }
</script>
 
<!-- Базовое событие -->
<button on:click={handleClick}>Click me</button>
 
<!-- Inline handler -->
<button on:click={() => console.log('clicked')}>Click</button>
 
<!-- С event object -->
<button on:click={(e) => console.log(e.clientX, e.clientY)}>
  Click
</button>
 
<!-- События ввода -->
<input on:input={handleInput} />
<input on:change={handleInput} />
<input on:keydown={handleKeydown} />
<input on:focus={() => console.log('focused')} />
<input on:blur={() => console.log('blurred')} />
 
<!-- События мыши -->
<div
  on:mouseenter={() => console.log('enter')}
  on:mouseleave={() => console.log('leave')}
  on:mousemove={(e) => console.log(e.clientX, e.clientY)}
>
  Hover me
</div>
 
<!-- Событие формы -->
<form on:submit|preventDefault={handleSubmit}>
  <button type="submit">Submit</button>
</form>

Модификаторы событий

<!-- preventDefault -->
<form on:submit|preventDefault={handleSubmit}>
 
<!-- stopPropagation -->
<button on:click|stopPropagation={handleClick}>
 
<!-- passive (улучшает производительность скролла) -->
<div on:scroll|passive={handleScroll}>
 
<!-- capture (фаза захвата) -->
<div on:click|capture={handleClick}>
 
<!-- once (один раз) -->
<button on:click|once={handleClick}>
 
<!-- self (только если target === currentTarget) -->
<div on:click|self={handleClick}>
 
<!-- Комбинация модификаторов -->
<form on:submit|preventDefault|stopPropagation={handleSubmit}>
 
<!-- trusted (только user-initiated события) -->
<button on:click|trusted={handleClick}>

Кастомные события компонента

<!-- Button.svelte -->
<script>
  import { createEventDispatcher } from 'svelte';
 
  const dispatch = createEventDispatcher();
 
  export let disabled = false;
 
  function handleClick() {
    dispatch('click', { timestamp: Date.now() });
  }
 
  function handleSpecialAction() {
    dispatch('special', {
      message: 'Something special happened!',
      data: { foo: 'bar' }
    });
  }
</script>
 
<button
  {disabled}
  on:click={handleClick}
  on:dblclick={() => dispatch('dblclick')}
>
  <slot />
</button>
 
<button on:click={handleSpecialAction}>
  Special Action
</button>
<!-- App.svelte -->
<script>
  import Button from './Button.svelte';
 
  function handleClick(event) {
    console.log('Button clicked at:', event.detail.timestamp);
  }
 
  function handleSpecial(event) {
    console.log('Special event:', event.detail);
  }
</script>
 
<Button
  on:click={handleClick}
  on:special={handleSpecial}
>
  Click me
</Button>

Пробрасывание событий (Event Forwarding)

<!-- Inner.svelte -->
<button on:click>
  <slot />
</button>
<!-- Outer.svelte -->
<script>
  import Inner from './Inner.svelte';
</script>
 
<!-- on:click без значения пробрасывает событие выше -->
<Inner on:click>
  <slot />
</Inner>
<!-- App.svelte -->
<script>
  import Outer from './Outer.svelte';
</script>
 
<Outer on:click={() => console.log('clicked')}>
  Click me
</Outer>

Слоты (Slots)

Базовый слот

<!-- Card.svelte -->
<div class="card">
  <slot />
</div>
 
<style>
  .card {
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 8px;
  }
</style>
<!-- App.svelte -->
<Card>
  <h2>Card Title</h2>
  <p>Card content goes here</p>
</Card>

Слот по умолчанию (Fallback)

<!-- Button.svelte -->
<button>
  <slot>
    <!-- Fallback content если слот пустой -->
    Default Button Text
  </slot>
</button>
<Button /> <!-- Покажет "Default Button Text" -->
<Button>Custom Text</Button> <!-- Покажет "Custom Text" -->

Именованные слоты

<!-- Layout.svelte -->
<div class="layout">
  <header>
    <slot name="header">Default Header</slot>
  </header>
 
  <main>
    <slot /> <!-- Дефолтный слот -->
  </main>
 
  <footer>
    <slot name="footer">Default Footer</slot>
  </footer>
</div>
<!-- App.svelte -->
<Layout>
  <h1 slot="header">My App</h1>
 
  <p>Main content here</p>
  <p>More content</p>
 
  <span slot="footer">
    Copyright 2024
  </span>
</Layout>
 
<!-- Или с svelte:fragment для множества элементов -->
<Layout>
  <svelte:fragment slot="header">
    <h1>Title</h1>
    <nav>Navigation</nav>
  </svelte:fragment>
 
  <p>Content</p>
</Layout>

Slot Props (передача данных из слота)

<!-- List.svelte -->
<script>
  export let items = [];
</script>
 
<ul>
  {#each items as item, index (item.id)}
    <li>
      <!-- Передаём данные в слот -->
      <slot {item} {index} isFirst={index === 0} isLast={index === items.length - 1} />
    </li>
  {/each}
</ul>
<!-- App.svelte -->
<script>
  let users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ];
</script>
 
<!-- Получаем данные через let: -->
<List items={users} let:item let:index let:isFirst>
  <span class:first={isFirst}>
    {index + 1}. {item.name}
  </span>
</List>
 
<!-- Для именованных слотов -->
<Component>
  <div slot="content" let:data>
    {data.value}
  </div>
</Component>

Проверка наличия слота

<!-- Card.svelte -->
<script>
  // $$slots — объект с информацией о переданных слотах
</script>
 
<div class="card">
  {#if $$slots.header}
    <header class="card-header">
      <slot name="header" />
    </header>
  {/if}
 
  <div class="card-body">
    <slot />
  </div>
 
  {#if $$slots.footer}
    <footer class="card-footer">
      <slot name="footer" />
    </footer>
  {/if}
</div>

Жизненный цикл

Lifecycle функции

<script>
  import {
    onMount,
    onDestroy,
    beforeUpdate,
    afterUpdate,
    tick
  } from 'svelte';
 
  let data = null;
  let element;
 
  // onMount — после первого рендера в DOM
  onMount(() => {
    console.log('Component mounted');
 
    // Загрузка данных
    fetchData();
 
    // Можно вернуть cleanup функцию
    return () => {
      console.log('Cleanup from onMount');
    };
  });
 
  // onMount с async
  onMount(async () => {
    const response = await fetch('/api/data');
    data = await response.json();
  });
 
  // onDestroy — перед удалением из DOM
  onDestroy(() => {
    console.log('Component will be destroyed');
    // Очистка: таймеры, подписки, listeners
  });
 
  // beforeUpdate — перед обновлением DOM
  beforeUpdate(() => {
    console.log('Before DOM update');
    // Сохранение состояния перед обновлением
  });
 
  // afterUpdate — после обновления DOM
  afterUpdate(() => {
    console.log('After DOM update');
    // Работа с обновлённым DOM
    if (element) {
      element.scrollTop = element.scrollHeight;
    }
  });
 
  async function handleClick() {
    data = 'new value';
 
    // tick — ждёт применения изменений к DOM
    await tick();
 
    // DOM уже обновлён
    console.log('DOM updated');
  }
</script>
 
<div bind:this={element}>
  {data}
</div>

Порядок вызова

1. <script> выполняется
2. onMount (после первого рендера)
3. beforeUpdate (перед каждым обновлением)
4. afterUpdate (после каждого обновления)
5. onDestroy (перед уничтожением)

При изменении данных:
beforeUpdate → DOM обновляется → afterUpdate

tick()

<script>
  import { tick } from 'svelte';
 
  let text = '';
  let inputElement;
 
  async function selectAllText() {
    text = 'New text that we want to select';
 
    // Ждём обновления DOM
    await tick();
 
    // Теперь можем работать с обновлённым DOM
    inputElement.select();
  }
</script>
 
<input bind:this={inputElement} bind:value={text} />
<button on:click={selectAllText}>Set and Select</button>

Stores (Стейт-менеджмент)

Writable Store

// stores/counter.ts
import { writable } from 'svelte/store';
 
// Создание store
export const count = writable(0);
 
// Store с начальным значением
export const user = writable({
  name: '',
  email: ''
});
 
// Store с кастомной логикой
function createCounter() {
  const { subscribe, set, update } = writable(0);
 
  return {
    subscribe,
    increment: () => update(n => n + 1),
    decrement: () => update(n => n - 1),
    reset: () => set(0),
    set: (value: number) => set(value)
  };
}
 
export const counter = createCounter();
<!-- Component.svelte -->
<script>
  import { count, counter } from './stores/counter';
 
  // Подписка с автоотпиской ($)
  // $count — автоматически подписывается и отписывается
</script>
 
<p>Count: {$count}</p>
<button on:click={() => $count++}>Increment</button>
<button on:click={() => $count = 0}>Reset</button>
 
<p>Counter: {$counter}</p>
<button on:click={counter.increment}>+</button>
<button on:click={counter.decrement}>-</button>
<button on:click={counter.reset}>Reset</button>

Readable Store

// stores/time.ts
import { readable } from 'svelte/store';
 
// Store только для чтения
export const time = readable(new Date(), function start(set) {
  // Функция вызывается при первой подписке
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);
 
  // Cleanup при последней отписке
  return function stop() {
    clearInterval(interval);
  };
});
 
// Пример: позиция мыши
export const mousePosition = readable({ x: 0, y: 0 }, (set) => {
  function handleMouseMove(event) {
    set({ x: event.clientX, y: event.clientY });
  }
 
  document.addEventListener('mousemove', handleMouseMove);
 
  return () => {
    document.removeEventListener('mousemove', handleMouseMove);
  };
});
<script>
  import { time, mousePosition } from './stores/time';
</script>
 
<p>Current time: {$time.toLocaleTimeString()}</p>
<p>Mouse: {$mousePosition.x}, {$mousePosition.y}</p>

Derived Store

// stores/derived.ts
import { writable, derived } from 'svelte/store';
 
export const firstName = writable('John');
export const lastName = writable('Doe');
 
// Derived от одного store
export const upperFirstName = derived(
  firstName,
  $firstName => $firstName.toUpperCase()
);
 
// Derived от нескольких stores
export const fullName = derived(
  [firstName, lastName],
  ([$firstName, $lastName]) => `${$firstName} ${$lastName}`
);
 
// Async derived
export const user = writable({ id: 1 });
 
export const userData = derived(
  user,
  ($user, set) => {
    fetch(`/api/users/${$user.id}`)
      .then(res => res.json())
      .then(data => set(data));
 
    // Начальное значение
    return { loading: true };
  },
  { loading: true } // Initial value
);
 
// С cleanup
export const delayedValue = derived(
  someStore,
  ($value, set) => {
    const timeout = setTimeout(() => set($value), 1000);
    return () => clearTimeout(timeout);
  }
);

Custom Stores

// stores/todos.ts
import { writable, derived } from 'svelte/store';
 
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}
 
function createTodoStore() {
  const { subscribe, set, update } = writable<Todo[]>([]);
 
  return {
    subscribe,
 
    add: (text: string) => {
      const todo: Todo = {
        id: Date.now(),
        text,
        completed: false
      };
      update(todos => [...todos, todo]);
    },
 
    toggle: (id: number) => {
      update(todos =>
        todos.map(todo =>
          todo.id === id
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      );
    },
 
    remove: (id: number) => {
      update(todos => todos.filter(todo => todo.id !== id));
    },
 
    clear: () => set([])
  };
}
 
export const todos = createTodoStore();
 
// Derived stores
export const completedCount = derived(
  todos,
  $todos => $todos.filter(t => t.completed).length
);
 
export const remainingCount = derived(
  todos,
  $todos => $todos.filter(t => !t.completed).length
);
<script>
  import { todos, completedCount, remainingCount } from './stores/todos';
 
  let newTodo = '';
 
  function addTodo() {
    if (newTodo.trim()) {
      todos.add(newTodo);
      newTodo = '';
    }
  }
</script>
 
<input bind:value={newTodo} on:keydown={(e) => e.key === 'Enter' && addTodo()} />
<button on:click={addTodo}>Add</button>
 
<ul>
  {#each $todos as todo (todo.id)}
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        on:change={() => todos.toggle(todo.id)}
      />
      <span class:completed={todo.completed}>{todo.text}</span>
      <button on:click={() => todos.remove(todo.id)}>×</button>
    </li>
  {/each}
</ul>
 
<p>Completed: {$completedCount} / Remaining: {$remainingCount}</p>
<button on:click={todos.clear}>Clear all</button>

Store в контексте

<!-- Parent.svelte -->
<script>
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';
 
  const theme = writable('light');
  setContext('theme', theme);
</script>
<!-- Child.svelte -->
<script>
  import { getContext } from 'svelte';
 
  const theme = getContext('theme');
</script>
 
<div class:dark={$theme === 'dark'}>
  Current theme: {$theme}
  <button on:click={() => $theme = $theme === 'light' ? 'dark' : 'light'}>
    Toggle
  </button>
</div>

Transitions и Animations

Встроенные transitions

<script>
  import { fade, fly, slide, scale, blur, draw } from 'svelte/transition';
  import { flip } from 'svelte/animate';
 
  let visible = true;
  let items = [1, 2, 3, 4, 5];
</script>
 
<!-- fade -->
{#if visible}
  <div transition:fade>Fades in/out</div>
{/if}
 
<!-- fade с параметрами -->
{#if visible}
  <div transition:fade={{ delay: 100, duration: 300 }}>
    Customized fade
  </div>
{/if}
 
<!-- fly -->
{#if visible}
  <div transition:fly={{ x: 200, y: 0, duration: 500 }}>
    Flies in from right
  </div>
{/if}
 
<!-- slide -->
{#if visible}
  <div transition:slide={{ duration: 300 }}>
    Slides down
  </div>
{/if}
 
<!-- scale -->
{#if visible}
  <div transition:scale={{ start: 0.5, opacity: 0.5 }}>
    Scales up
  </div>
{/if}
 
<!-- blur -->
{#if visible}
  <div transition:blur={{ amount: 10 }}>
    Blurs in/out
  </div>
{/if}
 
<!-- Разные transitions для in/out -->
{#if visible}
  <div in:fly={{ y: -200 }} out:fade>
    Different enter/exit
  </div>
{/if}
 
<!-- draw для SVG paths -->
<svg>
  {#if visible}
    <path
      transition:draw={{ duration: 1000 }}
      d="M0,0 L100,100"
    />
  {/if}
</svg>

Animate (flip)

<script>
  import { flip } from 'svelte/animate';
  import { fade } from 'svelte/transition';
 
  let items = [
    { id: 1, name: 'First' },
    { id: 2, name: 'Second' },
    { id: 3, name: 'Third' }
  ];
 
  function shuffle() {
    items = items.sort(() => Math.random() - 0.5);
  }
 
  function remove(id) {
    items = items.filter(item => item.id !== id);
  }
</script>
 
<button on:click={shuffle}>Shuffle</button>
 
<ul>
  {#each items as item (item.id)}
    <li
      animate:flip={{ duration: 300 }}
      in:fade
      out:fade
    >
      {item.name}
      <button on:click={() => remove(item.id)}>×</button>
    </li>
  {/each}
</ul>

Кастомные transitions

// transitions.ts
import type { TransitionConfig } from 'svelte/transition';
 
export function typewriter(node: Element, { speed = 1 }): TransitionConfig {
  const valid = node.childNodes.length === 1 &&
                node.childNodes[0].nodeType === Node.TEXT_NODE;
 
  if (!valid) {
    throw new Error('This transition only works on elements with a single text node child');
  }
 
  const text = node.textContent!;
  const duration = text.length / (speed * 0.01);
 
  return {
    duration,
    tick: (t) => {
      const i = Math.trunc(text.length * t);
      node.textContent = text.slice(0, i);
    }
  };
}
 
// Пример с CSS
export function spin(node: Element, { duration = 300 }): TransitionConfig {
  return {
    duration,
    css: (t) => {
      const eased = t; // можно применить easing
      return `
        transform: rotate(${eased * 360}deg) scale(${eased});
        opacity: ${eased};
      `;
    }
  };
}
<script>
  import { typewriter, spin } from './transitions';
 
  let visible = false;
</script>
 
<button on:click={() => visible = !visible}>Toggle</button>
 
{#if visible}
  <p transition:typewriter={{ speed: 2 }}>
    This text types itself out!
  </p>
{/if}
 
{#if visible}
  <div transition:spin>
    Spinning!
  </div>
{/if}

Easing функции

<script>
  import { fade } from 'svelte/transition';
  import { cubicOut, elasticOut, bounceOut } from 'svelte/easing';
 
  let visible = true;
</script>
 
{#if visible}
  <div transition:fade={{ duration: 500, easing: cubicOut }}>
    Cubic easing
  </div>
{/if}
 
{#if visible}
  <div transition:fade={{ duration: 800, easing: elasticOut }}>
    Elastic easing
  </div>
{/if}

Transition events

<script>
  let visible = true;
 
  function handleIntroStart(event) {
    console.log('Intro started');
  }
 
  function handleIntroEnd(event) {
    console.log('Intro ended');
  }
</script>
 
{#if visible}
  <div
    transition:fade
    on:introstart={handleIntroStart}
    on:introend={handleIntroEnd}
    on:outrostart={() => console.log('Outro started')}
    on:outroend={() => console.log('Outro ended')}
  >
    Content
  </div>
{/if}

Actions (Директивы)

Actions — это функции, вызываемые при монтировании элемента в DOM.

Создание action

// actions/clickOutside.ts
export function clickOutside(node: HTMLElement, callback: () => void) {
  function handleClick(event: MouseEvent) {
    if (!node.contains(event.target as Node)) {
      callback();
    }
  }
 
  document.addEventListener('click', handleClick, true);
 
  return {
    // Cleanup при удалении элемента
    destroy() {
      document.removeEventListener('click', handleClick, true);
    }
  };
}
<script>
  import { clickOutside } from './actions/clickOutside';
 
  let showDropdown = false;
</script>
 
{#if showDropdown}
  <div use:clickOutside={() => showDropdown = false}>
    Dropdown content
  </div>
{/if}

Action с параметрами

// actions/tooltip.ts
interface TooltipOptions {
  text: string;
  position?: 'top' | 'bottom' | 'left' | 'right';
}
 
export function tooltip(node: HTMLElement, options: TooltipOptions) {
  let tooltipElement: HTMLElement | null = null;
 
  function showTooltip() {
    tooltipElement = document.createElement('div');
    tooltipElement.className = `tooltip tooltip-${options.position || 'top'}`;
    tooltipElement.textContent = options.text;
    document.body.appendChild(tooltipElement);
 
    const rect = node.getBoundingClientRect();
    tooltipElement.style.left = `${rect.left + rect.width / 2}px`;
    tooltipElement.style.top = `${rect.top - 10}px`;
  }
 
  function hideTooltip() {
    if (tooltipElement) {
      tooltipElement.remove();
      tooltipElement = null;
    }
  }
 
  node.addEventListener('mouseenter', showTooltip);
  node.addEventListener('mouseleave', hideTooltip);
 
  return {
    // Вызывается при изменении параметров
    update(newOptions: TooltipOptions) {
      options = newOptions;
    },
 
    destroy() {
      hideTooltip();
      node.removeEventListener('mouseenter', showTooltip);
      node.removeEventListener('mouseleave', hideTooltip);
    }
  };
}
<script>
  import { tooltip } from './actions/tooltip';
 
  let tooltipText = 'Hello!';
</script>
 
<button use:tooltip={{ text: tooltipText, position: 'top' }}>
  Hover me
</button>
 
<input bind:value={tooltipText} placeholder="Change tooltip text" />

Полезные actions

// actions/longpress.ts
export function longpress(node: HTMLElement, duration = 500) {
  let timer: ReturnType<typeof setTimeout>;
 
  function handleMouseDown() {
    timer = setTimeout(() => {
      node.dispatchEvent(new CustomEvent('longpress'));
    }, duration);
  }
 
  function handleMouseUp() {
    clearTimeout(timer);
  }
 
  node.addEventListener('mousedown', handleMouseDown);
  node.addEventListener('mouseup', handleMouseUp);
 
  return {
    update(newDuration: number) {
      duration = newDuration;
    },
    destroy() {
      clearTimeout(timer);
      node.removeEventListener('mousedown', handleMouseDown);
      node.removeEventListener('mouseup', handleMouseUp);
    }
  };
}
 
// actions/intersection.ts
export function inview(node: HTMLElement, params = {}) {
  let observer: IntersectionObserver;
 
  const handleIntersect: IntersectionObserverCallback = (entries) => {
    const entry = entries[0];
    if (entry.isIntersecting) {
      node.dispatchEvent(new CustomEvent('enter'));
    } else {
      node.dispatchEvent(new CustomEvent('leave'));
    }
  };
 
  observer = new IntersectionObserver(handleIntersect, params);
  observer.observe(node);
 
  return {
    destroy() {
      observer.disconnect();
    }
  };
}
 
// actions/portal.ts
export function portal(node: HTMLElement, target: string | HTMLElement = 'body') {
  const targetEl = typeof target === 'string'
    ? document.querySelector(target)
    : target;
 
  if (targetEl) {
    targetEl.appendChild(node);
  }
 
  return {
    destroy() {
      node.remove();
    }
  };
}
<script>
  import { longpress, inview, portal } from './actions';
</script>
 
<button
  use:longpress={1000}
  on:longpress={() => alert('Long pressed!')}
>
  Long press me
</button>
 
<div
  use:inview
  on:enter={() => console.log('In view')}
  on:leave={() => console.log('Out of view')}
>
  Observed element
</div>
 
<!-- Рендерится в body -->
<div use:portal>
  <div class="modal">Modal content</div>
</div>

Context API

setContext и getContext

<!-- ThemeProvider.svelte -->
<script>
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';
 
  const theme = writable('light');
  const toggleTheme = () => {
    theme.update(t => t === 'light' ? 'dark' : 'light');
  };
 
  // Устанавливаем контекст для дочерних компонентов
  setContext('theme', {
    theme,
    toggleTheme
  });
</script>
 
<div class="theme-provider">
  <slot />
</div>
<!-- Child.svelte -->
<script>
  import { getContext } from 'svelte';
 
  // Получаем контекст
  const { theme, toggleTheme } = getContext('theme');
</script>
 
<div class:dark={$theme === 'dark'}>
  Current theme: {$theme}
  <button on:click={toggleTheme}>Toggle Theme</button>
</div>
<!-- App.svelte -->
<script>
  import ThemeProvider from './ThemeProvider.svelte';
  import Child from './Child.svelte';
</script>
 
<ThemeProvider>
  <Child />
  <Child />
</ThemeProvider>

hasContext

<script>
  import { hasContext, getContext } from 'svelte';
 
  // Проверка наличия контекста
  const hasTheme = hasContext('theme');
 
  let theme;
  if (hasTheme) {
    theme = getContext('theme');
  }
</script>

Типизация контекста

// context.ts
import { getContext, setContext } from 'svelte';
import type { Writable } from 'svelte/store';
 
interface ThemeContext {
  theme: Writable<'light' | 'dark'>;
  toggleTheme: () => void;
}
 
const THEME_KEY = Symbol('theme');
 
export function setThemeContext(context: ThemeContext) {
  setContext(THEME_KEY, context);
}
 
export function getThemeContext(): ThemeContext {
  return getContext(THEME_KEY);
}

SvelteKit

File-based Routing

src/routes/
├── +page.svelte          # /
├── +layout.svelte        # Layout для всех страниц
├── +error.svelte         # Страница ошибки
├── about/
│   └── +page.svelte      # /about
├── blog/
│   ├── +page.svelte      # /blog
│   ├── +page.ts          # Load функция для /blog
│   └── [slug]/
│       ├── +page.svelte  # /blog/[slug]
│       └── +page.ts      # Load функция
├── users/
│   ├── +page.svelte      # /users
│   └── [id]/
│       ├── +page.svelte  # /users/[id]
│       └── settings/
│           └── +page.svelte  # /users/[id]/settings
├── api/
│   └── posts/
│       └── +server.ts    # API endpoint: /api/posts
└── (auth)/               # Route group (не влияет на URL)
    ├── login/
    │   └── +page.svelte  # /login
    └── register/
        └── +page.svelte  # /register

Page компонент

<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';
 
  export let data: PageData;
</script>
 
<svelte:head>
  <title>{data.post.title}</title>
  <meta name="description" content={data.post.excerpt} />
</svelte:head>
 
<article>
  <h1>{data.post.title}</h1>
  <p class="date">{data.post.date}</p>
  <div class="content">
    {@html data.post.content}
  </div>
</article>

Load функции

// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
 
// Выполняется на сервере и клиенте (universal load)
export const load: PageLoad = async ({ params, fetch }) => {
  const response = await fetch(`/api/posts/${params.slug}`);
 
  if (!response.ok) {
    throw error(404, 'Post not found');
  }
 
  const post = await response.json();
 
  return {
    post
  };
};
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import { db } from '$lib/database';
 
// Выполняется ТОЛЬКО на сервере
export const load: PageServerLoad = async ({ params, locals }) => {
  const post = await db.posts.findUnique({
    where: { slug: params.slug }
  });
 
  if (!post) {
    throw error(404, 'Post not found');
  }
 
  // Можно работать с секретами, базой данных напрямую
  return {
    post,
    user: locals.user
  };
};

Layouts

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { page } from '$app/stores';
  import type { LayoutData } from './$types';
 
  export let data: LayoutData;
</script>
 
<nav>
  <a href="/" class:active={$page.url.pathname === '/'}>Home</a>
  <a href="/about" class:active={$page.url.pathname === '/about'}>About</a>
 
  {#if data.user}
    <span>Welcome, {data.user.name}</span>
    <a href="/logout">Logout</a>
  {:else}
    <a href="/login">Login</a>
  {/if}
</nav>
 
<main>
  <slot />
</main>
 
<footer>
  &copy; 2024 My App
</footer>
 
<style>
  nav a.active {
    font-weight: bold;
  }
</style>
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';
 
export const load: LayoutServerLoad = async ({ locals }) => {
  return {
    user: locals.user
  };
};

Form Actions

// src/routes/login/+page.server.ts
import type { Actions, PageServerLoad } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import { db } from '$lib/database';
 
export const load: PageServerLoad = async ({ locals }) => {
  if (locals.user) {
    throw redirect(303, '/dashboard');
  }
};
 
export const actions: Actions = {
  // Default action
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get('email') as string;
    const password = data.get('password') as string;
 
    // Валидация
    if (!email || !password) {
      return fail(400, {
        email,
        error: 'Email and password are required'
      });
    }
 
    // Аутентификация
    const user = await db.users.authenticate(email, password);
 
    if (!user) {
      return fail(401, {
        email,
        error: 'Invalid credentials'
      });
    }
 
    // Установка cookie
    cookies.set('session', user.sessionToken, {
      path: '/',
      httpOnly: true,
      sameSite: 'strict',
      maxAge: 60 * 60 * 24 * 7 // 1 week
    });
 
    throw redirect(303, '/dashboard');
  },
 
  // Named action
  register: async ({ request }) => {
    const data = await request.formData();
    // ... registration logic
  }
};
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { ActionData } from './$types';
 
  export let form: ActionData;
</script>
 
<h1>Login</h1>
 
{#if form?.error}
  <p class="error">{form.error}</p>
{/if}
 
<!-- use:enhance для progressive enhancement -->
<form method="POST" use:enhance>
  <label>
    Email
    <input
      type="email"
      name="email"
      value={form?.email ?? ''}
      required
    />
  </label>
 
  <label>
    Password
    <input type="password" name="password" required />
  </label>
 
  <button type="submit">Login</button>
</form>
 
<!-- Named action -->
<form method="POST" action="?/register">
  <!-- ... -->
</form>

API Routes

// src/routes/api/posts/+server.ts
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/database';
 
// GET /api/posts
export const GET: RequestHandler = async ({ url }) => {
  const limit = Number(url.searchParams.get('limit') ?? 10);
  const offset = Number(url.searchParams.get('offset') ?? 0);
 
  const posts = await db.posts.findMany({
    take: limit,
    skip: offset
  });
 
  return json(posts);
};
 
// POST /api/posts
export const POST: RequestHandler = async ({ request, locals }) => {
  if (!locals.user) {
    throw error(401, 'Unauthorized');
  }
 
  const data = await request.json();
 
  const post = await db.posts.create({
    data: {
      ...data,
      authorId: locals.user.id
    }
  });
 
  return json(post, { status: 201 });
};
// src/routes/api/posts/[id]/+server.ts
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/database';
 
// GET /api/posts/[id]
export const GET: RequestHandler = async ({ params }) => {
  const post = await db.posts.findUnique({
    where: { id: params.id }
  });
 
  if (!post) {
    throw error(404, 'Post not found');
  }
 
  return json(post);
};
 
// PUT /api/posts/[id]
export const PUT: RequestHandler = async ({ params, request, locals }) => {
  const post = await db.posts.findUnique({
    where: { id: params.id }
  });
 
  if (!post) {
    throw error(404, 'Post not found');
  }
 
  if (post.authorId !== locals.user?.id) {
    throw error(403, 'Forbidden');
  }
 
  const data = await request.json();
 
  const updated = await db.posts.update({
    where: { id: params.id },
    data
  });
 
  return json(updated);
};
 
// DELETE /api/posts/[id]
export const DELETE: RequestHandler = async ({ params, locals }) => {
  const post = await db.posts.findUnique({
    where: { id: params.id }
  });
 
  if (!post) {
    throw error(404, 'Post not found');
  }
 
  if (post.authorId !== locals.user?.id) {
    throw error(403, 'Forbidden');
  }
 
  await db.posts.delete({
    where: { id: params.id }
  });
 
  return new Response(null, { status: 204 });
};

Hooks

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { db } from '$lib/database';
 
// Выполняется для каждого запроса
export const handle: Handle = async ({ event, resolve }) => {
  // Получение сессии из cookie
  const sessionToken = event.cookies.get('session');
 
  if (sessionToken) {
    const user = await db.users.findBySession(sessionToken);
    if (user) {
      event.locals.user = user;
    }
  }
 
  // Продолжение обработки запроса
  const response = await resolve(event);
 
  // Можно модифицировать response
  return response;
};
 
// Обработка ошибок
export const handleError = async ({ error, event }) => {
  console.error('Server error:', error);
 
  return {
    message: 'Something went wrong',
    code: 'INTERNAL_ERROR'
  };
};
// src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit';
 
export const handleError: HandleClientError = async ({ error, event }) => {
  console.error('Client error:', error);
 
  return {
    message: 'Something went wrong',
    code: 'CLIENT_ERROR'
  };
};

Навигация

<script>
  import { goto, invalidate, invalidateAll } from '$app/navigation';
  import { page, navigating } from '$app/stores';
 
  async function handleClick() {
    // Программная навигация
    await goto('/dashboard');
 
    // С параметрами
    await goto('/search?q=svelte', { replaceState: true });
 
    // Без прокрутки
    await goto('/page', { noScroll: true });
  }
 
  async function refreshData() {
    // Перезагрузка данных текущей страницы
    await invalidateAll();
 
    // Или конкретного load
    await invalidate('app:posts');
  }
</script>
 
<!-- Статус навигации -->
{#if $navigating}
  <div class="loading-bar">Loading...</div>
{/if}
 
<!-- Текущий URL -->
<p>Current path: {$page.url.pathname}</p>
<p>Query: {$page.url.searchParams.get('q')}</p>
 
<!-- Ссылки -->
<a href="/about">About</a>
<a href="/blog/{post.slug}">Read more</a>
 
<!-- Preloading -->
<a href="/heavy-page" data-sveltekit-preload-data="hover">
  Preloads on hover
</a>

SSR и CSR режимы

// src/routes/+page.ts
// SSR выключен для этой страницы
export const ssr = false;
 
// CSR выключен (только SSR)
export const csr = false;
 
// Prerender
export const prerender = true;
 
// Trailing slash
export const trailingSlash = 'always';
// src/routes/+layout.ts
// Применяется ко всем дочерним страницам
export const ssr = true;
export const prerender = false;

Environment Variables

# .env
PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgres://...
SECRET_KEY=supersecret
// Публичные (доступны на клиенте)
import { PUBLIC_API_URL } from '$env/static/public';
 
// Приватные (только сервер)
import { DATABASE_URL, SECRET_KEY } from '$env/static/private';
 
// Динамические
import { env } from '$env/dynamic/private';
import { env as publicEnv } from '$env/dynamic/public';

Best Practices

1. Структура проекта

src/
├── lib/
│   ├── components/
│   │   ├── ui/           # Базовые UI компоненты
│   │   │   ├── Button.svelte
│   │   │   ├── Input.svelte
│   │   │   └── Modal.svelte
│   │   └── features/     # Feature компоненты
│   │       ├── UserCard.svelte
│   │       └── PostList.svelte
│   ├── stores/           # Svelte stores
│   │   ├── auth.ts
│   │   └── theme.ts
│   ├── utils/            # Утилиты
│   │   ├── format.ts
│   │   └── validation.ts
│   ├── types/            # TypeScript типы
│   │   └── index.ts
│   ├── actions/          # Svelte actions
│   │   └── clickOutside.ts
│   └── server/           # Серверный код
│       └── database.ts
├── routes/
└── app.html

2. Компоненты

<!-- ✅ Хорошо: Props с дефолтами и типами -->
<script lang="ts">
  export let variant: 'primary' | 'secondary' = 'primary';
  export let disabled: boolean = false;
  export let onClick: () => void = () => {};
</script>
 
<!-- ✅ Хорошо: Деструктуризация $$restProps -->
<button
  class="btn btn-{variant}"
  {disabled}
  on:click={onClick}
  {...$$restProps}
>
  <slot />
</button>

3. Реактивность

<script>
  // ✅ Хорошо: Использование $: для computed значений
  $: fullName = `${firstName} ${lastName}`;
 
  // ✅ Хорошо: Использование $: для side effects
  $: if (count > 10) {
    console.log('Count exceeded 10');
  }
 
  // ❌ Плохо: Мутация без присваивания
  // items.push(newItem);
 
  // ✅ Хорошо: Новый массив
  items = [...items, newItem];
</script>

4. Stores

// ✅ Хорошо: Кастомный store с методами
function createTodoStore() {
  const { subscribe, set, update } = writable<Todo[]>([]);
 
  return {
    subscribe,
    add: (text: string) => update(todos => [...todos, createTodo(text)]),
    remove: (id: number) => update(todos => todos.filter(t => t.id !== id)),
    clear: () => set([])
  };
}
 
// ❌ Плохо: Экспорт writable напрямую без инкапсуляции логики
export const todos = writable<Todo[]>([]);

5. Типизация

// types.ts
export interface User {
  id: number;
  name: string;
  email: string;
}
 
// +page.ts
import type { PageLoad } from './$types';
 
export const load: PageLoad = async ({ fetch }) => {
  const users: User[] = await fetch('/api/users').then(r => r.json());
  return { users };
};

Полезные ресурсы