Diversity w polskim IT
Luke Joliat
Luke JoliatWeb Developer @ Slalom Boston

Czy nadal potrzebujemy frameworków JavaScript?

Sprawdź, czy da się i czy warto zbudować złożoną aplikację front-endową w JS bez frameworków.
27.06.20199 min
Czy nadal potrzebujemy frameworków JavaScript?

Jako programista webowy staram się regularnie oceniać swój zestaw narzędzi i ustalać, czy mogę się obejść bez tego lub innego narzędzia. Ostatnio badałem, jak łatwo jest zbudować złożoną aplikację front-endową bez front-endowych frameworków.

Co to właściwie jest framework JavaScript?

Mówiąc wprost, framework JavaScript jest narzędziem, które można wykorzystać do tworzenia zaawansowanych aplikacji internetowych, zwłaszcza SPA.

W dawnych czasach programiści webowi wdrażali logikę front-endową, opierając się w dużym stopniu na standardowym JS i jQuery. Jednak wraz ze wzrostem złożoności aplikacji front-endowych, narzędzia te się rozrastały, aby sprostać tej złożoności.

Popularne obecnie frameworki mają kilka podstawowych cech wspólnych. Większość frameworków/bibliotek front-endowych, od Vue do React, zapewnia pewną kombinację następujących elementów:

  • Synchronizacja stanu i widoku
  • Routing
  • System szablonów
  • Elementy nadające się do wielokrotnego użytku

Czy frameworki są nadal potrzebne?

To zależy od tego, jak mocno zaakcentujesz słowo, potrzebne. Wiele osób twierdzi, że front-endowe frameworki nie są i nigdy nie były konieczne. Ale to bardzo przydatne narzędzia.

Więc pytanie brzmi: czy frameworki to dzisiejsze jQuery? Czy problemy, które rozwiązują, będą dalej rozwiązywane poprzez zmiany takie jak te w API DOM?

Ciężko powiedzieć, ale postępy w natywnym JS, specyfikacja web components i łatwe do skonfigurowania narzędzia budowania, sprawiły, że tworzenie SPA bez frameworków jest proste, jak nigdy dotąd.

W celu dalszego zbadania tej kwestii, opracowałem aplikację SPA wykorzystującą wyłącznie standardowy JavaScript, natywne web components i Parcel. Po drodze pojawiło się kilka pułapek i trudności, które uwydatniły mocne strony frameworków JS.

Jednak w tym samym czasie, po pokonaniu początkowych przeszkód, byłem zaskoczony tym, jak proste było stworzenie aplikacji SPA tylko ze standardowym JS.

Omówienie

Aplikacja jest prosta. Jest to aplikacja z przepisami z podstawowymi możliwościami CRUD. Użytkownik może tworzyć, edytować, usuwać, oznaczać jako ulubione i filtrować listę przepisów.

Ekran główny

Ekran tworzenia przepisów

Komponenty

Tworzenie web components jest również proste. Tworzysz klasę, która rozszerza HTMLElement (lub HTMLParagraphElement i tak dalej), a następnie używasz tej klasy do zdefiniowania elementu niestandardowego.

Można również wykorzystać hooki cyklu życia, takie jak connectedCallback, disconnectedCallback, attributeChangedCallback.

import template from './recipe.html'
import DATA_SERVICE from '../../utils/data'
export default class Recipe extends HTMLElement {
  constructor () {
    // attach shadow DOM, initialize private recipe property, and initialize data service
    super()
    this._shadowRoot = this.attachShadow({ mode: 'open' })
    this._recipe = null
    this.ds = new DATA_SERVICE()
  }
  connectedCallback () {
    // set html content to imported template
    this._shadowRoot.innerHTML = template
    // attach delete method to delete button
    this._shadowRoot
      .querySelector('.delete')
      .addEventListener('click', () => this._delete())
  }
  _render (title) {
    // set recipe title and text of favorite button
    this._shadowRoot.querySelector('.recipe-title').innerHTML = title
    this._shadowRoot.querySelector('.favorite').innerHTML = this._recipe
      .favorite
      ? 'Unfavorite'
      : 'Favorite'
  }
  _delete () {
    // delete recipe or display error
    try {
      await this.ds.deleteRecipe(this._recipe.id)
    } catch (e) {
      console.error(e)
      alert(
        'Sorry, there was a problem deleting the recipe. Please, try again.'
      )
    }
  }
  get recipe () {
    // getter for recipe
    return this._recipe
  }
  set recipe (recipe = {}) {
    // setter for recipe which triggers render method
    this._recipe = recipe
    this._render(this._recipe.title)
  }
}

