Jakie ramy internetowe rozwiązują i jak sobie z nimi radzić (część 1)

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ W tym artykule Noam Rosenthal zagłębia się w kilka cech technicznych, które są wspólne dla wszystkich frameworków i wyjaśnia, w jaki sposób niektóre z różnych frameworków je implementują i jakie są ich koszty.

Ostatnio bardzo zainteresowałem się porównywaniem frameworków do waniliowego JavaScriptu. Zaczęło się po pewnej frustracji, jaką miałem podczas używania Reacta w niektórych moich niezależnych projektach, a także po mojej niedawnej, bliższej znajomości standardów internetowych jako edytora specyfikacji.

Chciałem zobaczyć, jakie są podobieństwa i różnice między frameworkami , co platforma internetowa ma do zaoferowania jako szczuplejszą alternatywę i czy jest wystarczająca. Moim celem nie jest rozwalanie frameworków, ale raczej zrozumienie kosztów i korzyści, ustalenie, czy istnieje alternatywa i sprawdzenie, czy możemy się z niej uczyć, nawet jeśli zdecydujemy się na użycie frameworka.

W tej pierwszej części zagłębię się w kilka cech technicznych wspólnych dla różnych frameworków i jak implementują je niektóre z różnych frameworków. Przyjrzę się również kosztom korzystania z tych frameworków.

Ramy

Wybrałem cztery frameworki do przyjrzenia się: React, który obecnie jest dominujący, oraz trzy nowsze pretendentki, które twierdzą, że robią rzeczy inaczej niż React.

  • Reagować
    „React sprawia, że ​​tworzenie interaktywnych interfejsów użytkownika jest bezbolesne. Widoki deklaratywne sprawiają, że Twój kod jest bardziej przewidywalny i łatwiejszy do debugowania.”
  • SolidJS
    „Solid kieruje się tą samą filozofią, co React… Ma jednak zupełnie inną implementację, która rezygnuje z wirtualnego DOM”.
  • Smukły
    „Svelte to radykalnie nowe podejście do tworzenia interfejsów użytkownika… etap kompilacji, który ma miejsce podczas tworzenia aplikacji. Zamiast używać technik takich jak porównywanie wirtualnego DOM, Svelte pisze kod, który chirurgicznie aktualizuje DOM, gdy zmienia się stan Twojej aplikacji”.
  • Oświetlony
    „Opierając się na standardach Web Components, Lit dodaje tylko… reaktywność, deklaratywne szablony i garść przemyślanych funkcji”.

Podsumowując, co schematy mówią o ich wyróżnikach:

  • React ułatwia tworzenie interfejsów użytkownika dzięki widokom deklaratywnym.
  • SolidJS podąża za filozofią Reacta, ale używa innej techniki.
  • Svelte stosuje podejście do interfejsów użytkownika w czasie kompilacji.
  • Lit wykorzystuje istniejące standardy, z kilkoma dodatkowymi, lekkimi funkcjami.

Jakie ramy rozwiązują

Same frameworki wspominają słowa deklaratywny, reaktywność i wirtualny DOM. Zagłębmy się w to, co to znaczy.

Programowanie deklaratywne

Programowanie deklaratywne to paradygmat, w którym logika jest definiowana bez określania przepływu sterowania. Opisujemy, jaki powinien być wynik, a nie jakie kroki by nas tam zaprowadziły.

We wczesnych dniach frameworków deklaratywnych, około 2010 roku, interfejsy API DOM były o wiele bardziej puste i pełne, a pisanie aplikacji internetowych z imperatywnym JavaScriptem wymagało dużej ilości standardowego kodu. Wtedy koncepcja „modelu-widoku-modelu” (MVVM) stała się powszechna, dzięki przełomowym frameworkom Knockout i AngularJS, zapewniającymi warstwę deklaratywną JavaScript, która obsługiwała tę złożoność wewnątrz biblioteki.

MVVM nie jest dziś powszechnie używanym terminem i stanowi swego rodzaju odmianę starszego terminu „wiązanie danych”.

Wiązanie danych

Powiązanie danych to deklaratywny sposób wyrażania sposobu synchronizacji danych między modelem a interfejsem użytkownika.

Wszystkie popularne struktury interfejsu użytkownika zapewniają pewną formę powiązania danych, a ich samouczki zaczynają się od przykładu powiązania danych.

Oto wiązanie danych w JSX (SolidJS i React):

 function HelloWorld() { const name = "Solid or React"; return ( <div>Hello {name}!</div> ) }

Wiązanie danych w Lit:

 class HelloWorld extends LitElement { @property() name = 'lit'; render() { return html`<p>Hello ${this.name}!</p>`; } }

