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

от автора

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

Поскольку мой основной опыт разработки связан с 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.


ссылка на оригинал статьи https://habr.com/ru/post/467091/


Комментарии

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

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