Emulacja Reacta i JSX w Vanilla JS

Opublikowany: 2022-03-11

Niewiele osób nie lubi frameworków, ale nawet jeśli jesteś jednym z nich, powinieneś wziąć pod uwagę i zastosować funkcje, które nieco ułatwiają życie.

W przeszłości byłem przeciwny stosowaniu frameworków. Jednak ostatnio miałem doświadczenie w pracy z React i Angular w niektórych moich projektach. Kilka razy otworzyłem edytor kodu i zacząłem pisać kod w Angularze, co wydawało mi się dziwne i nienaturalne; zwłaszcza po ponad dziesięciu latach kodowania bez użycia jakichkolwiek frameworków. Po pewnym czasie postanowiłem zobowiązać się do nauki tych technologii. Bardzo szybko jedna duża różnica stała się widoczna: tak łatwo było manipulować DOM, tak łatwo było dostosować kolejność węzłów w razie potrzeby, a zbudowanie interfejsu użytkownika nie wymagało stron i stron kodu.

Choć nadal wolę swobodę nieprzywiązywania się do frameworka czy architektury, nie mogłem pominąć faktu, że tworzenie elementów DOM w jednym jest znacznie wygodniejsze. Zacząłem więc szukać sposobów na naśladowanie doświadczenia w waniliowym JS. Moim celem jest wyciągnięcie niektórych z tych pomysłów z Reacta i zademonstrowanie, jak te same zasady można zaimplementować w zwykłym JavaScript (często określanym jako vanilla JS), aby nieco ułatwić życie programistom. Aby to osiągnąć, zbudujmy prostą aplikację do przeglądania projektów GitHub.

Prosta wyszukiwarka GitHub

Aplikacja, którą budujemy.