Wiązanie danych w Svelte:

 <script> let name = 'world'; </script> <h1>Hello {name}!</h1>

Reaktywność

Reaktywność to deklaratywny sposób wyrażania propagacji zmiany.

Gdy mamy sposób na deklaratywne wyrażanie powiązania danych, potrzebujemy wydajnego sposobu na propagację zmian przez framework.

Silnik React porównuje wynik renderowania z poprzednim wynikiem i stosuje różnicę do samego DOM. Ten sposób obsługi propagacji zmian nazywa się wirtualnym DOM.

W SolidJS odbywa się to bardziej wyraźnie, z jego sklepem i wbudowanymi elementami. Na przykład element Show śledziłby zmiany wewnętrznie zamiast wirtualnego DOM.

W Svelte generowany jest kod „reaktywny”. Svelte wie, które zdarzenia mogą spowodować zmianę, i generuje prosty kod, który wyznacza linię między zdarzeniem a zmianą DOM.

W Lit, reaktywność jest osiągana za pomocą właściwości elementu, zasadniczo polegając na wbudowanej reaktywności niestandardowych elementów HTML.

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Logika

Gdy framework zapewnia deklaratywny interfejs do wiązania danych, wraz z implementacją reaktywności, musi również zapewnić pewien sposób wyrażenia części logiki, która tradycyjnie jest pisana bezwzględnie. Podstawowe bloki konstrukcyjne logiki to „jeśli” i „dla”, a wszystkie główne struktury zapewniają pewien wyraz tych bloków konstrukcyjnych.

Warunkowe

Oprócz wiązania podstawowych danych, takich jak liczby i ciąg, każdy framework dostarcza „warunkowy” prymityw. W React wygląda to tak:

 const [hasError, setHasError] = useState(false); return hasError ? <label>Message</label> : null; … setHasError(true);

SolidJS zapewnia wbudowany komponent warunkowy Show :

 <Show when={state.error}> <label>Message</label> </Show>

