Хабрахабр

[Перевод] Быстрое введение в Svelte с точки зрения разработчика на Angular

Скорее всего Svelte покажется совершенно не похожим на то, с чем вы имели дело до этого, но, пожалуй, это даже хорошо. Svelte — сравнительно новый UI фреймворк, разработанный Ричем Харрисом, который также является автором сборщика Rollup. В этой статье мы сосредоточимся на второй. Две самые впечатляющие особенности этого фреймворка — скорость и простота.

И именно об этом будет рассказано в этой статье: как в Svelte делать те же самые вещи, что и в Angular. Поскольку мой основной опыт разработки связан с Angular, вполне естественно, что я пытаюсь изучить Svelte, копируя уже привычные мне подходы.

Это простое и быстрое введение в Svelte для людей, которые уже используют Angular в качестве своего основного фреймворка. Примечание: Не смотря на то, что в ряде случаев я буду высказывать своё предпочтение, статья не является сравнением фреймворков.

Внимание спойлер: Svelte — это весело.

Компоненты

Например, компонент Button будет создан путем присвоения имени файлу Button.svelte. В Svelte каждый компонент соотносится с файлом, где он написан. (В Svelte имя импортируемого компонента также может не совпадать с именем файла — примечание переводчика) Конечно, мы обычно делаем то же самое в Angular, но у нас это просто соглашение.

Компоненты Svelte однофайловые, и состоят из 3 разделов: script, style и шаблон, который не нужно оборачивать ни в какой специальный тег.

Давайте создадим очень простой компонент, который показывает "Hello World".

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);

Воспользуемся реактивным выражением, которое заодно напомнит нам о существовании "меток" в Javascript: Однако, представление не будет перерисовываться при обновлении массива, потому что значение переменной allDone присваивается лишь единожды.

$: allDone = todos.every(({ done }) => done);

Если вам покажется, что тут "слишком много магии", напомню, что метки — это валидный Javascript. Выглядит весьма экзотично.

Небольшое демо, поясняющее вышесказанное:
demo

Внедрение содержимого

Для внедрения содержимого тоже применяются слоты, которые помещаются в нужное место внутри компонента.

Для простого отображения контента, который был передан внутри элемента компонента, используется специальный элемент 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

Проще говоря, она аналогична действию хука ngOnInit. Функция onMount принимает в качестве параметра callback-функцию, которая будет вызвана, когда компонент будет помещен в DOM.

Если 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>

То есть в фрагменте выше todo уже должно существовать. Важно помнить, что при вызове onMount все входящие в него свойства уже должны быть инициализированы.

Управление состоянием

Про многословность кода при использовании Redux можно забыть. Управлять состоянием в Svelte невероятно просто, и, пожалуй, эта часть фреймворка мне симпатизирует больше остальных. Для примера, создадим хранилище в нашем приложении для хранения и управления задачами.

Записываемые хранилища

Сначала нужно импортировать объект хранилища 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 и экспортирую из него переменную хранилища, чтобы компонент, куда я его импортирую мог работать с ним.

Для получения значения хранилища воспользуемся небольшой магией в Svelte: Очевидно, что теперь объект todos стал хранилищем и более не является массивом.

  • Добавлением символа $ к имени переменной хранилища мы получаем прямой доступ к его значению!

Таким образом, просто заменим в коде все упоминания переменной 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);
}

Подписка на изменение состояния

Имейте ввиду, что хранилище не является объектом observable, но предоставляет схожий интерфейс. Для того, чтобы узнать, что значение в хранилище изменилось, можно использовать метод subscribe.

const subscription = todos.subscribe(console.log);
subscription(); // так можно отменить подписку

Observables

Если эта часть вызывала у вас наибольшие волнения, то спешу обрадовать, что не так давно в Svelte была добавлена поддержка RxJS и пропозала Observable для ECMAScript.

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

Посмотрим на пример совместной работы этих инструментов: отобразим список репозиториев на 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 имея опыт работы с Angular. Я очень надеюсь, что в Svelte скоро добавят поддержку Typescript.

Соглашения и гайдлайны

Я надеюсь, что сообщество Svelte проработает ряд соглашений и гайдлайнов, чтобы помочь разработчикам писать чистый и понятный код компонентов. Отрисовка в представлении любой переменной из блока <script> — очень мощная возможность фреймворка, но на мой взгляд, может привести к замусориванию кода.

Поддержка сообществом

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

В заключение

Он простой, небольшой, но умеет очень многое. Несмотря на то, что я не был поклонником предыдущей версии фреймворка, Svelte 3 произвёл на меня хорошее впечатление. Он настолько отличается от всего вокруг, что напомнил мне тот восторг, который я испытал, когда перешёл с jQuery на Angular.

Как только вы узнаете основы и поймёте различия с тем, что вы уже привыкли писать, работать со Svelte станет очень легко. Вне зависимости от того, какой фреймворк вы используете сейчас, изучение Svelte, скорее всего, отнимет лишь пару часов.

В русскоязычном Telegram-канале @sveltejs вы обязательно найдёте разработчиков, имеющих опыт работы с различными фреймворками и готовых поделится своими историями, мыслями и советами касательно Svelte.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»