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

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

cmewbijh_gf8rjlvipourrlcw4w.jpeg

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

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

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


Компоненты

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

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

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

hello_world


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

В целом это похоже на импортирование JS-файла, но с парой оговорок:


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

    Из приведенных выше фрагментов очевидно, что количество строк для создания компонента в Svelte невероятно мало. Конечно, присутствуют некоторые неявности и ограничения, но при этом всё достаточно просто, чтобы быстро к этому привыкнуть.


    Базовый синтаксис


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

    Интерполяции в Svelte больше схожи с таковыми в React, нежели в Vue или Angular:

    
    
    { 3 + 5 }
    { someFunction() }
    { someFunction() ? 0 : 1 }

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


    Атрибуты

    Передать атрибуты в компоненты также довольно просто. Кавычки не обязательны и можно использовать любые Javascript-выражения:

    //Svelte
    
    
    


    События

    Синтаксис обработчиков событий выглядит так: on:событие={обработчик}.

    
    
    

    В отличие от Angular, нам не нужно использовать скобки после имени функции, чтобы вызывать её. Если нужно передать аргументы в обработчик, просто используем анонимную функцию:

     onChange(e, ‘a’)} />

    Мой взгляд на читабельность такого кода:


    • Печатать приходится меньше, поскольку нам не нужны кавычки и скобки — это в любом случае хорошо.
    • Читать сложнее. Мне всегда больше нравился подход Angular, а не React, поэтому для меня и Svelte здесь воспринимается тяжелее. Но это просто моя привычка и мое мнение несколько предвзято.


    Структурные директивы

    В отличие от структурных директив в Vue и Angular, Svelte предлагает специальный синтаксис для циклов и ветвлений внутри шаблонов:

    {#if todos.length === 0}
      Список дел пуст
    {:else}
      {#each todos as todo}
         
      {/each}
    {/if}

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


    Входные свойства

    Обозначить свойства, которые можно передать компоненту (аналог @Input в Angular) так же легко, как экспортировать переменную из JS модуля при помощи ключевого слова export. Пожалуй, поначалу это может сбивать с толку —, но давайте напишем пример и посмотрим, насколько это действительно просто:

    
    
    

    { todo.name } { todo.done ? '✓' : '✕' }


    • Как вы могли заметить, мы инициализировали свойство todo вместе со значением: оно будет являться значением свойства по умолчанию, в случае если оно не будет передано из родительского компонента

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

    
    
    {#each todos as todo}
      
    {/each}

    Аналогично полям в обычном JS-объекте, todo={todo} можно сократить и переписать код следующим образом:

    Сначала мне казалось это странным, но теперь я думаю, что это гениально.


    Выходные свойства

    Для реализации поведения директивы @Output, например, получения родительским компонентом каких-либо уведомлений от дочернего, мы будем использовать функцию createEventDispatcher, которая имеется в Svelte.


    • Импортируем функцию createEventDispatcher и присваиваем её возвращаемое значение переменной dispatch


    • Функция dispatch имеет два параметра: имя события и данные(которые попадут в поле detail объекта события)


    • Помещаем dispatch внутри функции markDone, которая вызывается по событию клика (on:click)


    
    
    

    { todo.name } { todo.done ? '✓' : '✕' }

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


    • Создаём функцию onDone
    • Присваиваем эту функцию обработчику события, которое вызывается в дочернем компоненте, таким образом: on:done={onDone}
    
    
    {#each todos as todo}
      
    {/each}

    Примечание: для запуска обнаружения изменения объекта, мы не мутируем сам объект. Вместо этого мы присваиваем переменной todos новый массив, где объект нужной задачи уже будет изменен на выполненный.

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


    ngModel

    В Svelte есть специальный синтаксис bind:<атрибут>={переменная} для привязки определенных переменных к атрибутам компонента и их синхронизации между собой.

    Иначе говоря, он позволяет организовать двухстороннюю привязку данных:

    
    
    


    Реактивные выражения

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

    Например, давайте создадим переменную, которая должна показывать нам, что в массиве todos все задачи отмечены как выполненные:

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

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

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

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

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


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

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

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

    // Button.svelte
    
    
    
    
    // App.svelte
    
    
    

    В этом случае строка "Отправить" займет место элемента .
    Именованным слотам потребуется присвоить имена:

    // Modal.svelte
    
    
    // App.svelte
    
    
    
     
    Заголовок
    Сообщение


    Хуки жизненного цикла

    Svelte предлагает 4 хука жизненного цикла, которые импортируются из пакета svelte.


    • onMount — вызывается при монтировании компонента в DOM
    • beforeUpdate — вызывается перед обновлением компонента
    • afterUpdate — вызывается после обновления компонента
    • onDestroy — вызывается при удалении компонента из DOM

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

    Если callback-функция возвращает другую функцию, то она будет вызвана при удалении компонента из DOM.

    Важно помнить, что при вызове 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}
      
    {/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:

    
    
    {#each $repos$ as repo}
      
    {/each}
    
    
    

    Просто добавляем символ $ к имени observable-переменной repos$ и Svelte автомагически отображает её содержимое.


    Мой список пожеланий для Svelte


    Поддержка Typescript

    Как энтузиаст Typescript, я не могу не пожелать возможности использования типов в Svelte. Я так привык к этому, что порой увлекаюсь и расставляю типы в своём коде, которые потом приходится убирать. Я очень надеюсь, что в Svelte скоро добавят поддержку Typescript. Думаю этот пункт будет в списке пожеланий любого, кто соберётся использовать Svelte имея опыт работы с Angular.


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

    Отрисовка в представлении любой переменной из блока