Это отчёт в продолжение статьи "Уйти от jQuery к Svelte, без боли".
Ниже я расскажу о трудностях с которыми столкнулся, их было не много, и только одна была настолько фундаментальной, где без поддержки сообщества я бы не справился.
Введение
Я планировал переписывать фронтэнд по кусочкам, это не то что бы совсем не получилось, получилось не совсем — переписывать пришлось большими кусками.
Во первых потому что подход jQuery — императивный, подход Svelte — декларативный.
Во вторых потому, что с использованием jQuery у нас область видимости всегда глобальная, из любой строчки кода нам доступны все элементы веб-страницы, мы к ним обращаемся по ID или CSS селектору, в то время как Svelte рекомендует использование компонентов и внутри компонента мы видим только сам компонент, ни внешних элементов ни внутренних у нас нет, и мы не имеем возможности обратиться к ним напрямую.
Со Svelte получается настоящее ООП: мы не можем внести изменения сами, мы можем только сообщить компоненту о необходимости изменений. Как эти изменения будут сделаны, знает только код внутри компонента.
И это прекрасно :)
Для сообщения с компонентами у Svelte есть два механизма.
Связывание переменных (бинды, мапинг)
Мы объявляем переменную и мапим её на атрибут компонента:
<script>
import DetailTaskView from './DetailTaskView.svelte';
let task = {};
let isModal = false;
function renderTask(data) {
const isSuccess = data.hasOwnProperty(0);
if (isSuccess) {
isModal = true;
task = data[0];
}
}
import {fade} from 'svelte/transition';
</script>
{#if isModal}
<div transition:fade>
<DetailTaskView bind:isModal="{isModal}" {...task}/>
</div>
{/if}
Что мы сделали ?
Мы объявили две локальные переменные «task» и «isModal», «task» это информация для отображения в компоненте, данные только выводятся, изменяться не будут, «isModal» это флаг видимости компонента, если пользователь нажал крестик на компоненте, то компонент должен исчезнуть, крестик принадлежит компоненту, поэтому о нажатии мы ни чего не узнаем, но узнаем что значение переменной «isModal» изменилось и благодаря реактивности отработаем это новое значение.
Если нам необходимо двустороннее связывание, то мы пишем «bind:», изменение значения внутри компонента будет сообщено «родительскому» компоненту.
Можно использовать сокращённую форму, если нам надо только сообщить компоненту значения, и если имя атрибута компонента совпадают с именем переменной, мы можем написать "{task}" или использовать деструктор "{...task}".
Удобно.
Но если у нас в один компонент вложен другой, а там ещё и третий, то конечно появляется болерпллейт по прокидыванию значения вверх и вниз по иерархии вложенности.
Всплытие событий
С терминологией могу ошибаться, сильно не пинайте.
Родительский компонент может обрабатывать события дочернего компонента, но только те события о которых дочерний компонент сообщит.
<!-- App.svelte компонент Приложения -->
<script>
import SearchForm from './SearchForm.svelte';
async function applySample(event) {
const sample = event.detail.sample;
if(sample){
search(sample);
}
if(!sample){
renderPage();
}
}
</script>
<div class="container">
<h1>Трекер рабочих заданий</h1>
<SearchForm on:search={applySample}/>
</div>
<!-- SearchForm.svelte компонент Строка поиска -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let sample = '';
function search(event) {
event.preventDefault();
dispatch('search', {
sample: sample
});
}
</script>
<form class="form-horizontal" id="search"
on:submit={search}>
<div class="form-group">
<label class="control-label col-sm-2" for="sample">
Поиск
</label>
<div class="col-sm-10">
<input id="sample" class="form-control" type="search"
placeholder="Введите наименование задачи"
autocomplete="on" bind:value={sample}
/>
</div>
</div>
</form>
Что тут происходит ?
В родительском компоненте по событию «on:search» дочернего компонента «SearchForm» выполняется функция «applySample», эта функция получает параметры (event.detail) из объекта события и обрабатывает их.
Что происходит в компоненте ?
Атрибут «value» элемента input замаплен на переменную «sample», по событию «on:submit» (элемента «form») выполняется функция «search», которая создаёт событие 'search' и в свойство «detail» записывает объект {sample: sample} — то есть значение строки поиска.
Таким образом значение строки поиска передаётся в родительский компонент и именно он решает что ему с этим значением делать.
Компонент несёт ответственность только за отображение формы ввода и передачу введённого значения, компонент не реализует выполнение поиска и отображение результатов, так мы разделяем ответственность.
Красота!
Переход от императивности к декларативности
Тут к сожалению не получиться так же наглядно показать разницу. На словах это звучит так: если при использовании jQuery я создавал html разметку и потом вставлял её в нужное место, то со Svelte я генерирую массив с атрибутами компонентов и потом в цикле добавляю компоненты с заранее рассчитанными атрибутами:
<!-- Paging.svelte компонент для перехода между страницами поисковой выдачи -->
<script>
import {createEventDispatcher} from 'svelte';
import OrdinalPlace from './OrdinalPlace.svelte';
let pagingPlaces;
// тут была бизнес логика расчёта атрибутов для массива pagingPlaces
function addPlace(
paging = [], index = Number.NEGATIVE_INFINITY,
text = "", css = "") {
paging.push({index:index,text:text,css:css});
return paging;
}
const dispatch = createEventDispatcher();
function browsePage(event) {
const pageIndex = event.detail.index;
dispatch('move', {
index: pageIndex
});
}
</script>
{#if pagingPlaces.length}
<table class = "table table-hover-cells table-bordered">
<tbody>
<tr>
{#each pagingPlaces as place (place.index)}
<OrdinalPlace on:move="{browsePage}"
{...place}>
</OrdinalPlace>
{/each}
</tr>
</tbody>
</table>
{/if}
<!-- OrdinalPlace.svelte компонент для отображения элемента "страницы" в списке страниц для перехода -->
<script>
export let index = -1;
export let text = "";
export let css = "";
let number = index +1;
function skip() {
return !(text === "");
}
let letSkip = skip();
let noSkip = !letSkip;
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
function moveTo() {
if(noSkip){
dispatch("move", {index:index});
}
}
</script>
<td class="{css}" on:click="{moveTo}">
{#if letSkip}
{text}
{/if}
{#if noSkip}
{number}
{/if}
</td>
Как это работает ?
При создании компонента Paging мы формируем массив «элементов» для перехода к определённым страницам — «pagingPlaces», далее циклом пробегаем по всем элементам и вставляем компонент для отображения одной позиции пейджинга — «OrdinalPlace».
Опять же декларативный подход, мы не формируем каждую позицию сами, мы сообщаем компоненту что нам необходимо отображение позиции с такими то атрибутами.
Тут мы видим замороченный случай всплытия события. Для перехода к странице поисковой выдачи пользователь кликает по компоненту «OrdinalPlace», этот компонент не умеет загружать страницу, поэтому он создаёт событие «move» с параметром индекс страницы и это событие подхватывает родительский компонент — «Paging», который тоже не умеет загружать страницу поэтому он создаёт событие 'move', и его уже подхватывает следующий родительский компонент и каким то образом обрабатывает.
Svelte и компонентный подход подталкивают нас к разделению ответственности и следованию SOLID.
Самая большая засада
В примере выше показано решение фундаментальной проблемы с которой я бы без подсказки не справился. Svelte кеширует все компоненты и надо ему помогать отслеживать изменения в этих компонентах.
Вот код, о котором идёт речь:
{#each pagingPlaces as place (place.index)}
<OrdinalPlace on:move="{browsePage}"
{...place}>
</OrdinalPlace>
{/each}
Для вывода списка страниц в пейджинге мы бежали по массиву и Svelte каждому компоненту сопоставил какой то индекс массива, теперь решение, о перерисовке компонента, Svelte принимает, исходя из этого индекса, если индекс во время перебора элементов массива не указать, то получиться не пойми что, я сутки пытался понять, потом обратился за помощью зала и в зале не сразу нашёлся человек хорошо знакомый с этими граблями, но мне помогли, спасибо ребятам.
При работе с массивами имейте это в виду: любой проход по массиву должен использовать индекс, ещё раз:
{#each pagingPlaces as place (place.index)}
«pagingPlaces as place (place.index)» — используйте обязательно.
Конечно если вы ранее работали с React/Vue, то наверное вы уже знакомы с этой особенностью.
Визуальные эффекты
В моём приложении использовались модальные окна. jQuery для этого устанавливает требования к разметке, без неё, метод jQuery.modal(), работать не будет.
У Svelte с этим проще:
{#if isModal}
<div transition:fade>
<DetailTaskView bind:isModal="{isModal}" {...task}/>
</div>
{/if}
Конкретно «transition:fade» отвечает за исчезновение / появление элементов на странице.
Нам ни кто не диктует какая у нас должна быть разметка.
Это хорошо.
Кроме этой анимации Svelte имеет ещё парочку: fly и tweened, примеры по ссылкам в учебнике.
Прочее
Имена переменных
Беда с именованием переменных/параметров/атрибутов, приходиться один словом называть и свойство объекта и переменную которую туда записываешь, правило телефонной трубки, когда ты по телефону должен рассказать о коде, так что бы на том конце не запутались и всё поняли, такие повторяющиеся имена нарушают.
AJAX
Это не касается Svelte, но касается отказа от использования jQuery, jQuery.ajax можно заменить на fetch(), я такую замену сделал, можно посмотреть в репозитории.
Заключение
Переход от использования jQuery к использованию Svelte потребует переписать логику создания разметки, но это не так сложно и долго как может показаться, особенно если ваш код этим не грешит.
Svelte упрощает вашу разметку и сокращает JS код, использование Svelte делает ваш код более переиспользуемым и устойчивым к случайным ошибкам.
Используйте Svelte, это будет хорошо для вас и ваших заказчиков!
Ссылки
Официальный сайт Svelte
Репозиторий с переходом от использования jQuery к использованию Svelte
Канал русскоязычного сообщества Svelte в Телеграм
Спасибо, что прочитали.
P.S.: Не знаю почему Хабр вырезает двоеточие из ссылки на канал сообщества, правильная строка ссылки: tg://resolve?domain=sveltejs
kovert99
Поздравляю, из одной крайности в другую