Niezależnie od tego, w jaki sposób budujemy frontend za pomocą JavaScript, będziemy uzyskiwać dostęp do DOM i manipulować nim. W naszej aplikacji będziemy musieli skonstruować reprezentację każdego repozytorium (miniaturka, nazwa i opis) i dodać ją do DOM jako element listy. Będziemy używać interfejsu API wyszukiwania GitHub do pobierania naszych wyników. A ponieważ mówimy o JavaScript, przeszukajmy repozytoria JavaScript. Gdy wysyłamy zapytanie do API, otrzymujemy następującą odpowiedź JSON:

 { "total_count": 398819, "incomplete_results": false, "items": [ { "id": 28457823, "name": "freeCodeCamp", "full_name": "freeCodeCamp/freeCodeCamp", "owner": { "login": "freeCodeCamp", "id": 9892522, "avatar_url": "https://avatars0.githubusercontent.com/u/9892522?v=4", "gravatar_id": "", "url": "https://api.github.com/users/freeCodeCamp", "site_admin": false }, "private": false, "html_url": "https://github.com/freeCodeCamp/freeCodeCamp", "description": "The https://freeCodeCamp.org open source codebase "+ "and curriculum. Learn to code and help nonprofits.", // more omitted information }, //... ] }

Podejście React

React bardzo ułatwia pisanie elementów HTML na stronie i jest jedną z funkcji, które zawsze chciałem mieć podczas pisania komponentów w czystym JavaScript. React używa JSX, który jest bardzo podobny do zwykłego HTML.

Jednak nie to czyta przeglądarka.

Pod maską React przekształca JSX w kilka wywołań funkcji React.createElement . Rzućmy okiem na przykład JSX używający jednego elementu z GitHub API i zobaczmy, na co to przekłada.

 <div className="repository"> <div>{item.name}</div> <p>{item.description}</p> <img src={item.owner.avatar_url} /> </div>;
 ; React.createElement( "div", { className: "repository" }, React.createElement( "div", null, item.name ), React.createElement( "p", null, item.description ), React.createElement( "img", { src: item.owner.avatar_url } ) );

JSX jest bardzo prosty. Piszesz zwykły kod HTML i wstrzykujesz dane z obiektu, dodając nawiasy klamrowe. Kod JavaScript w nawiasach zostanie wykonany, a wartość zostanie wstawiona do wynikowego DOM. Jedną z zalet JSX jest to, że React tworzy wirtualny DOM (wirtualną reprezentację strony) do śledzenia zmian i aktualizacji. Zamiast przepisywać cały kod HTML, React modyfikuje DOM strony za każdym razem, gdy informacje są aktualizowane. Jest to jeden z głównych problemów, do rozwiązania którego został stworzony React.

podejście jQuery

Programiści często używali jQuery. Chciałbym o tym wspomnieć, ponieważ jest nadal popularny, a także dlatego, że jest dość zbliżony do rozwiązania w czystym JavaScript. jQuery uzyskuje odwołanie do węzła DOM (lub zbioru węzłów DOM), wysyłając zapytanie do DOM. Otacza również to odniesienie różnymi funkcjami służącymi do modyfikowania jego zawartości.

Chociaż jQuery ma swoje własne narzędzia do tworzenia DOM, to, co najczęściej widzę na wolności, to po prostu konkatenacja HTML. Na przykład możemy wstawić kod HTML do wybranych węzłów, wywołując funkcję html() . Zgodnie z dokumentacją jQuery, jeśli chcemy zmienić zawartość węzła div za pomocą klasy demo-container , możemy to zrobić w ten sposób:

 $( "div.demo-container" ).html( "<p>All new content.<em>You bet!</em></p>" );

Takie podejście ułatwia tworzenie elementów DOM; jednak, gdy musimy zaktualizować węzły, musimy zapytać o węzły, których potrzebujemy, lub (częściej) wrócić do odtworzenia całego fragmentu, gdy wymagana jest aktualizacja.

Podejście DOM API

JavaScript w przeglądarkach ma wbudowane API DOM, które daje nam bezpośredni dostęp do tworzenia, modyfikowania i usuwania węzłów na stronie. Znajduje to odzwierciedlenie w podejściu Reacta, a korzystając z API DOM, zbliżamy się o krok do korzyści płynących z tego podejścia. Modyfikujemy tylko te elementy strony, które faktycznie wymagają zmiany. Jednak React śledzi również osobny wirtualny DOM. Porównując różnice między wirtualnym a rzeczywistym DOM, React jest wtedy w stanie zidentyfikować, które części wymagają modyfikacji.

Te dodatkowe kroki są czasami przydatne, ale nie zawsze, a bezpośrednie manipulowanie DOM może być bardziej wydajne. Możemy tworzyć nowe węzły DOM za pomocą funkcji _document.createElement_ , która zwróci referencję do utworzonego węzła. Śledzenie tych odniesień daje nam łatwy sposób na modyfikowanie tylko tych węzłów, które zawierają część wymagającą aktualizacji.

Korzystając z tej samej struktury i źródła danych, co w przykładzie JSX, możemy skonstruować DOM w następujący sposób:

 var item = document.createElement('div'); item.className = 'repository'; var nameNode = document.createElement('div'); nameNode.innerHTML = item.name item.appendChild(nameNode); var description = document.createElement('p'); description.innerHTML = item.description; item.appendChild(description ); var image = new Image(); Image.src = item.owner.avatar_url; item.appendChild(image); document.body.appendChild(item);

Jeśli myślisz tylko o wydajności wykonywania kodu, to takie podejście jest bardzo dobre. Jednak wydajność nie jest mierzona jedynie szybkością wykonania, ale także łatwością konserwacji, skalowalnością i plastycznością. Problem z tym podejściem polega na tym, że jest bardzo gadatliwy, a czasem zawiły. Musimy napisać kilka wywołań funkcji, nawet jeśli tylko konstruujemy podstawową strukturę. Drugą dużą wadą jest sama liczba tworzonych i śledzonych zmiennych. Załóżmy, że komponent, nad którym pracujesz, zawiera 30 własnych elementów DOM, musisz utworzyć i użyć 30 różnych elementów i zmiennych DOM. Możesz ponownie użyć niektórych z nich i trochę żonglować kosztem łatwości konserwacji i plastyczności, ale może to stać się naprawdę bałaganiarskie, bardzo szybko.

Kolejną istotną wadą jest liczba linii kodu, które musisz napisać. Z czasem coraz trudniej jest przenosić elementy od jednego rodzica do drugiego. To jedna rzecz, którą naprawdę doceniam w React. Mogę wyświetlić składnię JSX i uzyskać w ciągu kilku sekund, który węzeł jest zawarty, gdzie i w razie potrzeby zmienić. I choć na początku może się wydawać, że nie jest to wielka sprawa, większość projektów ma ciągłe zmiany, które sprawią, że będziesz szukać lepszej drogi.

Proponowane rozwiązanie

Praca bezpośrednio z DOM działa i wykonuje zadanie, ale sprawia również, że konstruowanie strony jest bardzo szczegółowe, zwłaszcza gdy musimy dodać atrybuty HTML i węzły zagnieżdżenia. Pomysł polegałby więc na uchwyceniu niektórych korzyści płynących z pracy z technologiami takimi jak JSX i uproszczeniu naszego życia. Zalety, które staramy się odtworzyć, są następujące:

  1. Pisz kod w składni HTML, aby tworzenie elementów DOM było łatwe do odczytania i modyfikacji.
  2. Ponieważ nie używamy odpowiednika wirtualnego DOM, jak w przypadku Reacta, musimy mieć łatwy sposób wskazywania i śledzenia interesujących nas węzłów.

Oto prosta funkcja, która umożliwiłaby to za pomocą fragmentu kodu HTML.

 Browser.DOM = function (html, scope) { // Creates empty node and injects html string using .innerHTML // in case the variable isn't a string we assume is already a node var node; if (html.constructor === String) { var node = document.createElement('div'); node.innerHTML = html; } else { node = html; } // Creates of uses and object to which we will create variables // that will point to the created nodes var _scope = scope || {}; // Recursive function that will read every node and when a node // contains the var attribute add a reference in the scope object function toScope(node, scope) { var children = node.children; for (var iChild = 0; iChild < children.length; iChild++) { if (children[iChild].getAttribute('var')) { var names = children[iChild].getAttribute('var').split('.'); var obj = scope; while (names.length > 0) { var _property = names.shift(); if (names.length == 0) { obj[_property] = children[iChild]; } else { if (!obj.hasOwnProperty(_property)){ obj[_property] = {}; } obj = obj[_property]; } } } toScope(children[iChild], scope); } } toScope(node, _scope); if (html.constructor != String) { return html; } // If the node in the highest hierarchy is one return it if (node.childNodes.length == 1) { // if a scope to add node variables is not set // attach the object we created into the highest hierarchy node // by adding the nodes property. if (!scope) { node.childNodes[0].nodes = _scope; } return node.childNodes[0]; } // if the node in highest hierarchy is more than one return a fragment var fragment = document.createDocumentFragment(); var children = node.childNodes; // add notes into DocumentFragment while (children.length > 0) { if (fragment.append){ fragment.append(children[0]); }else{ fragment.appendChild(children[0]); } } fragment.nodes = _scope; return fragment; }

Pomysł jest prosty, ale potężny; wysyłamy do funkcji HTML, który chcemy utworzyć jako ciąg znaków, w ciągu HTML dodajemy atrybut var do węzłów, które chcemy mieć utworzone dla nas referencje. Drugi parametr to obiekt, w którym te referencje będą przechowywane. Jeśli nie zostanie określony, utworzymy właściwość „węzły” na zwróconym węźle lub fragmencie dokumentu (w przypadku, gdy najwyższy węzeł hierarchii jest więcej niż jeden). Wszystko odbywa się w mniej niż 60 liniach kodu.

Funkcja działa w trzech krokach:

  1. Utwórz nowy pusty węzeł i użyj innerHTML w tym nowym węźle, aby utworzyć całą strukturę DOM.
  2. Przeprowadź iterację przez węzły i jeśli atrybut var istnieje, dodaj właściwość w obiekcie zasięgu, która wskazuje na węzeł z tym atrybutem.
  3. Zwróć najwyższy węzeł w hierarchii lub fragment dokumentu, jeśli jest więcej niż jeden.

Jak więc teraz wygląda nasz kod do renderowania przykładu?

 var UI = {}; var template = ''; template += '<div class="repository">' template += ' <div var="name"></div>'; template += ' <p var="text"></p>' template += ' <img var="image"/>' template += '</div>'; var item = Browser.DOM(template, UI); UI.name.innerHTML = data.name; UI.text.innerHTML = data.description; UI.image.src = data.owner.avatar_url;

Najpierw definiujemy obiekt (UI), w którym będziemy przechowywać referencje do utworzonych węzłów. Następnie tworzymy szablon HTML, którego będziemy używać, jako ciąg znaków, oznaczając docelowe węzły atrybutem „var”. Następnie wywołujemy funkcję Browser.DOM z szablonem i pustym obiektem, który będzie przechowywać referencje. Na koniec używamy przechowywanych referencji do umieszczania danych wewnątrz węzłów.

To podejście oddziela również budowanie struktury DOM i wstawianie danych w osobnych krokach, co pomaga w utrzymaniu porządku i dobrej struktury kodu. Dzięki temu możemy osobno tworzyć strukturę DOM i uzupełniać (lub aktualizować) dane, gdy stają się one dostępne.

Wniosek

Chociaż niektórym z nas nie podoba się pomysł przejścia na frameworki i oddania kontroli, ważne jest, abyśmy zdali sobie sprawę z korzyści, jakie te frameworki przynoszą. Nie bez powodu są tak popularne.

I chociaż framework może nie zawsze odpowiadać Twojemu stylowi lub potrzebom, niektóre funkcje i techniki mogą być adoptowane, emulowane, a czasem nawet oddzielone od frameworka. Niektóre rzeczy zawsze zostaną utracone w tłumaczeniu, ale wiele można zyskać i wykorzystać przy niewielkim ułamku kosztów, jakie ponosi framework.