Svelte – wszystko, co powinieneś wiedzieć o nowej wersji tego narzędzia
Na początku kwietnia 2019 odbyła się konferencja ’You Gotta Love Frontend’, na której Rich Harris (twórca m.in. Rollup.js) podczas prezentacji zapowiedział trzecią wersję swojego narzędzia Svelte. Prezentacja wywarła na mnie spore wrażenie, na tyle, by wraz z premierą zapowiadanego narzędzia wypróbować je.
Szymon Kołodziejczak. Programista front-end w Insys Video Technologies. Absolwent Politechniki Poznańskiej i prywatnie pasjonat webowych technologii. W Insys na co dzień zajmuje się współtworzeniem interfejsu dla wdrożeń internetowych telewizji. Ma więc to szczęście, że jego pasje przekładają się na komercyjne doświadczenie. Poza tym chętnie łapie się różnych aktywności fizycznych, by nie spędzać czasu wyłącznie przed monitorem – aktualnie – najchętniej gra w squasha.
Zdaje sobie sprawę, że w świecie JavaScript nowe rozwiązania pojawiają się jak przysłowiowe grzyby po deszczu. Co jakiś czas jednak powstają rozwiązania, przy których programiści zostają na dłużej. Jako autor artykułu, uważam, że Svelte wyróżnia się na tyle, by poświęcić mu chwilę uwagi… a potem jeszcze chwilę i kolejną.
Svelte w dosłownym tłumaczeniu oznacza elegancki, szczupły. Tłumaczenie to bardzo dobrze opisuje narzędzie, ponieważ aplikacja napisana w Svelte waży mało, a kod źródłowy aplikacji zawiera mało linii kodu w stosunku do swoich konkurencyjnych narzędzi. Zdaje sobię sprawę, że powyższe zdanie nie jest miarodajne w żaden sposób, dlatego już śpieszę z konkretami.
W tym celu pomocny okaże się projekt RealWorld, z którego analizy przytoczę liczby. W tym miejscu pozwolę sobie na krótką dygresję, by opowiedzieć o wspomnianym projekcie. RealWorld służy za miejsce zbiorcze dla wielu implementacji portalu Medium.com. Medium.com to portal, w którym można publikować artykuły na dowolne tematy – komercyjny i ambitniejszy przykład ni jeżeli kolejne klasyczne Todo.
Mamy więc do czynienia z wieloma implementacjami jednej aplikacji; m.in w React, Angular, Vue. Implementacje mają restrykcyjne założenia i są weryfikowane przez kompetentnych ludzi, mogą więc posłużyć za reprezentatywny przykład wykorzystania konkretnego narzędzia. Koniec dygresji. Interesują mnie poruszone wcześniej charakterystyki: rozmiar aplikacji oraz liczba linii kodu źródłowego. Wyniki pochodzą z tego artykułu.
Spis treści
Łatwiej zarządzać krótkim kodem
Z tabeli wyników wyselekcjonowałem 4 implementacje; oprócz tytułowego narzędzia wybrałem 3 najbardziej popularne wg npm-trends: React, Vue i Angular. W przypadku React wybrałem implementację ’React-mobx’ – z czystej sympatii do biblioteki wybrałem jej najlepszy wariant w kontekście rozpatrywanych statystyk.
Tablica 1: Wyselekcjonowane wyniki z artykułu pt. ’A RealWorld Comparison of Front-End Frameworks with Benchmarks’ autorstwa Jacek Schae.
Tytułowe narzędzie w wyodrębnionych statystykach wyróżnia się bezdyskusyjnie, zawierając dwa razy mniej linii kodu niż konkurencja. Rozmiar aplikacji napisanej w Svelte jest 10 razy (!) mniejszy od React.
Na GitHubie możemy znaleźć następujący opis narzędzia: ’Svelte to kompilator, który konwertuje deklaratywne komponenty do wydajnego, chirurgicznie aktualizującego drzewo DOM JavaScriptu’. Zacytowana sentencja jest myślą przewodnią dla całego artykułu i postaram się przybliżyć jej słowa w jego kolejnych częściach.
Chciałbym w tym miejscu wyczulić czytelników – ten tekst nie jest przeglądem Svelte, w którym przepisuje z dokumentacji wszystkie jego możliwości. Opisywane narzędzie w kontekście funkcjonalności na żaden krok nie odstępuje od znanych powszechnie rozwiązań. To, co wprowadza to inny sposób implementacji tych funkcjonalności – w Svelte ograniczają się do prostych zapisów, a czasami bywają dla programisty transparentne.
To przekłada się na całą jakość pracy dewelopera – implementuje się szybciej, napisany kod jest łatwiej zarządzany z powodu jego małej ilości, a mniej kodu oznacza mniej bugów.
Kompilatory
Kompilator może wyjść poza sztywne ramy języka, definiując własne zasady. W świecie front-endu na co dzień mamy do czynienia m.in. z SASS, Typescriptem czy Babelem. W przypadku wymienionej trójki, programista ma do dyspozycji narzędzie poprawiające komfort implementacji, a twórcy przeglądarek nie są w żaden sposób zobligowani do dostarczenia nowej funkcjonalności, ponieważ kody transpilowane są do zwykłego JS i CSS (dodatkowo zapewniona jest kompatybilność wsteczna).
Warto w tym miejscu wyróżnić pewien fakt – kompilatorów czy nowych języków transpilowanych do JS powstało już nie mało (np. Imba, Elm), jednak te, które są dzisiaj powszechnie wykorzystywane łączy wspólna rzecz: niski próg wejścia i rozwiązywanie realnego problemu. SASS nie zmienia składni CSS’a a jedynie ją wzbogaca. Dostarcza mechanizmy, których brakuje (brakowało) i wydają się naturalne dla programisty.
Zagnieżdżone selektory, zmienne. Poprawny kod CSS jest pełni poprawnym kodem w SASS, programista nie musi więc porzucać swojego bagażu doświadczeń, ponieważ wszystko, co wprowadza nowy język jest opcjonalne. Identycznie sytuacja wygląda w Typescript – nadzbiorze JavaScript, który wprowadza typowanie. Dla kontrastu wspomniane języki Imba czy Elm łączy z JavaScriptem tylko tyle, że jest on językiem docelowym dla transpilacji kodu. Ich składnia jest zupełnie inna. Rozpoczęcie z nimi pracy wiąże się z koniecznością zaopatrzenia się w odpowiednie narzędzia, chociażby do kolorowania kodu w edytorze tekstowym. JavaScript jest regularnie rozwijany przy pomocy ogromnej społeczności, której wysiłek zostaje częściowo odrzucony.
Są więc pewne cechy kompilatorów, które są wspólne dla narzędzi cieszących się popularnością w świecie Front-endu. W którą stronę idzie Svelte?
Kompilator Svelte
<style> .foo { color: red } </style> <div class =’foo’>Hello world from view</div> <script> console.log(’hello world from console ’) ; </script>
Listing 1: Komponent Svelte
Każdy plik o rozszerzeniu .svelte jest poprawnym komponentem. Może składać się z HTML, JS i CSS (listing 1). Każda z tych części jest opcjonalna, podobnie jak ich kolejność. Co warte uwagi – nie ma tutaj żadnego boilerplate w postaci koniecznych importów, rejestracji czegokolwiek.
Komponent wygląda jak fragment pliku HTML. Właściwie, poprzednie wersje Svelte wykorzystywały rozszerzenie .html, jednak od trzeciej części zdecydowano się na autorskie rozszerzenie. Daje to większą swobodę autorom języka oraz ułatwia selekcje plików potrzebnych do skompilowania.
Pokazałem prosty przykład, który tak naprawdę nie robi nic związanego ze Svelte. Spójrzmy teraz jakie ’wzmocnienia’ ma programista do dyspozycji.
CSS
Zasięg reguł CSS jest ograniczony wyłącznie do komponentu. Programista może więc pisać proste selektory, generyczne nazwy klas bez obaw o konflikty w globalnej przestrzeni nazw. Reguły są ograniczane poprzez dołączenie unikatowej klasy, co obrazuje poniższy przykład:
.foo { color : red; }
Powyższy zapis zostanie zastąpiony przez kompilator w następujący sposób:
.foo .svelte−urs9w7 { color : red; }
Nieużywane reguły zostaną przez kompilator pominięte. Wszystkie globalne style powinny znajdować się w plikach z rozszerzeniem css.
Javascript
Svelte umożliwia deklarowanie reaktywnych zmiennych oraz instrukcji. Spójrzmy na poniższy przykład:
let a = 2; let b = a + 1;
Wartość zmiennej ’b’ zależy od ’a’ tylko w momencie przypisania. W celu utrzymania tej zależności najprościej wykorzystać zwykłą funkcję pomocniczą, np. getB = () => a + 1;
Podobny scenariusz w Svelte można uzyskać przy użyciu specjalnego zapisu:
$ : b = a + 1 ;
Przy deklaracji reaktywnej zmiennej zamiast słówka kluczowego let
lub const
używamy znaku dolara i dwukropek $:
. Składniowo ten zapis jest poprawny i nazywa się labeled statement
. W czystym JavaScript przykłady użycia takiego zapisu ograniczają się do odwołań do konkretnej pętli przy słówkach kluczowych ’break’ i ’continue’ (listing 2). Ale nie w Svelte.
loop1: for (let i = 0; i < 10; i++) { loop2: for (let j = 0; j < 10; j++) { if (j === 5) continue loop1; console.log (i, j); } }
Listing 2: Przykład wykorzystania labeled statement
w JavaScript
Etykiety (label) nie muszą być unikatowe, więc Svelte nie wychodzi poza składnie JavaScriptu, zmienia natomiast jego semantykę – można powiedzieć, że zwyczajnie oszukuje.
Oprócz przypisań programista ma do dyspozycji reaktywne wyrażenia:
let a = 1; $: if (a === 2) { console.log(’Hello’); }
Przy każdej zmianie wartości ’a’, gdy ta będzie równa 2, w konsoli pojawi się prosty komunikat.
HTML
W tym kontekście Svelte dostarcza programistom funkcjonalności, do których już przywykli z innych popularnych rozwiązań jak Angular czy Vue. Znajdziemy więc: pętle foreach, proste warunki, dyrektywy czy możliwość wyrażeń JavaScript w mark’upie. Tak naprawdę ten podpunkt to temat na osobny artykuł – konsekwentnie wyczerpuje go dokumentacja wzbogacona o interaktywne przykłady, serdecznie zachęcam do jej przejrzenia.
Pod koniec artykułu podam nieco bardziej skomplikowany przykład komponentu, w którym znajdą się flagowe funkcjonalności.
Chirurgiczna aktualizacja drzewa DOM
Svelte uzyskuje spójność pomiędzy danymi komponentu a widokiem poprzez śledzenie instrukcji przypisania. Jeżeli chcemy odświeżyć komponent poniższy (listing 5):
<script> let a = 2; </script> <div>{a}</div>
wystarczy, że przypiszemy zmiennej a
inną wartość.
To odpowiedni moment na rozwinięcie kwestii chirurgicznego aktualizowania drzewa DOM. Kompilator analizuje HTML komponentu: rejestruje węzły wyświetlające dynamiczne dane i na ich podstawie dostarcza – szytą na miarę – funkcję aktualizującą widok. Rozmawiamy o naprawdę prostym zapisie:
if (variable_changed) { text_node_that_displays_changed_variable.data = variable; }
A skąd wiadomo kiedy wywołać przygotowaną funkcję? Tym też zajmuje się kompilator – poprzez śledzenie instrukcji przypisania. Jeżeli zmienna dotyczy wyświetlanego widoku to instrukcja przypisania zostanie obudowana w specjalną funkcję ($$invalidade
), która sprawdza, czy nowa wartość uległa zmianie – jeśli tak, komponent zostanie zaktualizowany. Poniżej prosty przykład:
const setA = value => a = value;
Zakładając, że zmienna a
występuje w widoku, powyższy przykład zostanie zastąpiony przez kompilator w poniższy sposób:
const setA = value => $$invalidate (’a’ , a = value);
Przy okazji, śledzenie instrukcji przypisania przez kompilator to coś, na czym można się złapać mutując obiekt bez przypisania, np. zapis array.push(1)
nie odświeży widoku. Widok odświeży natomiast instrukcja array = [...array, 1]
.
Przygotowana funkcja aktualizująca widok:
p(changed, ctx) { if (changed.a) set_data (t, ctx.a); },
Chwila dla funkcji aktualizującej. Nazywa się ona p
, jej pierwszy argument changed
to obiekt zawierający klucze zmian (funkcja $$invalidate
wymaga klucza w pierwszym argumencie), drugi ctx
to obiekt będący kontekstem komponentu – zawiera aktualne dane jego instancji. Funkcja set_data
to prymitywna obudówka na operacje na węźle tekstowym (zmienna t
jest referencją do węzła tekstowego) – jej implementacja wygląda następująco:
function set_data (text, data) { data = ’’ + data; if (text.data !== data) text.data = data; }
Jak sami widzicie, czytanie kodu źródłowego utworzonego przez kompilator nie jest żadnym wyzwaniem przyprawiającym o ból głowy. Zaznaczę jednak, że robię to ze zwykłej ciekawości, ponieważ kompilator nie sprawia żadnych niespodzianek i nie ma potrzeby stale kontrolować jego pracy. By podkreślić impresywność tego prostego mechanizmu detekcji zmian, chciałbym, w skrócie, opowiedzieć jak wygląda to w innych narzędziach. Na warsztat wezmę wspomnianą wcześniej trójkę: Angular, Vue i React.
- Angular wykorzystuje Zone.js i w odpowiedzi na każdą zarejestrowaną asynchroniczną akcję włącza detekcje zmian dla wszystkich komponentów (poczynając od korzenia), szukając zmian, które powinny zostać odzwierciedlone w DOM.
- React wykorzystuje deklaratywne API (setState) i VDOM (upłycając sprawę – zwykły obiekt JavaScript). Przy aktualizacji komponentu tworzony jest jego nowy fragment VDOM, który jest porównywany z jego poprzednią wersją (szukanie zmian). Zmiany w VDOM są odwzorowywane w DOM.
- Vue dla każdego pola danych tworzy specjalny getter i setter implementując wzorzec obserwatora. Przy pobieraniu wartości pola (get) interesant jest zapamiętywany i przy zmianie wartości (set) zostanie powiadomiony. Nie ma więc scenariusza, w którym zmiany szukane są na ślepo, ale dzieje się to kosztem przechowywaniem interesantów, czyli wykorzystywanej ilości pamięci (przy okazji Vue też wykorzystuje VDOM).
Mała dygresja: opisane strategie w Angular i React są domyślnymi i można je poddać optymalizacji, dochodzimy jednak do sytuacji, w której to programiści robią rzeczy za narzędzie, a nie narzędzie za nich.
Powyższe przykłady pokazują, że prosta aktualizacja widoku dla tej trójki wiążę się z dużą ilością wykonywanych operacji. Co więcej, wykorzystywane są w tym celu dodatkowe narzędzia i warstwy abstrakcji, które muszą zostać obliczone przez klienta, a jeszcze wcześniej zostać pobrane.
Dostarczenie wydajnej aplikacji to jeden ze sposobów na zapewnienie pozytywnych doświadczeń użytkownikowi aplikacji. Svelte, odcinając się od opisanych strategii, przyczynia się do realizacji tego sposobu.
Brak zależności
Przeglądając frameworki / biblioteki do tworzenia interfejsów użytkownika często zwracamy uwagę na ich rozmiar. O znaczeniu wagi biblioteki pokazuje popularność narzędzia Preact. Upraszczając, Preact to lżejsza wersja biblioteki React. Ważąc mniej m.in. poprzez ograniczenie funkcjonalności (!), udostępnia niemalże identyczne API. Dla kontrastu, w Svelte autorzy mogą dodawać nowe funkcjonalności bez obaw o powiększające się rozmiary ich projektu – liczy się wyłącznie skompilowany kod.
Kompilator buduje całość kodu komponentu na etapie kompilacji. To znaczy, że Svelte sam w sobie nie jest dołączany do zbudowanego kodu jako jawna zależność i istnieje w projekcie wyłącznie jako tzw. deweloperska zależność. Wykorzystywany jest więc tylko na etapie implementacji.
Mimo to należy wspomnieć, że w trakcie procesu budowania dołączany jest kod współdzielony przez wszystkie komponenty, m.in. prymitywne nakładki na manipulacje DOM (np. przedstawiona wcześniej funkcja set_data
) czy prosty planista (tak, aby do funkcji aktualizującej widok – p
– przekazać wszystkie zmiany komponentu w jednym jej wywołaniu). Jest to jednak relatywnie mały narzut, co udowadnia rozmiar implementacji w projekcie Realworld.io
.
Todo
Zgodnie z obietnicą, na koniec artykułu czas na bardziej zaawansowany przykład niż pojedyncze instrukcje. Poniżej implementacja Todo w Svelte:
<script> let todos = []; $: todosCount = todos.length; const addTodo = () => todos = [...todos, '']; const removeTodo = (idxToRemove) => { todos = todos.filter((_, idx) => idx !== idxToRemove); } </script> <h2>Todos count: {todosCount}</h2> {#each todos as todo, idx} <div> <input bind_value={todo} /> <button on_click={() => removeTodo(idx)}>remove</button> </div> {:else} <span>Empty array</span> {/each} <button on_click={addTodo}>Add</button>
Na co warto zwrócić uwagę? Przede wszystkim na liczbę linii kodu – przykład jest bardzo zwięzły. W kodzie nie ma użycia słowa kluczowego this
– w Svelte programista nie musi kłopotać się kontekstem wywołania zaimplementowanych funkcji komponentu. Dzięki dyrektywom on:event
i bind:value
implementacja edycji poszczególnych wpisów tablicy odbyła się bez dedykowanej funkcji.
Blok else
przy pętli for-each
, który zostanie wykonany kiedy tablica będzie pusta. Reaktywna zmienna todosCount
, której wartość będzie aktualizowana bez dodatkowej ingerencji programisty.
Podsumowanie
Svelte to narzędzie, które pozwala od razu przejść do programowania logiki biznesowej. Dzięki swojej wydajności świetnie sprawdzi się na wszystkich urządzeniach Internet of Things.
Krótkie zapisy pozwalają na szybką implementację, która na pewno zostanie doceniona na hackathonach. Niski próg wejścia, czytelność kodu sprawia, że Svelte ma już niemałą grupę sympatyków. Część firm z powodzeniem wykorzystuje Svelte produkcyjnie, np. Teamspeak czy New York Times.
Mam nadzieję, że ten artykuł przysłuży się do jeszcze większej popularności tytułowego narzędzia – bo naprawdę na nią zasługuję. I na koniec – po raz kolejny zachęcam do przejrzenia dokumentacji i przykładów.
Zdjęcie główne artykułu pochodzi z stocksnap.io.