Крупный гайд по Svelte

Svelte — веб-фреймворк, который отличается принципом работы от React и Vue.

React и Vue формируют приложение прямо в браузере, когда пользователь открывает необходимый ему ресурс, Svelte же заранее компилирует исходный код и предоставляет часть приложения статичной версткой, а затем гидрирует приложение, благодаря чему приложение получается быстрым, простым в отладке и надежным😌

У Svelte нет Virtual DOM, то есть он напрямую работает с DOM в браузере и изменяет его по мере необходимости.

Установка

Использовать сам фреймворк можно с помощью нескольких способов:

  • С помощью Vite
  • С помощью npm create svelte@latest

Синтаксис

Каждый компонент в Svelte — SFC (Single File Component). Как и во Vue или React внутри SFC можно разместить:

  • Стили
  • Верстку
  • Логику
<script> // Здесь идет логика (JS) </script> <!-- Здесь идет размeтка (HTML) --> <style> /* Здесь идут стили (CSS) */ </style>

Все что находится внутри <script> выполняется во время создания компонента.

Внутри <script> также есть дополнительные правила, которые будет рассмотрены ниже.

Важно помнить что Svelte — фреймворк, который компилирует исходный код, а значит у Svelte может быть дополнительные значения у привычных для нас вещей (напр. export, который описан ниже)

Состояния

Состояния внутри Svelte объявляются с помощью let и const. Все верно, каждая переменная объявленная в глобальном скоупе является состоянием🫡

На случай, если это ваш первый реактивный фреймворк:
Состояния нужны для обновления интерфейса. Как только состояние обновляется — обновляется и интерфейс приложения, который показывает актуальные данные.

<script> // Новое состояние name let name = 'Daniil'; // Новое неизменяемое состояние const age = 20; </script>

Шаблоны

Шаблон компонента можно писать прямо в файле .svelte:

<script> </script> <div> <!-- Здесь находится какая-то верстка --> </div>

Интерполяция

Для того чтобы использовать состояния внутри шаблона нужно укзаать на имя состояния в фигурных скобках:

<script> // Состояние, которое хранит имя const name = 'Mike'; </script> <h1>Hello, { name }</h1> <!-- Выведется: Hello, Mike -->

Мы также можем использовать методы JS, для того чтобы взаимодействовать с состоянием прямо внутри скобок для интерполяции:

<script> const age = 20; </script> <div> <h1>Пользователь старше 18 лет?</h1> <span>{ age > 18 }</span> <!-- true --> </div>

Также мы можем вставлять HTML с помощью модификатора @html:

<script> const htmlCode = 'Hello, <strong>world!</strong>'; </script> <span>{@html htmlCode}</span>

Атрибуты

Интерполяция работает с атрибутами точно также, как и с текстом внутри элемента:

<script> const imageSource = '/img/1.jpg'; </script> <img src={src} alt="Default image alt"> <!-- или --> <img {src} alt="Default image alt">

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

В HTML нет условий, в Svelte они есть. Использовать условный рендеринг можно следующим образом:

<script> const hasButton = false; </script> <div> {#if hasButton} <!-- Если переменная hasButton равна false, то данный кусок шаблона не отобразится --> <button> Log in </button> {:else} <!-- Вместо первого шаблона - отобразится данный шаблон --> <div>Тут нет кнопки :(</div> {/if} </div>

Также мы можем использовать {:else if <условие>}, для того чтобы задать дополнительный логический блок. Прямо как `else if` в JS👀

Цикличный рендеринг

Также как и с условным рендерингом — в Svelte есть цикличный рендеринг. Он позволяет проходится по спискам и для каждого элемента рендерить одну и ту же верстку:

<script> const names = ['Alex', 'Alexo', 'Nina', 'Jeff']; </script> <div> Список имен: <ul> <!-- Читается как: для каждого элемента names, который именуется как name --> {#each names as name} <li> { name } </li> {/each} </ul> </div>

Данный кусок кода выведет следующий список в HTML:

<div> Список имен: <ul> <li>Alex</li> <li>Alexo</li> <li>Nina</li> <li>Jeff</li> </ul> </div>

Изменяемый список

В случае если список изменяется — нужно дать каждом элементу id, для того чтобы Svelte лучше управлялся с такими элементами и рендерил все правильно:

<script> const names = ['Alex', 'Alexo', 'Nina', 'Jeff']; </script> <div> Список имен: <ul> <!-- В качестве id может быть строка, число или объект --> <!-- ID указывается в круглых скобках сразу после названия элемента --> <!-- В данном случае мы как ID передали индекс в массиве--> {#each names as name, index (index)} <li> { name } </li> {/each} </ul> </div>

Асинхронные блоки

Svelte позволяет нам отрисовывать разный шаблон по мере жизнедеятельности промиса:

<script> /** * Получает случайного персонажа из Rick And Morty */ async function getCharacter() { const res = await fetch(`https://rickandmortyapi.com/api/character/${Math.floor(Math.random() * 600)}`); const text = await res.text(); if (res.ok) { return text; } else { throw new Error(text); } } // Сразу получаем персонажа при отрисовке let promise = getCharacter(); /** * Метод, при вызове которого подгружается новый случайный персонаж */ function handleClick() { promise = getCharacter(); } </script> <!-- При клике на кнопку вызывается метод handleClick --> <button on:click={handleClick}> Get random character </button> <!-- Асинхронный блок --> {#await promise} <p>Данные запрашиваются</p> {:then data} <code> {data} </code> {:catch error} <p> Данные не получены :( </p> {/await}

Обратите внимание, что:

  • # в начале названия блока используется для объвления блока
  • : в начале названия блока используется для того чтобы продлить блок
  • / в начале названия блока используется для того чтобы закрыть блок

Импортирование компонентов

Можно импортировать компоненты в другие компоненты:

<script> import Button from './Button.svelte'; </script> <Button />

Пропсы

Пропсы — данные, которые компонент берет из родительского компонента. Пропсы передаются как атрибут при использовании компонента 🙌

С помощью ключевого слова export мы можем объявлять пропсы (по сути просто экспортируя переменную):

<!-- Hello.svelte --> <script> export let name = 'John'; export let age = 0; </script> <div> Hello, { name } Your age is { age } </div>

Когда мы будем использовать компонент Hello. svelte — мы будем передавать имя через name=«значение». Если мы не передадим значение, то будет использоваться значение, с которым переменная name инициализировалась (John).

<!-- App.svelte --> <script> import Hello from './Hello.svelte'; </script> <div> <!-- Выведется "Hello, Daniil Your age is 20" --> <Hello name="Daniil" age=20 /> <!-- Выведется "Hello, John Your age is 0", ибо мы не передали пропс --> <Hello /> </div>

Важно
Нужно обязательно инициализировать пропс с дефолтным значением (даже если это undefined). Если мы просто напишем следующее, то Svelte будет жаловаться:

<script> export let foo; </script>

Если мы хотим передать сразу несколько пропсов, которые находятся внутри объекта и имена свойств совпадают с именами пропсов, то мы можем использовать spread-оператор:

<!-- App.svelte --> <script> import Hello from './Hello.svelte'; const person = { name: 'Daniil', age: 20 }; </script> <div> <Hello {...person} /> </div>

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

Все переменные которые объявлены внутри <script> по умолчанию являются реактивными🚀

То есть, если мы инициализируем переменную для счетчика, которая будет отображать число кликов и с помощью метода прибавим к ней 1, то значение сразу же отрисуется в верстке:

<script> let counter = 0; function increment() { counter += 1; } </script> <div on:click={increment}> { counter } </div>

Если вас смущает on:click={}, то не стоит сейчас об этом думать, мы рассмотрим привязку ивентов чуть позже. Если вкратце, то тут мы привязываем на клик функцию increment👇

Реактивность в Svelte основана на присвоении переменной значения. Это значит что при использовании методов, которые так или иначе меняют значения структуры (напр. массива) Svelte не будет триггерить ререндер (обновления интерфейса)🤓

<script> let foodList = []; // Данный метод __не__ вызовет перерисовку function lostRender() { foodList.push('Milk'); } // Данный метод вызовет перерисовку function niceRender() { foodList = [...foodList, '']; }

В примере вверху функция lostRender не затриггерит перерисовку, так как мы используем встроенный метод для того чтобы обновить массив.

Функция niceRender перерисует значения на странице, так как мы присваиваем новое значение переменой foodList🍗

Обновление значений

Блок <script> внутри компонента выполняется полностью только однажды (при создании компонента). Это значит что после того как компонент примонтируется, внутренние состояния, которые зависят от пропсов не будут обновляться:

<script> export let name = ''; let uppercaseName = name.toUpperCase(); </script>

Если мы используем компонент и в процессе взаимодействия с интерфейсом пользователь изменит имя, которое передается в пропс компонента вверху, то переменная uppercaseName не обновится (хоть name и обновится), так как весь код был выполнен при создании компонента.

Прослушиватели (watchers)

Прослушиватели в Svelte сделаны с помощью лэйблов в JS. Для реактивного блока создан специальный лэйбл $.

После $ могут идти фигурные скобки или название переменной, в зависимости от которых будет меняться контекст (см. далее).

Наблюдатель

В случае с фигурными скобками — блок будет выполняться каждый раз, когда задействованные переменные в нем обновляются:

<script> let counter = 0; function incrementCounter() { counter += 1; } $: { // Будет выполняться каждый раз, как переменная counter будет обновляться console.log('Счетчик поменялся!', counter); } </script> <h1 on:click={incrementCounter}>Счетчик: { counter }</h1>

Вычисляемое значение

В случае если после $ будет идти присвоение — то новая переменная будет обновлять свое значение каждый раз, как реактивная переменная которая учавствует в присвоении была обновлена.

В «Обновлении значений» мы говорили о том, что при инициализации переменной с помощью пропса — она не будет меняться, даже если значение пропса изменилось. Это можно исправить с помощью вычисляемого значения. Сделать так, чтобы переменная пересчитывалась каждый раз, как меняется пропс можно следующим образом:

<script> export let name = ''; $: uppercaseName = name.toUpperCase(); </script>

Нужно обратить внимание, что перед uppercaseName не используется let 🤓

Ивенты и директивы

Мы уже видели как используется on:click 👀

on используется для того чтобы определить какой-то ивент. Например:

  • `on:click` — сработает при клике
  • `on:keydown` — сработает при нажатии кнопки
  • `on:drag` — сработает при переносе чего-либо
  • `on:mouseenter` — сработает, когда мышь окажется внутри какого-то элемента

Мы можем использовать в качестве обработчиков событий функции внутри компонента или писать стрелочные функции прямо внутри скобок:

<script> let m = { x: 0, y: 0 }; </script> <div on:mousemove={ e => m = { x: e.clientX, y: e.clientY }}> The mouse position is {m.x} x {m.y} </div> <style> div { width: 100%; height: 100%; } </style>

Модификаторы ивентов

Мы можем использовать следующие модификаторы:

  • preventDefault — вызывает event. preventDefault() перед тем как выполнить хэндлер.
  • stopPropagation — вызывает event. stopPropagation(), чтобы предотвратить всплытие.
  • passive — улучшает производительность при скролле (Svelte сам будет добавлять данный модификатор где это уместно).
  • nonpassive — отключает модификатор passive.
  • capture — вызывает хэндлер во время фазы захвата, а не всплытия
  • once — хэндлер сработает только один раз
  • self — тригерит ивент, если пользователь взаимодействовал только с элементом, на котором висит данный ивент (избегание всплытия)
  • trusted — тригерит ивент, если event. isTrusted === true

Модификаторы указываются следующим образом:

<script> function handleClick() { alert('clicked') } </script> <button on:click|once={handleClick}> Click me </button>

Кастомные ивенты

Иногда нам нужны кастомные ивенты, которые бы тригерили функции извне компонента. В Vue для этого есть emit, в Svelte — eventDispatcher 💁🏻‍♂

Допустим, что у родительского компонента нет доступа к состояниям дочернего компонента. Но, мы все же хотим получить оттуда какие-либо данные. Для этого мы создаем коллбэк, который будет с этими данными взаимодействовать (допустим выводить их с помощью alert):

<!-- App.svelte --> <script> import Subcomponent from './Subcomponent.svelte' function handleMessage(message) { alert(message); } </script> <Subcomponent on:message={handleMessage} />

В дочернем компоненте мы при нажатии на кнопку будем посылать сообщение в родительский компонент:

<!-- Subcomponent.svelte --> <script> // Импортируем специальный метод, который создает dispatcher // с помощью которого мы будем посылать наш ивент import {createEventDispatcher} from 'svelte'; const dispatch = createEventDispatcher(); function handleClick() { const message = 'Hello, World!'; dispatch('message', {message}); } </script> <button on:click={handleClick}>Click me!</button>

Теперь при клике на кнопку мы создадим специальный ивент, который вызовет коллбэк handleMessage из родительского компонента и передаст ему все данные, которые нам нужны 🙌

> К слову мы могли назвать ивент как хотим. Важно, чтобы первый аргумент в dispatch() и слово после on: совпадали.

Всплытие

Кастомные ивенты не всплывают, но если нам нужно, чтобы они всплывали, то у каждого дочернего компонента в иерархии нужно указывать `on:message`. Это довольно редкий кейс, однако подробнее можно узнать тут.

Биндинги

Биндинги существуют для того чтобы облегчать двухстороннее связывание 🥂

Двухстороннее связывание — техника, которую в основном применяют к инпутам. Она связывает value и событие oninput, таким образом чтобы пользовательский ввод все время обновлял value.

В Svelte это выглядит следующим образом:

<script> let inputValue = 'Write here!'; function handleInput(ev) { inputValue = ev.target.value; } </script> <input value={inputValue} on:input={handleInput} /> <h1>{ inputValue }</h1>

Однако, все данные действия можно сделать намного проще с помощью инпутов:

<script> let inputValue = 'Write here!'; </script> <!-- Связываем inputValue с вводимым значением с помощью bind --> <input bind:value={inputValue} /> <h1>{ inputValue }</h1>

Хуки

Хуки — это методы, которые цепляются за жизненный цикл компонента🪝

В Svelte существует следующие хуки:

  • onMount — хук, который выполняется как только компонент примонтировался к DOM.
  • beforeUpdate — хук, который работает перед тем как в компонент придут новые данные (например из пропсов)
  • afterUpdate — хук, который работает после того, как в компонент пришли новые данные
  • onDestroy — хук, который работает когда компонент размонтируется (удаляется из DOM)
<script> onMount(() => { console.log('Компонент только что примонтировался') }); </script>

Обычно хук onMount используют для того, чтобы подтянуть какие-то данные со сторонних сервисов с помощью fetch и использовать их внутри компонента.

Если использовать SvelteKit (для рендеринга на стороне сервера) и расположить fetch внутри onMount, то данные будут запрашиваться на стороне клиента, а если просто внутри <script>, то запрос отправится еще на сервере.

Тик

Все фреймворки для создания веб-приложений работают следующим образом:

1. Сбор операций которые нужно выполнить пачкой

2. Подбор времени для выполнения

3. Оптимизация собраного стека

4. Выполнение этих операций (тик)

В Svelte тоже есть тики. Например, если мы используем специальный метод tick внутри хука beforeUpdate, то после выполнения мы уже будем находиться на ивенте afterUpdate:

<script> import {beforeUpdate, tick} from 'svelte'; beforeUpdate(async() => { console.log('Данное сообщение выводится до того как компонент обновится'); await tick(); console.log('Данное сообщение выводится после того как компонент обновится'); }) </script>

Сторы

Как уже понятно — мы можем объявлять состояния внутри компонентов. Однако бывают ситуации, когда нам нужно использовать глобальные состояния, при обновлении которых компоненты также будут обновляться (перерендериваться). Для этого сторы и придумали.

Самый простой стор, который можно придумать — счетчик, однако в сторы можно запихнуть и сложных структуры данных, тут стор используется для примера.

Допустим что у нас есть два разных компонента:

  • В одном из них мы управляем нашим счетчиком
  • В другом мы используем значения из этого счетчика

Мы можем объявлять счетчики вне компонентов в отдельных файлах JS/TS:

import {writable} from 'svelte'; export const counter = writable(0);

В данном компоненте мы будем управлять счетчиком:

<!-- ControlCounter.svelte --> <script> import { counter } from './counter.js' /** * Инкрементирует значение в сторе */ function increment() { counter.update(n => n + 1); } /** * Декрементирует значение в сторе */ function decrement() { counter.update(n => n - 1); } </script> <button on:click={increment}>Увеличить</button> <button on:click={decrement}>Уменьшить</button>

В данном компоненте мы будем отрисовывать счетчик:

<script> import { counter } from './counter.js'; let count; // Подписываемся на обновления в сторе counter.subscribe(value => { count = value; }); </script> <h1>Счетчик: { count }</h1>

Если мы будем подписываться на счетчик, то нам нужно будет отписаться от него с помощью unsubscribe, когда компонент размонтируется.

Чтобы не делать этого мы можем просто использовать стор добавив к его названию $$counter.

Заключение

Мы обозрели все ключевые темы Svelte, научились создавать компоненты, управлять ими с помощью пропсов, состояний, наблюдателей. Научились создавать сторы, а также протягивать ивенты к инпутам.

Если вам было интересно читать данную статью, то можете заглянуть ко мне телеграм, там собрано много всего интересного для веб-разработчиков.

Еще обязательно увидимся в следующих гайдах и туториалах ❤

1717
31 комментарий

Выглядит как 💩
Какой-то убогий искусственно выдуманный синкаксис, странная структура документов, переменные, странные методы, которые откуда-то появляются - видимо тоже объявлены в глобальной области видимости.
Это точно кому-то надо?
Пишут что виртуальный дом - это плохо. Чем плохо? Медленно работает или тем что некоторые криворукие разрабы не включают голову?
Хоть какие-то есть внятные доводы использовать этот велосипед вместо реакта?

6

В статье же нигде не сказано: “Юзайте Svelte вместо React”👀

Фреймворк выделяется тем, что это немного другой подход к разработке приложений.

Svelte компилирует исходники и отдает часть уже готовой статики. VDOM ему не нужен не потому что “Virtual DOM плохой”, а потому что весь код подготавливается и проверяется на стадии компиляции)

Про синтаксис можно холиварить сколько угодно, это ведь индивидуально))

8

Выглядит пиздато, реакт заебал. Очень похож на нечто среднее между php шаблонизаторами и vue

1

И это только на обычном счётчике, представь что будет на более сложном проекте, хотя бы обычном блоге, страшно представить 😄

1

виртуальный дом это дико хорошо, люди просто не помнят времена когда сложные интерфейсы пытались сделать на прямых библиотеках.

Только новый apple music на нем написан

JSX вот что действительно выглядит как 💩