Svelte dostarcza dyrektywę #if :

 {#if state.error} <label>Message</label> {/if}

W Lit, użyjesz jawnej trójskładnikowej operacji w funkcji render :

 render() { return this.error ? html`<label>Message</label>`: null; }

Listy

Inną wspólną podstawową strukturą jest obsługa list. Listy stanowią kluczową część interfejsów użytkownika — listy kontaktów, powiadomień itp. — i aby działały wydajnie, muszą być reaktywne, a nie aktualizować całej listy w przypadku zmiany jednego elementu danych.

W React obsługa list wygląda tak:

 contacts.map((contact, index) => <li key={index}> {contact.name} </li>)

React używa specjalnego atrybutu key do rozróżniania elementów listy i zapewnia, że ​​cała lista nie zostanie zastąpiona przy każdym renderowaniu.

W SolidJS używane są wbudowane elementy for i index :

 <For each={state.contacts}> {contact => <DIV>{contact.name}</DIV> } </For>

Wewnętrznie SolidJS używa własnego sklepu w połączeniu z for i index aby decydować, które elementy zaktualizować, gdy elementy się zmienią. Jest bardziej jednoznaczny niż React, co pozwala nam uniknąć złożoności wirtualnego DOM.

Svelte używa dyrektywy each , która jest transpilowana na podstawie jej aktualizatorów:

 {#each contacts as contact} <div>{contact.name}</div> {/each}

Lit dostarcza funkcję repeat , która działa podobnie do mapowania list opartego na key w React:

 repeat(contacts, contact => contact.id, (contact, index) => html`<div>${contact.name}</div>`

Model komponentu

Jedną z rzeczy, która wykracza poza zakres tego artykułu, jest model komponentów w różnych frameworkach i sposób, w jaki można sobie z tym poradzić za pomocą niestandardowych elementów HTML.

Uwaga : to duży temat i mam nadzieję, że omówię go w przyszłym artykule, ponieważ ten byłby zbyt długi. :)

Koszt

Struktury zapewniają deklaratywne wiązanie danych, elementy podstawowe przepływu sterowania (warunki i listy) oraz reaktywny mechanizm propagacji zmian.

Zapewniają również inne ważne rzeczy, takie jak sposób ponownego wykorzystania komponentów, ale to temat na osobny artykuł.

Czy frameworki są przydatne? TAk. Dają nam wszystkie te wygodne funkcje. Ale czy to właściwe pytanie? Korzystanie z frameworka wiąże się z kosztami. Zobaczmy, jakie są te koszty.

Rozmiar pakietu

Patrząc na rozmiar pakietu, lubię patrzeć na zminimalizowany rozmiar inny niż Gzip. Jest to rozmiar, który najbardziej odpowiada kosztowi procesora podczas wykonywania JavaScriptu.

  • ReactDOM ma około 120 KB.
  • SolidJS ma około 18 KB.
  • Świeci około 16 KB.
  • Svelte ma około 2 KB, ale rozmiar wygenerowanego kodu jest różny.

Wygląda na to, że dzisiejsze frameworki spisują się lepiej niż React polegający na utrzymywaniu małego rozmiaru pakietu. Wirtualny DOM wymaga dużo JavaScriptu.

Buduje

W jakiś sposób przyzwyczailiśmy się do „budowania” naszych aplikacji internetowych. Nie da się rozpocząć projektu front-end bez skonfigurowania Node.js i bundlera, takiego jak Webpack, radzenia sobie z niektórymi ostatnimi zmianami konfiguracji w pakiecie startowym Babel-TypeScript i całym tym jazzem.

Im bardziej wyrazisty i mniejszy rozmiar pakietu, tym większe obciążenie narzędziami do budowania i czasem transpilacji.

Svelte twierdzi, że wirtualny DOM to czysta narzuta. Zgadzam się, ale być może „budowanie” (jak w przypadku Svelte i SolidJS) i niestandardowe silniki szablonów po stronie klienta (jak w przypadku Lit) są również czystymi kosztami ogólnymi, innego rodzaju?

Debugowanie

Z budową i transpilacją wiąże się inny rodzaj kosztów.

Kod, który widzimy, gdy używamy lub debugujemy aplikację internetową, jest zupełnie inny niż ten, który napisaliśmy. Teraz polegamy na specjalnych narzędziach do debugowania o różnej jakości, aby odtwarzać wstecznie to, co dzieje się na stronie i łączyć to z błędami w naszym własnym kodzie.

W React stos wywołań nigdy nie jest „twój” — React zajmuje się planowaniem za Ciebie. Działa to świetnie, gdy nie ma błędów. Ale spróbuj zidentyfikować przyczynę ponownego renderowania w nieskończonej pętli, a czeka cię świat bólu.

W Svelte rozmiar pakietu samej biblioteki jest niewielki, ale zamierzasz dostarczyć i debugować całą masę zaszyfrowanego wygenerowanego kodu, który jest implementacją reaktywności Svelte, dostosowaną do potrzeb Twojej aplikacji.

Z Lit nie chodzi o budowanie, ale aby skutecznie debugować, musisz zrozumieć jego silnik szablonów. To może być największy powód, dla którego mój sentyment do frameworków jest sceptyczny.

Kiedy szukasz niestandardowych rozwiązań deklaratywnych, kończy się to bardziej bolesnym, imperatywnym debugowaniem. Przykłady w tym dokumencie używają Typescript do specyfikacji API, ale sam kod nie wymaga transpilacji.

Aktualizacje

W tym dokumencie przyjrzałem się czterem frameworkom, ale jest ich więcej niż mogę zliczyć (AngularJS, Ember.js i Vue.js, żeby wymienić tylko kilka). Czy możesz liczyć na to, że framework, jego programiści, jego mindshare i jego ekosystem będą pracować dla Ciebie w miarę rozwoju?

Jedną rzeczą, która jest bardziej frustrująca niż naprawianie własnych błędów, jest konieczność znalezienia obejścia błędów frameworka. I jedna rzecz, która jest bardziej frustrująca niż błędy w frameworku, to błędy, które pojawiają się podczas uaktualniania frameworka do nowej wersji bez modyfikowania kodu.

To prawda, że ​​ten problem występuje również w przeglądarkach, ale kiedy się pojawi, zdarza się każdemu, a w większości przypadków poprawka lub opublikowane obejście jest nieuchronne. Ponadto większość wzorców w tym dokumencie jest oparta na dojrzałych interfejsach API platformy internetowej; nie zawsze trzeba iść z krwawiącą krawędzią.

Streszczenie

Zanurzyliśmy się nieco głębiej w zrozumieniu podstawowych problemów, które frameworki próbują rozwiązać oraz tego, jak je rozwiązywać, koncentrując się na wiązaniu danych, reaktywności, warunkach warunkowych i listach. Przyjrzeliśmy się również kosztom.

W części 2 zobaczymy, jak te problemy można rozwiązać bez użycia frameworka i czego możemy się z nich nauczyć. Bądźcie czujni!

Specjalne podziękowania dla następujących osób za recenzje techniczne: Yehonatan Daniv, Tom Bigelajzen, Benjamin Greenbaum, Nick Ribal i Louis Lazaris.