window.customElements.define('recipe-item', Recipe)


Routing

Routing dla naszej aplikacji z przepisami również jest dość prosty. W przypadku wystąpienia zdarzenia nawigacyjnego, zmieniam zawartość aplikacji na odpowiedni web component..

Początkowo używałem pakietu npm o nazwie Vanilla JS Router. Z API historii przeglądarki, to jest na tyle proste, że można zrobić swoją własną implementację w mniej niż 100 linijkach! Uwaga: Nie implementuję naprawdę skomplikowanej logiki, takiej jak route guard.

import './components/error/error'
import content404 from './components/404/404.html'
import DATA_SERVICE from './utils/data'
const ds = new DATA_SERVICE()
// get SPA containing element
const $el = document.getElementById('app')

// define routes
const home = async () => {
  await import('./components/recipe/recipe')
  await import('./components/recipe-list/recipe-list')
  await import('./components/modal/modal.js')
  $el.innerHTML = `<recipe-list></recipe-list>`
}

const create = async () => {
  await import('./components/create-recipe/create-recipe')
  $el.innerHTML = `<create-recipe></create-recipe>`
}

const edit = async () => {
  await import('./components/edit-recipe/edit-recipe')
  $el.innerHTML = `<edit-recipe></edit-recipe>`
}

const error404 = async () => {
  $el.innerHTML = content404
}

// match routes with paths
// grab recipe by id param for edit route
const routes = {
  '/': home,
  '/create': create,
  '/error': error404,
  '/edit': async function (params) {
    const id = params.get('id')
    const recipe = await ds.getRecipe(id)
    await edit()
    $el.querySelector('edit-recipe').recipe = recipe
  }
}

// on pop state get params from url and pass to route
// if no such route, error
window.onpopstate = async () => {
  const url = new URL(
    window.location.pathname + window.location.search,
    window.location.origin
  )
  if (routes[window.location.pathname]) {
    await routes[window.location.pathname](url.searchParams)
  } else routes['/error']()
}

// on pop state get params from url and pass to route
// if no such route, error
// add route to browser history
let onNavItemClick = async pathName => {
  const url = new URL(pathName, window.location.origin)
  const params = url.searchParams
  if (routes[url.pathname]) {
    window.history.pushState({}, pathName, window.location.origin + pathName)
    await routes[url.pathname](params)
  } else {
    window.history.pushState({}, '404', window.location.origin + '/404')
    routes['/error']()
  }
}

// on page load/reload, set appropriate route
;(async () => {
  const url = new URL(
    window.location.pathname + window.location.search,
    window.location.origin
  )
  if (routes[window.location.pathname]) {
    await routes[window.location.pathname](url.searchParams)
  } else routes['/error']()
})()

// export routes and nav click method
const router = {
  onNavItemClick,
  routes
}
export { router }


Oto szybkie streszczenie. Chcę by ten artykuł był w miarę krótki. Zaimplementowałem kilka zabawnych funkcji, takich jak nieskończone przewijanie, niestandardowy uploader przeciągnij i upuść  i wiele więcej!

Retrospektywa

Po stworzeniu aplikacji, poświęciłem trochę czasu na zastanowienie się nad zaletami i wadami całego procesu od początku do końca. Zacznę od złych wieści.

Wady

Specyfikacja ciągle się zmienia

Specyfikacja web components jest zarówno stara jak i nowa. Jest tu od dłuższego czasu, niż pierwotnie sądziłem. Web Components zostały po raz pierwszy zaprezentowane przez Alexa Russella na konferencji Fronteers 2011. Jednak w ciągu ostatnich lat nastąpił faktyczny wzrost liczby web components. Nadal jest dużo zamieszania w specyfikacji. Na przykład, specyfikacja HTML imports została porzucona, choć większość dokumentacji/zasobów nadal się do niej odnosi.

