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

в 21:59, , рубрики: angular, javascript, SvelteJs, Разработка веб-сайтов

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

Быстрое введение в Svelte с точки зрения разработчика на Angular - 1

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

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

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

Компоненты

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

Компоненты 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);

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

$: 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

Функция 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js