Svelte — сравнительно новый UI фреймворк, разработанный Ричем Харрисом, который также является автором сборщика Rollup. Скорее всего Svelte покажется совершенно не похожим на то, с чем вы имели дело до этого, но, пожалуй, это даже хорошо. Две самые впечатляющие особенности этого фреймворка — скорость и простота. В этой статье мы сосредоточимся на второй.
Поскольку мой основной опыт разработки связан с Angular, вполне естественно, что я пытаюсь изучить Svelte, копируя уже привычные мне подходы. И именно об этом будет рассказано в этой статье: как в Svelte делать те же самые вещи, что и в Angular.
Примечание: Не смотря на то, что в ряде случаев я буду высказывать своё предпочтение, статья не является сравнением фреймворков. Это простое и быстрое введение в Svelte для людей, которые уже используют Angular в качестве своего основного фреймворка.
Внимание спойлер: Svelte — это весело.
Компоненты
В Svelte каждый компонент соотносится с файлом, где он написан. Например, компонент Button
будет создан путем присвоения имени файлу Button.svelte
. Конечно, мы обычно делаем то же самое в Angular, но у нас это просто соглашение. (В Svelte имя импортируемого компонента также может не совпадать с именем файла — примечание переводчика)
Компоненты Svelte однофайловые, и состоят из 3 разделов: script
, style
и шаблон, который не нужно оборачивать ни в какой специальный тег.
Давайте создадим очень простой компонент, который показывает "Hello World".
Импортирование компонентов
В целом это похоже на импортирование JS-файла, но с парой оговорок:
- необходимо явно указывать расширение файла компонента
.svelte
- компоненты импортируются внутри тега
<script>
<script>
import Todo from './Todo.svelte';
</script>
<Todo></Todo>
Из приведенных выше фрагментов очевидно, что количество строк для создания компонента в Svelte невероятно мало. Конечно, присутствуют некоторые неявности и ограничения, но при этом всё достаточно просто, чтобы быстро к этому привыкнуть.
Базовый синтаксис
Интерполяции
Интерполяции в Svelte больше схожи с таковыми в React, нежели в Vue или Angular:
<script>
let someFunction = () => {...}
</script>
<span>{ 3 + 5 }</span>
<span>{ someFunction() }</span>
<span>{ someFunction() ? 0 : 1 }</span>
Я привык использовать двойные фигурные скобки, так что иногда опечатываюсь, но, возможно, такая проблема только у меня.
Атрибуты
Передать атрибуты в компоненты также довольно просто. Кавычки не обязательны и можно использовать любые Javascript-выражения:
//Svelte
<script>
let isFormValid = true;
</script>
<button disabled={!isFormValid}>Отправить</button>
События
Синтаксис обработчиков событий выглядит так: on:событие={обработчик}
.
<script>
const onChange = (e) => console.log(e);
</script>
<input on:input={onChange} />
В отличие от Angular, нам не нужно использовать скобки после имени функции, чтобы вызывать её. Если нужно передать аргументы в обработчик, просто используем анонимную функцию:
<input on:input={(e) => onChange(e, ‘a’)} />
Мой взгляд на читабельность такого кода:
- Печатать приходится меньше, поскольку нам не нужны кавычки и скобки — это в любом случае хорошо.
- Читать сложнее. Мне всегда больше нравился подход Angular, а не React, поэтому для меня и Svelte здесь воспринимается тяжелее. Но это просто моя привычка и мое мнение несколько предвзято.
Структурные директивы
В отличие от структурных директив в Vue и Angular, Svelte предлагает специальный синтаксис для циклов и ветвлений внутри шаблонов:
{#if todos.length === 0}
Список дел пуст
{:else}
{#each todos as todo}
<Todo {todo} />
{/each}
{/if}
Мне очень нравится. Нет необходимости в дополнительных HTML элементах, и с точки зрения читаемости это выглядит потрясающе. К сожалению, символ #
в британской раскладке клавиатуры моего Macbook находится в труднодоступном месте, и это негативно сказывается на моем опыте работы с этими структурами.
Входные свойства
Обозначить свойства, которые можно передать компоненту (аналог @Input
в Angular) так же легко, как экспортировать переменную из JS модуля при помощи ключевого слова export
. Пожалуй, поначалу это может сбивать с толку — но давайте напишем пример и посмотрим, насколько это действительно просто:
<script>
export let todo = { name: '', done: false };
</script>
<p>
{ todo.name } { todo.done ? '✓' : '✕' }
</p>
- Как вы могли заметить, мы инициализировали свойство
todo
вместе со значением: оно будет являться значением свойства по умолчанию, в случае если оно не будет передано из родительского компонента
Теперь создадим контейнер для этого компонента, который будет передавать ему данные:
<script>
import Todo from './Todo.svelte';
const todos = [{
name: "Изучить Svelte",
done: false
},
{
name: "Изучить Vue",
done: false
}];
</script>
{#each todos as todo}
<Todo todo={todo}></Todo>
{/each}
Аналогично полям в обычном JS-объекте, todo={todo}
можно сократить и переписать код следующим образом:
<Todo {todo}></Todo>
Сначала мне казалось это странным, но теперь я думаю, что это гениально.
Выходные свойства
Для реализации поведения директивы @Output
, например, получения родительским компонентом каких-либо уведомлений от дочернего, мы будем использовать функцию createEventDispatcher
, которая имеется в Svelte.
- Импортируем функцию
createEventDispatcher
и присваиваем её возвращаемое значение переменнойdispatch
- Функция
dispatch
имеет два параметра: имя события и данные(которые попадут в полеdetail
объекта события) - Помещаем
dispatch
внутри функцииmarkDone
, которая вызывается по событию клика (on:click
)
<script>
import { createEventDispatcher } from 'svelte';
export let todo;
const dispatch = createEventDispatcher();
function markDone() {
dispatch('done', todo.name);
}
</script>
<p>
{ todo.name } { todo.done ? '✓' : '✕' }
<button on:click={markDone}>Выполнено</button>
</p>
В родительском компоненте нужно создать обработчик для события done
, чтобы можно было отметить нужные объекты в массиве todo
.
- Создаём функцию
onDone
- Присваиваем эту функцию обработчику события, которое вызывается в дочернем компоненте, таким образом:
on:done={onDone}
<script>
import Todo from './Todo.svelte';
let todos = [{
name: "Изучить Svelte",
done: false
},
{
name: "Изучить Vue",
done: false
}];
function onDone(event) {
const name = event.detail;
todos = todos.map((todo) => {
return todo.name === name ? {...todo, done: true} : todo;
});
}
</script>
{#each todos as todo}
<Todo {todo} on:done={onDone}></Todo>
{/each}
Примечание: для запуска обнаружения изменения объекта, мы не мутируем сам объект. Вместо этого мы присваиваем переменной todos
новый массив, где объект нужной задачи уже будет изменен на выполненный.
Поэтому Svelte и считается по-настоящему реактивным: при обычном присваивании значения переменной изменится и соответсвующая часть представления.
ngModel
В Svelte есть специальный синтаксис bind:<атрибут>={переменная}
для привязки определенных переменных к атрибутам компонента и их синхронизации между собой.
Иначе говоря, он позволяет организовать двухстороннюю привязку данных:
<script>
let name = "";
let description = "";
function submit(e) { // отправка данных формы }
</script>
<form on:submit={submit}>
<div>
<input placeholder="Название" bind:value={name} />
</div>
<div>
<input placeholder="Описание" bind:value={description} />
</div>
<button>Добавить задачу</button>
</form>
Реактивные выражения
Как мы уже видели ранее, Svelte реагирует на присваивание значений переменным и перерисовывает представление. Также можно использовать реактивные выражения, чтобы реагировать на изменение значения одной переменной и обновлять значение другой.
Например, давайте создадим переменную, которая должна показывать нам, что в массиве todos
все задачи отмечены как выполненные:
let allDone = todos.every(({ done }) => done);
Однако, представление не будет перерисовываться при обновлении массива, потому что значение переменной allDone
присваивается лишь единожды. Воспользуемся реактивным выражением, которое заодно напомнит нам о существовании "меток" в Javascript:
$: allDone = todos.every(({ done }) => done);
Выглядит весьма экзотично. Если вам покажется, что тут "слишком много магии", напомню, что метки — это валидный Javascript.
Небольшое демо, поясняющее вышесказанное:
Внедрение содержимого
Для внедрения содержимого тоже применяются слоты, которые помещаются в нужное место внутри компонента.
Для простого отображения контента, который был передан внутри элемента компонента, используется специальный элемент slot
:
// Button.svelte
<script>
export let type;
</script>
<button class.type={type}>
<slot></slot>
</button>
// App.svelte
<script>
import Button from './Button.svelte';
</script>
<Button>
Отправить
</Button>
В этом случае строка "Отправить"
займет место элемента <slot></slot>
.
Именованным слотам потребуется присвоить имена:
// Modal.svelte
<div class='modal'>
<div class="modal-header">
<slot name="header"></slot>
</div>
<div class="modal-body">
<slot name="body"></slot>
</div>
</div>
// App.svelte
<script>
import Modal from './Modal.svelte';
</script>
<Modal>
<div slot="header">
Заголовок
</div>
<div slot="body">
Сообщение
</div>
</Modal>
Хуки жизненного цикла
Svelte предлагает 4 хука жизненного цикла, которые импортируются из пакета svelte
.
- onMount — вызывается при монтировании компонента в DOM
- beforeUpdate — вызывается перед обновлением компонента
- afterUpdate — вызывается после обновления компонента
- onDestroy — вызывается при удалении компонента из DOM
Функция onMount
принимает в качестве параметра callback-функцию, которая будет вызвана, когда компонент будет помещен в DOM. Проще говоря, она аналогична действию хука ngOnInit
.
Если callback-функция возвращает другую функцию, то она будет вызвана при удалении компонента из DOM.
<script>
import {
onMount,
beforeUpdate,
afterUpdate,
onDestroy
} from 'svelte';
onMount(() => console.log('Смонтирован', todo));
afterUpdate(() => console.log('Обновлён', todo));
beforeUpdate(() => console.log('Сейчас будет обновлён', todo));
onDestroy(() => console.log('Уничтожен', todo));
</script>
Важно помнить, что при вызове onMount
все входящие в него свойства уже должны быть инициализированы. То есть в фрагменте выше todo
уже должно существовать.
Управление состоянием
Управлять состоянием в Svelte невероятно просто, и, пожалуй, эта часть фреймворка мне симпатизирует больше остальных. Про многословность кода при использовании Redux можно забыть. Для примера, создадим хранилище в нашем приложении для хранения и управления задачами.
Записываемые хранилища
Сначала нужно импортировать объект хранилища writable
из пакета svelte/store
и сообщить ему начальное значение initialState
import { writable } from 'svelte/store';
const initialState = [{
name: "Изучить Svelte",
done: false
},
{
name: "Изучить Vue",
done: false
}];
const todos = writable(initialState);
Обычно, я помещаю подобный код в отдельный файл вроде todos.store.js
и экспортирую из него переменную хранилища, чтобы компонент, куда я его импортирую мог работать с ним.
Очевидно, что теперь объект todos
стал хранилищем и более не является массивом. Для получения значения хранилища воспользуемся небольшой магией в Svelte:
- Добавлением символа
$
к имени переменной хранилища мы получаем прямой доступ к его значению!
Таким образом, просто заменим в коде все упоминания переменной todos
на $todos
:
{#each $todos as todo}
<Todo todo={todo} on:done={onDone}></Todo>
{/each}
Установка состояния
Новое значение записываемого хранилища может быть указано вызовом метода set
, которое императивно изменяет состояние согласно переданному значению:
const todos = writable(initialState);
function removeAll() {
todos.set([]);
}
Обновление состояния
Для обновления хранилища (в нашем случае todos
), основываясь на его текущем состоянии, нужно вызвать метод update
и передать ему callback-функцию, которая будет возвращать новое состояние для хранилища.
Перепишем функцию onDone
, которую мы создали ранее:
function onDone(event) {
const name = event.detail;
todos.update((state) => {
return state.map((todo) => {
return todo.name === name ? {...todo, done: true} : todo;
});
});
}
Здесь я использовал редьюсер прямо в компоненте, что является плохой практикой. Переместим его в файл с нашим хранилищем и экспортируем из него функцию, которая просто будет обновлять состояние.
// todos.store.js
export function markTodoAsDone(name) {
const updateFn = (state) => {
return state.map((todo) => {
return todo.name === name ? {...todo, done: true} : todo;
});
});
todos.update(updateFn);
}
// App.svelte
import { markTodoAsDone } from './todos.store';
function onDone(event) {
const name = event.detail;
markTodoAsDone(name);
}
Подписка на изменение состояния
Для того, чтобы узнать, что значение в хранилище изменилось, можно использовать метод subscribe
. Имейте ввиду, что хранилище не является объектом observable
, но предоставляет схожий интерфейс.
const subscription = todos.subscribe(console.log);
subscription(); // так можно отменить подписку
Observables
Если эта часть вызывала у вас наибольшие волнения, то спешу обрадовать, что не так давно в Svelte была добавлена поддержка RxJS и пропозала Observable для ECMAScript.
Как разработчик на Angular, я уже привык работать с реактивным программированием, и отсутствие аналога async pipe было бы крайне неудобным. Но Svelte удивил меня и тут.
Посмотрим на пример совместной работы этих инструментов: отобразим список репозиториев на Github, найденных по ключевому слову "Svelte"
.
Вы можете скопировать код ниже и запустить его прямо в REPL:
<script>
import rx from "https://unpkg.com/rxjs/bundles/rxjs.umd.min.js";
const { pluck, startWith } = rx.operators;
const ajax = rx.ajax.ajax;
const URL = `https://api.github.com/search/repositories?q=Svelte`;
const repos$ = ajax(URL).pipe(
pluck("response"),
pluck("items"),
startWith([])
);
</script>
{#each $repos$ as repo}
<div>
<a href="{repo.url}">{repo.name}</a>
</div>
{/each}
<!--
Имплементация в Angular:
<div *ngFor="let repo of (repos$ | async)>
<a [attr.href]="{{ repo.url }}">{{ repo.name }}</a>
</div>
-->
Просто добавляем символ $
к имени observable-переменной repos$
и Svelte автомагически отображает её содержимое.
Мой список пожеланий для Svelte
Поддержка Typescript
Как энтузиаст Typescript, я не могу не пожелать возможности использования типов в Svelte. Я так привык к этому, что порой увлекаюсь и расставляю типы в своём коде, которые потом приходится убирать. Я очень надеюсь, что в Svelte скоро добавят поддержку Typescript. Думаю этот пункт будет в списке пожеланий любого, кто соберётся использовать Svelte имея опыт работы с Angular.
Соглашения и гайдлайны
Отрисовка в представлении любой переменной из блока <script>
— очень мощная возможность фреймворка, но на мой взгляд, может привести к замусориванию кода. Я надеюсь, что сообщество Svelte проработает ряд соглашений и гайдлайнов, чтобы помочь разработчикам писать чистый и понятный код компонентов.
Поддержка сообществом
Svelte — грандиозный проект, который, при увеличении усилий со стороны сообщества в написании сторонних пакетов, руководств, статей в блогах и прочим, может взлететь и стать признанным инструментом в удивительном мире Frontend-разработки, который мы имеем сегодня.
В заключение
Несмотря на то, что я не был поклонником предыдущей версии фреймворка, Svelte 3 произвёл на меня хорошее впечатление. Он простой, небольшой, но умеет очень многое. Он настолько отличается от всего вокруг, что напомнил мне тот восторг, который я испытал, когда перешёл с jQuery на Angular.
Вне зависимости от того, какой фреймворк вы используете сейчас, изучение Svelte, скорее всего, отнимет лишь пару часов. Как только вы узнаете основы и поймёте различия с тем, что вы уже привыкли писать, работать со Svelte станет очень легко.
В русскоязычном Telegram-канале @sveltejs вы обязательно найдёте разработчиков, имеющих опыт работы с различными фреймворками и готовых поделится своими историями, мыслями и советами касательно Svelte.
Автор: Alexey Schebelev