Testowanie

Nie ma zbyt wielu zasobów dedykowanych do testowania natywnych web components. Istnieje kilka obiecujących narzędzi, takich jak skatejs ssr i web components tester z Polymer. Ale te narzędzia są tak naprawdę przeznaczone do użytku z ich odpowiednimi bibliotekami. To stwarza pewne trudności w użyciu z natywnymi web components.

Wykrywanie zmian

Posiadanie systemu, który pod spodem automatycznie synchronizuje widok z modelem danych, jest czymś niesamowicie przydatnym. To jest to, co początkowo przyciągnęło wszystkich do Angulara i innych frameworków.

Utrzymanie stanu zsynchronizowanego z widokiem w małej skali nie jest takie trudne. Ale może bardzo szybko wymknąć się spod kontroli, i zmusić cię do dodania mnóstwa listenerów i selektorów.


Shadow DOM

Jestem naprawdę rozdarty jeśli chodzi o shadow DOM. Z jednej strony, uwielbiam ideę enkapsulacji. Jest to rozsądny wzorzec projektowy, sprawia, że kaskada stylów jest łatwiejsza do opanowania, upraszcza podział odpowiedzialności itd. Jednakże, stwarza to również problemy, gdy chcesz, aby pewne rzeczy nie były enkapsulowane (np. wspólny arkusz stylów) i wciąż toczą się debaty na temat najlepszego sposobu, w jaki można to zrobić.

Generowanie struktur DOM

Częścią wspaniałości frameworków/bibliotek takich jak Angular i React jest to, że są mistrzami swojej DOMeny. Oznacza to, że są one doskonałe w efektywnym renderowaniu i re-renderowaniu struktur w DOM. Z bloga Angular University:

Angular nie generuje HTML, by następnie przekazać go do przeglądarki, aby był przetwarzany. Zamiast tego bezpośrednio generuje struktury danych DOM!

Np. Angular, w przeciwieństwie do jQuery, bezpośrednio renderuje struktury danych DOM. Zamiast przekazywać HTML do przeglądarki, w której ma być przetwarzany, a następnie renderowany do struktur danych DOM. Jest to bardziej wydajne, ponieważ eliminuje etap parsowania. Wirtualny DOM jest również bardzo przydatny, ponieważ uniemożliwia ponowne renderowanie wszystkiego za każdym razem, gdy potrzebujesz zaktualizować widok.

Zalety

Z drugiej strony, istnieją niezaprzeczalne korzyści płynące z rozwoju aplikacji w ten sposób:

Rozmiar paczki

Produkt końcowy może być (nacisk na może) o wiele mniejszy i bardziej kompaktowy niż wszystko, co zostało opracowane w oparciu o nowoczesne frameworki. Dla przykładu, build mojej w pełni funkcjonalnej aplikacji z przepisami był o ponad połowę mniejszy niż świeży build z Angulara.

Rozmiar pakietu Angulara

Pakiet aplikacji z przepisami

Uwaga: Są to zaktualizowane, zoptymalizowane rozmiary pakietów.

Zrozumienie

Jeśli tak naprawdę tworzyłeś tylko z frameworkiem i jego CLI, stworzenie aplikacji internetowej bez dodatkowych narzędzi, może to być dla Ciebie świetnym ćwiczeniem. Jako osoba, która chce osiągnąć pewien poziom mistrzostwa (w zakresie, w jakim jest to możliwe) w tworzeniu stron internetowych, niezbędne było dla mnie zdobycie większego praktycznego doświadczenia z narzędziami do budowania, API przeglądarki, wzorcami projektowymi itp.

Wydajność

To, co te front-endowe frameworki i biblioteki robią za kulisami, jest niesamowite. Jednakże, możesz zapłacić za to wydajnością, jeśli zdecydujesz się skorzystać z któregokolwiek z nich; nic nie jest za darmo. Istnieje wiele potencjalnych strat wydajności przy większej skali: czy to zmarnowane re-rendery, nadmierna liczba listenerów, głębokie porównanie obiektów, czy też niepotrzebne i duże manipulacje na DOM. Można w tym wypadku pozbyć się złożoności, wdrażając rzeczy od zera.

