Уйти от jQuery к Svelte, как это было

от автора

Всем привет.

Это отчёт в продолжение статьи "Уйти от 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», эта функция из объекта события получает параметры и обрабатывает их.

Что происходит в компоненте ?

Атрибут «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, примеры по ссылкам в учебнике.

Прочее

Нет выражений в разметке

Из мелочей, как следствие декларативного подхода, в директивах типа "#if" нельзя использовать выражения, только ранее вычисленные значения.

Поэтому мне вместо того что бы писать:

    {#if letSkip}         {text}     {/if}     {#if !letSkip}         {number}     {/if} 

Пришлось завести ещё одну переменную:

    let letSkip = skip();     let noSkip = !letSkip; 

И конечно настоящая беда с именованием переменных/параметров/атрибутов, приходиться один словом называть и свойство объекта и переменную которую туда записываешь, правило телефонной трубки, когда ты по телефону должен рассказать о коде, так что бы на том конце не запутались и всё поняли, такие повторяющиеся имена нарушают.

AJAX

Это не касается Svelte, но касается отказа от использования jQuery, jQuery.ajax можно заменить на fetch(), я такую замену сделал, можно посмотреть в репозитории.

Заключение

Переход от использования jQuery к использованию Svelte потребует переписать логику создания разметки, но это не так сложно и долго как может показаться, особенно если ваш код этим не грешит.

Svelte упрощает вашу разметку и сокращает JS код, использование Svelte делает ваш код более переиспользуемым и устойчивым к случайным ошибкам.

Используйте Svelte, это будет хорошо для вас и ваших заказчиков!

Ссылки

Официальный сайт Svelte
Репозиторий с переходом от использования jQuery к использованию Svelte
Канал русскоязычного сообщества Svelte в Телеграм

Спасибо, что прочитали.

P.S.: Не знаю почему Хабр вырезает двоеточие из ссылки на канал сообщества: tg://resolve?domain=sveltejs

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


Комментарии

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

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