Svelte - nie tylko React, Vue i Angular
- Czas potrzebny na przeczytanie:8 minut
- Opublikowane:
Cześć 👋
Obecnie w środowisku frontendowym mamy świętą trójcę jeśli chodzi o frameworki/biblioteki UI. Jest to jak zapewne wiecie React, Vue i Angular. Warto jednak wyrwać się na chwilę z tej bańki i poznać coś innego. Dokładnie tak było w moim przypadku, chciałem czegoś nowego i natrafiłem na Svelte. W tym wpisie chciałbym się podzieli moimi przemyśleniami, wprowadzają was jednocześnie w świat Svelte, zaczynajmy!
Wprowadzenie
Svelte jest frameworkiem, trochę podobnym do Reakta, trochę do Vue, takie to dziwne, ale przyjemne połączenie. Główną różnicą pomiędzy popularnymi frameworkami a Svelte, jest to, że Svelte nie korzysta z Virtual DOM, a zamiast niego pracuje na zwykłym DOMie. Twórcy reklamują Svelte jako rozwiązanie, które nie posiada za dużo niepotrzebnego boilerplatu.
Czy tak jest na prawdę? Sprawdźmy!
Nie traktuj tego wpisu jako poradnika/kursu Svelte, jest to luźne wprowadzenie, pokazujące główne koncepty tego frameworka
Ten sam komponent napisany w Reakcie, Vue i Svelte:
React:
import React, { useState } from "react";
export default const App = () => {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
function handleChangeA(event) {
setA(+event.target.value);
}
function handleChangeB(event) {
setB(+event.target.value);
}
return (
<div>
<input type="number" value={a} onChange={handleChangeA} />
<input type="number" value={b} onChange={handleChangeB} />
<p>
{a} + {b} = {a + b}
</p>
</div>
);
};
Vue:
<template>
<div>
<input type="number" v-model.number="a" />
<input type="number" v-model.number="b" />
<p>{{a}} + {{b}} = {{a + b}}</p>
</div>
</template>
<script>
export default {
data: function () {
return {
a: 1,
b: 2,
};
},
};
</script>
Svelte:
<script>
let a = 1;
let b = 2;
</script>
<input type="number" bind:value="{a}" />
<input type="number" bind:value="{b}" />
<p>{a} + {b} = {a + b}</p>
Wygląda zachęcająco? No to zaczynajmy!
Początek
Zacznijmy od początku, zainstalujmy Svelte:
npx degit sveltejs/template my-svelte-project
W Svelte nasz komponent ma końcówkę .svelte
i składa się z trzech części: logiki, stylów i treści.
<script>
import SomeComponent from './SomeComponent.svelte';
const name = 'John';
</script>
<style>
.name {
color: #213454;
}
</style>
<p class="name">Hello, my name is {name}</p>
<SomeComponent />
Nasza logika będzie umieszczana w tagu <script>
, style w <style>
, a sama treść luźno wrzucona pod nimi. Dla kogoś kto miał już do czynienia z innym frameworkiem, nie będzie tutaj zaskoczenia. Tworzymy zmienna, a następnie podajemy ją w tzw. wąsach. Style działają w zakresie pliku.
Logika i Propsy
Znamy już podstawy, teraz przyszedł czas na wprowadzenie jakieś logiki, propsów.
Zacznijmy od tych drugich, propsy przekazujemy:
Component.svelte:
<script>
export let name;
</script>
<p>My name is {name}</p>
Tak pobieramy propsy od naszego rodzica, trochę dziwne, przecież w zwykłym JavaScripcie export
działa w inny sposób 🤔
Nie martw się to dopiero początek Sveltowych dziwactw.
A tak przekazujemy propsy:
App.svelte:
<script>
import Component from './Component.svelte';
let name = 'Olaf';
</script>
<Component name="{name}" />
Propsy możemy również spreadować i podawać im defaultową wartość - export let name = "Kuba"
.
Przejdźmy do logiki i bloków if/else
. Tutaj pojawia się rzecz, której na prawdę nie lubię w Svelte. Wydaję mi się to mało czytelne i lepszym rozwiązaniem byłoby klasyczne podejście, zobaczmy jak to wygląda:
<script>
let isOn = false;
function toggle() {
isOn = !isOn;
}
</script>
<button on:click="{toggle}">
Toggle me
</button>
{#if isOn}
<span>
👋
<span>
{:else}
<span>
❌
<span>
{/if}
Blok otwieramy znakiem #
a zamykamy /
, możemy w środku wyrażenia, dodać jakaś kontynuację, czyli w tym przypadku :else
i ta kontynuacja musi być poprzedzona :
.
Możesz tutaj zauważyć zdarzenie
on:click
, ale o tym za chwilę!
Kolejnym elementem logiki będzie iterowanie i zwracanie jakiś danych z tablicy, coś co często robimy chociażby w Reakcie.
<script>
let animals = [
{ name: "Cat", emoji: "🐱" },
{ name: "Dog", emoji: "🐶" },
{ name: "Panda", emoji: "🐼" },
];
</script>
<ul>
{#each animals as animal}
<li>
<h2>{animal.name}</h2>
<span>{animal.emoji}</span>
</li>
{/each}
</ul>
Tym razem zamiast if
używamy bloku each
, który przeiteruje po naszych zwierzętach i zwróci nam potrzebne dane.
Możemy użyć tutaj destrukturyzacji, podać po przecinku
index
jak w mapie, a nawet przekazać klucz, dzięki któremu będziemy mogli wykonywać specjalne akcje{#each animals as animal (animals.name)}
Zdarzenia
Tutaj nie ma większego zaskoczenia, zdarzenia podajemy z początkiem on:
i nazwą danego eventu.
<button on:click="{e => console.log(e.target)}">Hi there 👋</button>
Ciekawą są za to modyfikatory, dzięki którym możemy wpływać na działanie samego zdarzenia, na przykład wywołać je tylko raz:
<button on:click|once="{e => console.log(e.target)}">Hi there 👋</button>
Przydatne będzie na pewno użycie modyfikatora preventDefault
. W łatwy sposób możemy również dispachować zdarzenia korzystając ze specjalnej funkcji createEventDispatcher()
.
Component.svelte
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('say-hello', {
message: 'Hello!',
});
}
</script>
<button on:click="{sayHello}">Hi there 👋</button>
App.svelte
<script>
import Component from './Component.svelte';
function handleSayHello(e) {
console.log(e.detail.message);
}
</script>
<Component on:say-hello="{handleSayHello}" />
Reaktywność i Binding
Dzięki reaktywności w Svelte, pewne dane mogą być zależne od siebie i tworzyć tzw. reaktywne deklaracje, takim przykładem jest doubled
, który jest po prostu dwukrotnością count
.
<script>
let count = 0;
$: doubled = count * 2;
function handleClick() {
count += 1;
}
</script>
<button on:click="{handleClick}">Clicked {count} {count === 1 ? 'time' : 'times'}</button>
<p>{count} doubled is {doubled}</p>
Reaktywność w Svelte to nie tylko deklaracje, możemy również korzystać ze statementów, przykład takiego wykorzystania znajdziesz w oficjalnym poradniku Svelte
Bindingi ułatwiają nam pracę, odciążając nas trochę z niepotrzebnego boilerplatu.
Weźmy przykładowe zdarzenie on:input
, stwórzmy funkcję onChange
, która będzie uaktualniała naszą zmienną name
, całość wyświetlimy w tagu h1
.
<script>
let name = 'world';
function onChange(e) {
name = e.target.value;
}
</script>
<input on:input="{onChange}" />
<h1>Hello {name}!</h1>
Nie ma tutaj za dużo tego niepotrzebnego kodu, moglibyśmy to trochę skrócić, wstawiając funkcję inlinowo do zdarzenia, ale to trochę psuję czytelność.
Na ratunek przychodzą właśnie bindingi!
<script>
let name = 'world';
</script>
<input bind:value="{name}" />
<h1>Hello {name}!</h1>
Bindować możemy różne wartości, zaczynając od zwykłego tekstu po wartości boolean
, this
i kończąc np. na wymiarach elementu(tutaj wartości typu number
):
<script>
let w;
let h;
let size = 42;
let text = 'edit me';
</script>
<style>
input {
display: block;
}
div {
display: inline-block;
}
span {
word-break: break-all;
}
</style>
<input type="range" bind:value="{size}" />
<input bind:value="{text}" />
<p>size: {w}px x {h}px</p>
<div bind:clientWidth="{w}" bind:clientHeight="{h}">
<span style="font-size: {size}px">{text}</span>
</div>
Metody cyklu życia komponentu
Każdy komponent w Svelte, ma własne metody cyklu życia, są to, jak możecie się domyślać, specjalne funkcję, które pozwalają uruchomić jakiś kod w danym momencie tego cyklu.
Pierwszą z nich jest onMount
, działa ona na podobnej zasadzie jak metoda componentDidMount
w React. Ta metoda wywoływana jest wtedy, gdy komponent pierwszy raz wyrenderuje się w DOM. Tutaj powinny odbywać się wszystkie zapytania do API:
<script>
import { onMount } from 'svelte';
let user = [];
onMount(async () => {
const res = await fetch(`https://randomuser.me/api/`);
user = await res.json();
});
</script>
Kolejną metodą jest onDestroy
, odpala się ona gdy komponent zostaje zniszczony, czy też ładniej mówiąc zostaję odmontowany. Tutaj czyścimy wszelkiego rodzaju subskrypcje z onMount
, czy też timery:
<script>
import { onDestroy } from 'svelte';
let seconds = 0;
function onInterval(callback, milliseconds) {
const interval = setInterval(callback, milliseconds);
onDestroy(() => {
clearInterval(interval);
});
}
onInterval(() => (seconds += 1), 1000);
</script>
<p>The page has been open for {seconds} {seconds === 1 ? 'second' : 'seconds'}</p>
Oprócz tych dwóch podstawowych mamy jeszcze metody beforeUpdate
i afterUpdate
. Odpalają się one odpowiednio przed i po zaktualizowaniu DOM.
Store
W Svelte możemy stworzyć tzw. store
, będzie to taki globalny stan naszej aplikacji, z którego będziemy mogli korzystać w niezależnych od siebie komponentach. Dzięki temu możemy uniknąć ciągłego przekazywania propsów w dół naszego drzewka. Tutaj dla przykładu, stworzyliśmy store i wszystkie komponenty w jednym pliku, jednak dobrą praktyką było rozdzielenie ich na osobne komponenty, wtedy store
staję się użyteczny.
Store to po prostu obiekt, który posiada metody: set
, update
i subscribe
. Subscribe nie będzie nam tutaj przydatny, ponieważ, nie musimy subskrybować do danej wartości i później czyścić tą subskrypcje, wystarczy, że przed nazwą stora dodamy $
, dzięki temu dostaniemy jego wartość.
<script>
import { writable } from 'svelte/store';
const store = writable(0);
function reset() {
store.set(0);
}
function increment() {
store.update((n) => n + 1);
}
function decrement() {
store.update((n) => n - 1);
}
</script>
<h1>The count is {$store}</h1>
<button on:click="{increment}">+</button>
<button on:click="{decrement}">-</button>
<button on:click="{reset}">reset</button>
Slots
Są to specjalne komponenty, które mogą przyjmować dzieci. Slota definiujemy jak normalny tag html <slot></slot>
, możemy mu nadać nazwę i odpowiedni defaultowy content, w przypadku gdy nie zostanie on podany. Ten fallback, czyli początkowy kontent, podajemy w środku slota, dzięki nazwie za to, mamy większą kontrolę nad slotami
i możemy definiować to, w jaki sposób będą przetwarzane dane z nich.
Animal.svelte
<style>
.box {
width: 300px;
border: 1px solid #aaa;
border-radius: 2px;
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.1);
padding: 1em;
margin: 0 0 1em 0;
}
</style>
<div class="box">
<slot name="animal-name">
<h1>Panda</h1>
</slot>
<slot name="animal-emoji">
<span>🐼</span>
</slot>
</div>
App.svelte
<script>
import Animal from './Animal.svelte';
</script>
<Animal>
<h2 slot="animal-name">Dog</h2>
<p slot="animal-emoji">🐶</p>
</Animal>
Do slotów
możemy przekazywać również propsy.
Podsumowanie
To by było na tyle w tym krótkim wstępie do Svelte, zostało jeszcze dużo rzeczy do nauczenia, których nie poruszyłem tutaj. Szczególnie fajne wydają się animację, warto sprawdzić!
Svelte ma świetny tutorial, z którego korzystałem w tym wpisie. Dostępne są również gotowe przykłady które znajdziesz tutaj.
Svelte wydaję się bardzo fajnym, szybkim i lekkim rozwiązaniem, które na pewno redukuję dużo niepotrzebnego boilerplatu, część rzeczy mi się w nim podoba, część nie, ale nie ma rozwiązań idealnych. Myślę jednak, że warto zainteresować się nim i dać mu szansę, chociażby dla czystej zabawy!
Do usłyszenia!