Zespoły Angular i React zdają się zdawać sobie sprawę z tych pułapek i zapewniły takie rzeczy, jak nadpisywanie metody shouldUpdate oraz onPush ChangeDetection jako sposób dalszej optymalizacji wydajności.

Prostota i własność kodu

Za każdym razem, gdy wprowadzasz kod pochodzący od osoby trzeciej, podejmujesz pewne ryzyko. Ryzyko to jest zmniejszone dzięki wypróbowanym i przetestowanym bibliotekom/frameworkom, ale nigdy nie zostanie prawdziwie wyeliminowane. Jeśli możesz napisać kod samodzielnie lub z zespołem, możesz zmniejszyć to ryzyko i utrzymywać znaną Ci bazę kodu.

Uwagi i ciekawostki

Miałem niezły ubaw pracując z Parcel. Był bardziej ograniczony niż Webpack, gdy próbowałem pracować nad pewnymi przypadkami brzegowymi, ale przekonałem się, że tag line 'zero config' zazwyczaj działa.

Jest dla mnie również jasne, że wiele osób nazywa React "biblioteką", a Vue to dla nich "progresywny" framework. Chociaż rozumiem tego przyczyny, myślę, że React, Vue i Angular rozwiązują wiele z tych samych problemów. W związku z tym uważam je wszystkie za “frameworki”.

Dlaczego nie użyłem Stencil lub Polymer? Chciałem uniknąć używania pakietów, bibliotek i frameworków na tyle na ile było to możliwe. Chciałem zobaczyć, jak daleko zaszły standardy sieciowe, aby sprostać współczesnym wymaganiom deweloperów(poza narzędziami do budowania).

Jestem pewien, że istnieje wiele innych sposobów developmentu SPA lub aplikacji front-endowych bez głównego frameworku czy bibliotek, tutaj wypróbowałem jeden sposób ale chętnie poznam inne!

Podsumowanie

Duża pomocą w podjęciu decyzji o stosowaniu lub niestosowaniu frameworków jest coś, co nazywam "punktem zwrotnym". W miarę rozwoju aplikacji przychodzi punkt, w którym tworzy się własny framework w celu ponownego wykorzystania funkcji i struktury. Np. masz kilka formularzy i chcesz utworzyć logikę wielokrotnego użytku do reaktywnej walidacji.

Gdy znajdziesz się w tym punkcie, musisz zdecydować, czy warto zainwestować czas w tworzenie systemów, aby osiągnąć to, co można szybko osiągnąć za pomocą frameworka lub biblioteki. Na różnych etapach rozwoju będziesz skłaniał się ku jednej lub drugiej w zależności od tego, jakie są ograniczenia czasowe lub budżetowe, ale frameworki są nadal bardzo istotne, biorąc pod uwagę właściwe scenariusze.

To powiedziawszy, wiele z tego, co robią frameworki, prawdopodobnie stanie się łatwiejsze do zrobienia z mniejszymi bibliotekami i/lub natywnym kodem w miarę upływu czasu. Weźmy moją aplikację jako przykład. Jednocześnie, jeśli duże frameworki i biblioteki pozostaną uniwersalne, mogą się one przekształcać, dostosowywać i utrzymywać jako przydatne narzędzia. Jeśli nie, mogą one skończyć jak jQuery - w większości przypadków to narzędzie przeszłości.

Wniosek

Istnieją ciekawe sposoby rozwijania złożonych front-endowych aplikacji bez frameworków. Jednak specyfikacja dla rzeczy takich jak web components wciąż ewoluuje i wciąż trzeba dopracować pewne rzeczy. Frameworki nadal robią wiele niesamowitych rzeczy i mogą sprawić, że rozwój będzie przebiegał znacznie płynniej.

Obecnie, o ile mi wiadomo, zalety stosowania frameworków często przewyższają wady. Jeśli jednak frameworki nie zaczną rozwiązywać nowych problemów i nie będą się dalej rozwijać, ostatecznie znikną.


Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>