Migracja Frankensteina: podejście niezależne od struktury (część 2)

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ Niedawno rozmawialiśmy o tym, czym jest „Frankenstein Migration”, porównaliśmy ją z konwencjonalnymi typami migracji i wspomnieliśmy o dwóch głównych blokach konstrukcyjnych: mikroserwisach i komponentach internetowych . Otrzymaliśmy również teoretyczne podstawy działania tego typu migracji. Jeśli nie przeczytałeś lub zapomniałeś tej dyskusji, możesz najpierw wrócić do części 1, ponieważ pomaga to zrozumieć wszystko, co omówimy w drugiej części artykułu.

W tym artykule przetestujemy całą teorię, wykonując krok po kroku migrację aplikacji, zgodnie z zaleceniami z poprzedniej części. Aby uprościć sprawę, zmniejszyć niepewność, niewiadome i niepotrzebne zgadywanie, na praktycznym przykładzie migracji, postanowiłem zademonstrować praktykę na prostej aplikacji do zrobienia.

Czas przetestować teorię
Czas przetestować teorię. (duży podgląd)

Ogólnie rzecz biorąc, zakładam, że dobrze rozumiesz, jak działa ogólna aplikacja do zrobienia. Ten typ aplikacji bardzo dobrze odpowiada naszym potrzebom: jest przewidywalny, ale ma minimalną wykonalną liczbę wymaganych komponentów, aby zademonstrować różne aspekty migracji Frankensteina. Jednak bez względu na rozmiar i złożoność Twojej prawdziwej aplikacji, podejście jest dobrze skalowalne i powinno być odpowiednie dla projektów o dowolnej wielkości.

Domyślny widok aplikacji TodoMVC
Domyślny widok aplikacji TodoMVC (duży podgląd)

W tym artykule jako punkt wyjścia wybrałem aplikację jQuery z projektu TodoMVC — przykład, który może być już znany wielu z Was. jQuery jest wystarczająco przestarzałe, może odzwierciedlać rzeczywistą sytuację w twoich projektach, a co najważniejsze, wymaga znacznej konserwacji i hacków do zasilania nowoczesnej dynamicznej aplikacji. (To powinno wystarczyć do rozważenia migracji do czegoś bardziej elastycznego).

Czym jest ta „bardziej elastyczna”, do której będziemy wtedy migrować? Aby pokazać wysoce praktyczny przypadek przydatny w prawdziwym życiu, musiałem wybrać jeden z dwóch najpopularniejszych obecnie frameworków: React i Vue. Jednak cokolwiek bym wybrała, przegapilibyśmy niektóre aspekty innego kierunku.

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

Więc w tej części omówimy oba z poniższych:

  • Migracja aplikacji jQuery do Reacta oraz
  • Migracja aplikacji jQuery do Vue .
Nasze cele: rezultaty migracji do React i Vue
Nasze cele: rezultaty migracji do React i Vue. (duży podgląd)

Repozytoria kodów

Cały wspomniany tutaj kod jest publicznie dostępny i możesz się do niego dostać, kiedy tylko chcesz. Dostępne są dwa repozytoria:

  • Frankenstein TodoMVC
    To repozytorium zawiera aplikacje TodoMVC w różnych frameworkach/bibliotekach. Na przykład w tym repozytorium możesz znaleźć gałęzie takie jak vue , angularjs , react i jquery .
  • Demo Frankensteina
    Zawiera kilka gałęzi, z których każda reprezentuje określony kierunek migracji pomiędzy aplikacjami, dostępnymi w pierwszym repozytorium. Istnieją gałęzie takie jak migration/jquery-to-react i migration/jquery-to-vue , które omówimy później.

Oba repozytoria są w toku i należy regularnie dodawać do nich nowe oddziały z nowymi aplikacjami i kierunkami migracji. ( Możesz również wnieść swój wkład! ) Historia zatwierdzeń w gałęziach migracji jest dobrze ustrukturyzowana i może służyć jako dodatkowa dokumentacja z jeszcze większą ilością szczegółów, niż mógłbym omówić w tym artykule.

A teraz ubrudźmy sobie ręce! Przed nami długa droga, więc nie oczekuj, że jazda będzie gładka. Od Ciebie zależy, w jaki sposób chcesz śledzić ten artykuł, ale możesz wykonać następujące czynności:

  • Sklonuj gałąź jquery z repozytorium Frankenstein TodoMVC i ściśle przestrzegaj wszystkich poniższych instrukcji.
  • Alternatywnie możesz otworzyć gałąź dedykowaną migracji do Reacta lub migracji do Vue z repozytorium Frankenstein Demo i śledzić wraz z historią zatwierdzeń.
  • Alternatywnie, możesz się zrelaksować i czytać dalej, ponieważ zamierzam tutaj podkreślić najbardziej krytyczny kod, a o wiele ważniejsze jest zrozumienie mechaniki procesu niż samego kodu.

Przypomnę jeszcze raz, że będziemy ściśle postępować zgodnie z krokami przedstawionymi w teoretycznej pierwszej części artykułu.

Zanurzmy się od razu!

  1. Zidentyfikuj mikroserwisy
  2. Zezwalaj na dostęp z hosta do obcego
  3. Napisz obcą mikrousługę/komponent
  4. Napisz opakowanie komponentów sieci Web wokół usługi obcych
  5. Zastąp usługę hosta komponentem sieciowym
  6. Wypłucz i powtórz dla wszystkich komponentów
  7. Przełącz na obcego

1. Zidentyfikuj mikroserwisy

Jak sugeruje część 1, na tym etapie musimy podzielić naszą aplikację na małe , niezależne usługi dedykowane do jednego konkretnego zadania . Uważny czytelnik może zauważyć, że nasza aplikacja do wykonania jest już mała i niezależna i może samodzielnie reprezentować jedną mikrousługę. Tak sam bym to potraktował, gdyby ta aplikacja żyła w jakimś szerszym kontekście. Pamiętaj jednak, że proces identyfikacji mikroserwisów jest całkowicie subiektywny i nie ma jednej poprawnej odpowiedzi.

Aby więc dokładniej przyjrzeć się procesowi migracji Frankensteina, możemy pójść o krok dalej i podzielić tę aplikację do wykonania na dwie niezależne mikroserwisy:

  1. Pole wejściowe do dodania nowej pozycji.
    Usługa ta może również zawierać nagłówek aplikacji, oparty wyłącznie na pozycjonowaniu bliskości tych elementów.
  2. Lista już dodanych pozycji.
    Ta usługa jest bardziej zaawansowana i wraz z samą listą zawiera również akcje takie jak filtrowanie, akcje elementów listy i tak dalej.
Aplikacja TodoMVC podzielona na dwie niezależne mikroserwisy
Aplikacja TodoMVC podzielona na dwie niezależne mikroserwisy. (duży podgląd)

Wskazówka : aby sprawdzić, czy wybrane usługi są rzeczywiście niezależne, usuń znaczniki HTML reprezentujące każdą z tych usług. Upewnij się, że pozostałe funkcje nadal działają. W naszym przypadku powinno być możliwe dodawanie nowych wpisów do localStorage (które ta aplikacja używa jako magazynu) z pola wejściowego bez listy, podczas gdy lista nadal renderuje wpisy z localStorage , nawet jeśli brakuje pola wejściowego. Jeśli aplikacja zgłasza błędy podczas usuwania znaczników potencjalnego mikrousługi, spójrz na sekcję „Refaktoruj w razie potrzeby” w części 1, aby zapoznać się z przykładem postępowania w takich przypadkach.

Oczywiście moglibyśmy kontynuować i podzielić drugą usługę i listę pozycji jeszcze bardziej na niezależne mikroserwisy dla każdej konkretnej pozycji. Jednak w tym przykładzie może to być zbyt szczegółowe. Na razie wnioskujemy, że nasza aplikacja będzie miała dwie usługi; są niezależni i każdy z nich pracuje nad własnym, konkretnym zadaniem. Dlatego podzieliliśmy naszą aplikację na mikroserwisy .

2. Zezwól na dostęp z hosta do obcego

Pozwólcie, że pokrótce przypomnę, co to jest.

  • Gospodarz
    Tak nazywa się nasza obecna aplikacja. Jest napisany we frameworku, od którego mamy zamiar odejść . W tym konkretnym przypadku nasza aplikacja jQuery.
  • Obcy
    Mówiąc najprościej, jest to stopniowe przepisywanie Hosta na nowy framework, do którego mamy zamiar przejść . Ponownie, w tym konkretnym przypadku jest to aplikacja React lub Vue.

Ogólną zasadą przy dzieleniu Hosta i Obcego jest to, że powinieneś być w stanie opracować i wdrożyć dowolne z nich bez łamania drugiego — w dowolnym momencie.

Utrzymanie niezależności Hosta i Obcego ma kluczowe znaczenie dla Migracji Frankensteina. To jednak sprawia, że ​​zorganizowanie komunikacji między nimi jest nieco trudne. Jak umożliwić hostowi dostęp do Obcego bez ich niszczenia?

Dodawanie obcego jako podmodułu hosta

Chociaż istnieje kilka sposobów na osiągnięcie potrzebnej konfiguracji, najprostszą formą zorganizowania projektu w celu spełnienia tego kryterium są prawdopodobnie podmoduły git. Właśnie tego użyjemy w tym artykule. Pozostawię tobie dokładne przeczytanie o działaniu podmodułów w git, aby zrozumieć ograniczenia i problemy tej struktury.

Ogólne zasady architektury naszego projektu z podmodułami git powinny wyglądać tak:

  • Zarówno Host, jak i Alien są niezależne i przechowywane w oddzielnych repozytoriach git ;
  • Host odwołuje się do Obcego jako podmodułu. Na tym etapie Host wybiera określony stan (zatwierdzenie) Obcego i dodaje go jako, jak wygląda, podfolder w strukturze folderów Hosta.
React TodoMVC dodany jako podmoduł git do aplikacji jQuery TodoMVC
React TodoMVC dodany jako podmoduł git do aplikacji jQuery TodoMVC. (duży podgląd)

Proces dodawania podmodułu jest taki sam dla każdej aplikacji. Nauczanie git submodules wykracza poza zakres tego artykułu i nie jest bezpośrednio związane z samą migracją Frankensteina. Przyjrzyjmy się więc pokrótce możliwym przykładom.

W poniższych fragmentach jako przykładu używamy kierunku React. Dla każdego innego kierunku migracji, zastąp react nazwą gałęzi z Frankenstein TodoMVC lub dostosuj do własnych wartości tam, gdzie jest to potrzebne.

Jeśli postępujesz zgodnie z oryginalną aplikacją jQuery TodoMVC:

 $ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i

Jeśli podążasz wraz z gałęzią migration/jquery-to-react (lub dowolnym innym kierunkiem migracji) z repozytorium Frankenstein Demo, aplikacja Alien powinna już tam być jako git submodule i powinieneś zobaczyć odpowiedni folder. Jednak folder jest domyślnie pusty i należy zaktualizować i zainicjować zarejestrowane moduły podrzędne.

Z katalogu głównego projektu (twojego hosta):

 $ git submodule update --init $ cd react $ npm i

Zauważ, że w obu przypadkach instalujemy zależności dla aplikacji Alien, ale te zostają umieszczone w piaskownicy w podfolderze i nie zanieczyszczają naszego Hosta.

Po dodaniu aplikacji Alien jako podmodułu swojego Hosta, otrzymujesz niezależne (pod względem mikroserwisów) aplikacje Alien i Host. Jednak w tym przypadku Host uważa Obcy za podfolder i oczywiście umożliwia to Hostowi bezproblemowy dostęp do Obcego.

3. Napisz obcą mikrousługę/komponent

Na tym etapie musimy zdecydować, jaki mikroserwis zmigrować jako pierwszy i napisać/używać go po stronie Obcego. Prześledźmy tę samą kolejność usług, którą zidentyfikowaliśmy w kroku 1 i zacznijmy od pierwszego: pola wejściowego do dodania nowego elementu. Zanim jednak zaczniemy, zgódźmy się, że poza tym punktem będziemy używać korzystniejszego terminu komponent zamiast mikroserwisu lub usługi , ponieważ zbliżamy się do założeń frameworków frontendowych, a termin komponent jest zgodny z definicjami praktycznie każdego nowoczesnego struktura.

Gałęzie repozytorium Frankensteina TodoMVC zawierają wynikowy komponent, który reprezentuje pierwszą usługę „Pole wejściowe do dodania nowego elementu” jako komponent nagłówka:

  • Komponent nagłówka w React
  • Komponent nagłówka w Vue

Pisanie komponentów w wybranych przez Ciebie ramach wykracza poza zakres tego artykułu i nie jest częścią Migracji Frankensteina. Jest jednak kilka rzeczy, o których należy pamiętać podczas pisania komponentu Alien.

Niezależność

Po pierwsze, komponenty w Alien powinny działać na tej samej zasadzie niezależności, ustawionej wcześniej po stronie Hosta: komponenty nie powinny w żaden sposób zależeć od innych komponentów.

Interoperacyjność

Dzięki niezależności usług najprawdopodobniej komponenty Twojego Hosta komunikują się w jakiś ugruntowany sposób, czy to poprzez system zarządzania stanem, komunikację poprzez jakąś współdzieloną pamięć masową, czy bezpośrednio poprzez system zdarzeń DOM. „Współdziałanie” komponentów Alien oznacza, że ​​powinny one być w stanie połączyć się z tym samym źródłem komunikacji, ustanowionym przez Hosta, aby wysyłać informacje o zmianach jego stanu i nasłuchiwać zmian w innych komponentach. W praktyce oznacza to, że jeśli komponenty w twoim hoście komunikują się za pośrednictwem zdarzeń DOM, budowanie twojego komponentu Alien wyłącznie z myślą o zarządzaniu stanem nie będzie działać bezbłędnie w przypadku tego typu migracji.

Jako przykład spójrz na plik js/storage.js , który jest głównym kanałem komunikacji dla naszych komponentów jQuery:

 ... fetch: function() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); }, save: function(todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); var event = new CustomEvent("store-update", { detail: { todos } }); document.dispatchEvent(event); }, ...

Tutaj używamy localStorage (ponieważ ten przykład nie jest krytyczny dla bezpieczeństwa) do przechowywania naszych elementów do wykonania, a gdy zmiany w pamięci zostaną zarejestrowane, wysyłamy niestandardowe zdarzenie DOM na elemencie document , którego może nasłuchiwać dowolny komponent.

Jednocześnie po stronie Obcego (powiedzmy Reacta) możemy ustawić dowolną złożoną komunikację zarządzania stanem. Jednak prawdopodobnie rozsądnie jest zachować to na przyszłość: aby pomyślnie zintegrować nasz komponent Alien React z Hostem, musimy połączyć się z tym samym kanałem komunikacyjnym, którego używa Host. W tym przypadku jest to localStorage . Aby uprościć sprawę, po prostu skopiowaliśmy plik pamięci hosta do Alien i podłączyliśmy do niego nasze komponenty:

 import todoStorage from "../storage"; class Header extends Component { constructor(props) { this.state = { todos: todoStorage.fetch() }; } componentDidMount() { document.addEventListener("store-update", this.updateTodos); } componentWillUnmount() { document.removeEventListener("store-update", this.updateTodos); } componentDidUpdate(prevProps, prevState) { if (prevState.todos !== this.state.todos) { todoStorage.save(this.state.todos); } } ... }

Teraz nasze komponenty Alien mogą mówić tym samym językiem co komponenty Host i na odwrót.

4. Napisz opakowanie komponentów sieci Web wokół usługi obcych

Mimo że jesteśmy dopiero na czwartym kroku, osiągnęliśmy całkiem sporo:

  • Podzieliliśmy naszą aplikację Host na niezależne usługi, które są gotowe do zastąpienia przez usługi Alien;
  • Ustawiliśmy Host i Alien tak, aby były całkowicie niezależne od siebie, ale bardzo dobrze połączone za pomocą git submodules ;
  • Napisaliśmy nasz pierwszy komponent Alien, używając nowego frameworka.

Teraz nadszedł czas na utworzenie pomostu między Hostem a Obcym, aby nowy komponent Obcy mógł działać w Hostie.

Przypomnienie z części 1 : Upewnij się, że Twój Host ma dostępny pakiet pakietów. W tym artykule polegamy na Webpack, ale nie oznacza to, że technika ta nie będzie działać z Rollupem lub innym wybranym pakietem. Mapowanie z pakietu Webpack pozostawiam jednak waszym eksperymentom.

Konwencja nazewnictwa

Jak wspomniano w poprzednim artykule, do integracji Alien z Hostem użyjemy Web Components. Po stronie Hosta tworzymy nowy plik: js/frankenstein-wrappers/Header-wrapper.js . (Będzie to nasz pierwszy wrapper Frankensteina.) Pamiętaj, że dobrym pomysłem jest nazwanie swoich wrapperów tak samo jak komponenty w aplikacji Alien, np. po prostu dodając przyrostek „ -wrapper ”. Później zobaczysz, dlaczego jest to dobry pomysł, ale na razie zgódźmy się, że oznacza to, że jeśli komponent Alien nazywa się Header.js (w React) lub Header.vue (w Vue), odpowiednie opakowanie na Strona hosta powinna mieć nazwę Header-wrapper.js .

W naszym pierwszym wrapperu zaczynamy od podstawowego boilerplate’u do rejestracji niestandardowego elementu:

 class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);

Następnie musimy zainicjować Shadow DOM dla tego elementu.

Zapoznaj się z Częścią 1, aby dowiedzieć się, dlaczego używamy Shadow DOM.

 class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }

Dzięki temu mamy skonfigurowane wszystkie niezbędne elementy komponentu sieciowego i nadszedł czas, aby dodać nasz komponent Alien do miksu. Przede wszystkim na początku naszego wrappera Frankensteina powinniśmy zaimportować wszystkie bity odpowiedzialne za renderowanie komponentu Alien.

 import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...

Tutaj musimy się na chwilę zatrzymać. Zauważ, że nie importujemy zależności Alien z node_modules hosta. Wszystko pochodzi od samego Obcego, który znajduje się w podfolderze React react/ . Dlatego krok 2 jest tak ważny i kluczowe jest upewnienie się, że Host ma pełny dostęp do zasobów Obcego.

Teraz możemy renderować nasz komponent Alien w ramach Shadow DOM komponentu Web Component:

 ... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...

Uwaga : w tym przypadku React nie potrzebuje niczego więcej. Jednak, aby wyrenderować komponent Vue, musisz dodać węzeł zawijania, który będzie zawierał komponent Vue, jak poniżej:

 ... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...

Powodem tego jest różnica w sposobie renderowania komponentów React i Vue: React dołącza komponent do przywoływanego węzła DOM, podczas gdy Vue zastępuje przywoływany węzeł DOM komponentem. W związku z tym, jeśli .$mount(this.shadowRoot) dla Vue, zasadniczo zastąpi to Shadow DOM.

To wszystko, co na razie musimy zrobić z naszym opakowaniem. Aktualny wynik dla wrappera Frankensteina w obu kierunkach migracji jQuery-to-React i jQuery-to-Vue można znaleźć tutaj:

  • Frankenstein Wrapper dla komponentu React
  • Frankenstein Wrapper dla komponentu Vue

Podsumowując mechanikę wrappera Frankensteina:

  1. Utwórz niestandardowy element,
  2. Zainicjuj Shadow DOM,
  3. Zaimportuj wszystko, co jest potrzebne do renderowania komponentu Obcego,
  4. Renderuj komponent Obcy w obszarze Shadow DOM elementu niestandardowego.

Nie oznacza to jednak, że nasz Obcy w Hostie jest automatycznie renderowany. Musimy zastąpić istniejący znacznik hosta naszym nowym opakowaniem Frankensteina.

Zapnij pasy, może nie być tak proste, jak można by się spodziewać!

5. Zastąp usługę hosta komponentem sieciowym

Przejdźmy dalej i dodajmy nasz nowy plik Header-wrapper.js do index.html i zastąpmy istniejący znacznik nagłówka nowo utworzonym elementem niestandardowym <frankenstein-header-wrapper> .

 ... <!-- <header class="header">--> <!-- <h1>todos</h1>--> <!-- <input class="new-todo" placeholder="What needs to be done?" autofocus>--> <!-- </header>--> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script type="module" src="js/frankenstein-wrappers/Header-wrapper.js"></script>

Niestety, to nie będzie tak proste. Jeśli otworzysz przeglądarkę i sprawdzisz konsolę, czeka na Ciebie Uncaught SyntaxError . W zależności od przeglądarki i obsługi modułów ES6 będzie to związane z importem ES6 lub sposobem renderowania komponentu Alien. Tak czy inaczej, musimy coś z tym zrobić, ale problem i rozwiązanie powinny być znajome i jasne dla większości czytelników.

5.1. W razie potrzeby zaktualizuj Webpack i Babel

Powinniśmy włączyć trochę magii Webpack i Babel przed integracją naszego opakowania Frankensteina. Walka z tymi narzędziami wykracza poza zakres tego artykułu, ale możesz spojrzeć na odpowiadające im zmiany w repozytorium Frankenstein Demo:

  • Konfiguracja do migracji do React
  • Konfiguracja do migracji do Vue

Zasadniczo ustawiliśmy przetwarzanie plików, a także nowy punkt wejścia frankenstein w konfiguracji Webpacka, aby zawierał wszystko, co dotyczy opakowań Frankensteina w jednym miejscu.

Gdy Webpack w Host wie, jak przetwarzać komponent Alien i komponenty sieciowe, jesteśmy gotowi do zastąpienia znaczników Hosta nowym opakowaniem Frankensteina.

5.2. Rzeczywista wymiana komponentu

Wymiana komponentu powinna być teraz prosta. W index.html hosta wykonaj następujące czynności:

  1. Zamień <header class="header"> element DOM na <frankenstein-header-wrapper> ;
  2. Dodaj nowy skrypt frankenstein.js . To jest nowy punkt wejścia w Webpack, który zawiera wszystko, co dotyczy opakowań Frankensteina.
 ... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>

Otóż ​​to! W razie potrzeby uruchom ponownie serwer i zobacz magię komponentu Alien zintegrowanego z Hostem.

Jednak czegoś wciąż brakuje. Składnik Alien w kontekście Host nie wygląda tak samo, jak w kontekście samodzielnej aplikacji Alien. Jest po prostu niestylizowany.

Unstyled Alien React po zintegrowaniu z Hostem
Unstyled Alien React komponent po zintegrowaniu z Hostem (duży podgląd)

Dlaczego tak jest? Czy style składnika nie powinny być automatycznie integrowane ze składnikiem Obcy w programie Host? Chciałbym, żeby tak było, ale jak w zbyt wielu sytuacjach, to zależy. Dochodzimy do wymagającej części Migracji Frankensteina.

5.3. Ogólne informacje na temat stylizacji obcego komponentu

Przede wszystkim ironia polega na tym, że nie ma błędu w sposobie działania rzeczy. Wszystko jest tak, jak zostało zaprojektowane do pracy. Aby to wyjaśnić, pokrótce wspomnijmy o różnych sposobach stylizacji komponentów.

Style globalne

Wszyscy je znamy: style globalne mogą być (i zwykle są) dystrybuowane bez żadnego konkretnego komponentu i stosowane do całej strony. Style globalne wpływają na wszystkie węzły DOM z pasującymi selektorami.

Kilka przykładów stylów globalnych to tagi <style> i <link rel="stylesheet"> znalezione w index.html . Alternatywnie, globalny arkusz stylów można zaimportować do jakiegoś głównego modułu JS, aby wszystkie komponenty również miały do ​​niego dostęp.

Problem stylizowania aplikacji w ten sposób jest oczywisty: utrzymanie monolitycznych arkuszy stylów dla dużych aplikacji staje się bardzo trudne. Ponadto, jak widzieliśmy w poprzednim artykule, style globalne mogą łatwo zepsuć komponenty, które są renderowane bezpośrednio w głównym drzewie DOM, tak jak w React lub Vue.

Dołączone style

Te style są zwykle ściśle powiązane z samym składnikiem i rzadko są dystrybuowane bez tego składnika. Style zazwyczaj znajdują się w tym samym pliku co komponent. Dobrymi przykładami tego typu stylizacji są styled-components w modułach React lub CSS oraz Scoped CSS w pojedynczych komponentach plikowych w Vue. Jednak bez względu na różnorodność narzędzi do pisania stylów w pakiecie, zasadnicza zasada w większości z nich jest taka sama: narzędzia zapewniają mechanizm określania zakresu, który blokuje style zdefiniowane w komponencie, tak aby style nie łamały innych komponentów ani nie były globalne. style.

Dlaczego style ograniczone mogą być kruche?

W części 1, uzasadniając użycie Shadow DOM w migracji Frankensteina, pokrótce omówiliśmy temat scopingu a enkapsulacji) oraz tego, czym enkapsulacja Shadow DOM różni się od narzędzi do stylizacji zakresu. Nie wyjaśniliśmy jednak, dlaczego narzędzia do określania zakresu zapewniają tak delikatną stylizację naszym komponentom, a teraz, gdy zmierzyliśmy się z niestylizowanym komponentem Obcego, staje się on niezbędny do zrozumienia.

Wszystkie narzędzia do określania zakresu dla nowoczesnych frameworków działają podobnie:

  • Piszesz style dla swojego komponentu w jakiś sposób, nie zastanawiając się zbytnio nad zakresem lub enkapsulacją;
  • Uruchamiasz swoje komponenty z zaimportowanymi/osadzonymi arkuszami stylów przez jakiś system wiązania, taki jak Webpack lub Rollup;
  • Bundler generuje unikalne klasy CSS lub inne atrybuty, tworząc i wstrzykując indywidualne selektory zarówno dla Twojego HTML, jak i odpowiednich arkuszy stylów;
  • Bundler tworzy wpis <style> w <head> twojego dokumentu i umieszcza tam style twoich komponentów z unikalnymi mieszanymi selektorami.

To prawie wszystko. Działa i działa dobrze w wielu przypadkach. Z wyjątkiem sytuacji, gdy tak nie jest: kiedy style dla wszystkich komponentów żyją w globalnym zakresie stylizacji, łatwo jest je przełamać, np. stosując wyższą szczegółowość. To wyjaśnia potencjalną kruchość narzędzi do określania zakresu, ale dlaczego nasz komponent Obcy jest całkowicie pozbawiony stylu?

Rzućmy okiem na obecnego Hosta za pomocą DevTools. Na przykład podczas sprawdzania nowo dodanego opakowania Frankensteina z komponentem Alien React, możemy zobaczyć coś takiego:

Opakowanie Frankensteina z komponentem Alien w środku. Zwróć uwagę na unikalne klasy CSS w węzłach Obcego.
Opakowanie Frankensteina z komponentem Alien w środku. Zwróć uwagę na unikalne klasy CSS w węzłach Obcego. (duży podgląd)

Tak więc Webpack generuje unikalne klasy CSS dla naszego komponentu. Świetnie! Gdzie zatem są style? Cóż, style są dokładnie tam, gdzie zostały zaprojektowane — w <head> dokumentu.

Podczas gdy komponent Alien znajduje się w opakowaniu Frankensteina, jego style znajdują się w głowie dokumentu.
Podczas gdy komponent Alien znajduje się w opakowaniu Frankensteina, jego style znajdują się w <head> dokumentu. (duży podgląd)

Czyli wszystko działa jak należy i to jest główny problem. Ponieważ nasz komponent Alien znajduje się w Shadow DOM i jak wyjaśniono w części 1, Shadow DOM zapewnia pełną enkapsulację komponentów z reszty strony i stylów globalnych, w tym nowo wygenerowanych arkuszy stylów dla komponentu, który nie może przekroczyć granicy cienia i dostać się do komponentu Obcy. W związku z tym składnik Obcy pozostaje bez stylu. Jednak teraz taktyka rozwiązania problemu powinna być jasna: powinniśmy w jakiś sposób umieścić style komponentu w tym samym Shadow DOM, w którym znajduje się nasz komponent (zamiast <head> ).

5.4. Style mocowania obcego komponentu

Do tej pory proces migracji do dowolnego frameworka był taki sam. Jednak tutaj sprawy zaczynają się rozchodzić: każdy framework ma swoje zalecenia dotyczące stylizacji komponentów, a co za tym idzie, różne są sposoby radzenia sobie z problemem. Tutaj omawiamy najczęstsze przypadki, ale jeśli framework, z którym pracujesz, używa jakiegoś unikalnego sposobu stylizowania komponentów, musisz pamiętać o podstawowych taktykach, takich jak umieszczanie stylów komponentu w Shadow DOM zamiast <head> .

W tym rozdziale omówimy poprawki dla:

  • Dołączone style z modułami CSS w Vue (taktyka dla Scoped CSS jest taka sama);
  • Dołączone style ze styled-components w React;
  • Ogólne moduły CSS i style globalne. Łączę je, ponieważ ogólnie moduły CSS są bardzo podobne do globalnych arkuszy stylów i mogą być importowane przez dowolny komponent, co powoduje, że style są odłączone od konkretnego komponentu.

Ograniczenia po pierwsze: wszystko, co robimy, aby naprawić stylizację, nie powinno psuć samego komponentu Alien . W przeciwnym razie tracimy niezależność naszych systemów Obcych i Hosta. Tak więc, aby rozwiązać problem stylizacji, będziemy polegać na konfiguracji bundlera lub wrappera Frankensteina.

Dołączone style w Vue i Shadow DOM

Jeśli piszesz aplikację Vue, najprawdopodobniej używasz komponentów z jednym plikiem. Jeśli używasz również Webpacka, powinieneś znać dwa programy vue-loader i vue-style-loader . Pierwsza pozwala na zapisanie tych pojedynczych komponentów pliku, podczas gdy druga dynamicznie wstrzykuje CSS komponentu do dokumentu jako znacznik <style> . Domyślnie vue-style-loader wstrzykuje style komponentu do <head> dokumentu. Jednak oba pakiety akceptują w konfiguracji opcję shadowMode , która pozwala nam łatwo zmienić domyślne zachowanie i wstrzykiwać style (jak sugeruje nazwa opcji) do Shadow DOM. Zobaczmy to w akcji.

Konfiguracja pakietu internetowego

Jako minimum plik konfiguracyjny Webpack powinien zawierać następujące elementy:

 const VueLoaderPlugin = require('vue-loader/lib/plugin'); ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { shadowMode: true } }, { test: /\.css$/, include: path.resolve(__dirname, '../vue'), use: [ { loader:'vue-style-loader', options: { shadowMode: true } }, 'css-loader' ] } ], plugins: [ new VueLoaderPlugin() ] }

W prawdziwej aplikacji, twój blok test: /\.css$/ będzie bardziej wyrafinowany (prawdopodobnie będzie zawierał regułę oneOf ), aby uwzględnić zarówno konfiguracje Hosta, jak i Obcego. Jednak w tym przypadku nasz jQuery jest stylizowany za pomocą prostego <link rel="stylesheet"> w index.html , więc nie budujemy stylów dla Hosta za pośrednictwem Webpack i można bezpiecznie obsługiwać tylko Alien.

Konfiguracja owijarki

Oprócz konfiguracji Webpacka musimy również zaktualizować nasz wrapper Frankensteina, wskazując Vue na poprawny Shadow DOM. W naszym Header-wrapper.js renderowanie komponentu Vue powinno zawierać właściwość shadowRoot prowadzącą do shadowRoot naszego wrappera Frankensteina:

 ... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...

Po zaktualizowaniu plików i ponownym uruchomieniu serwera, powinieneś otrzymać coś takiego w swoim DevTools:

Style dołączone do komponentu Alien Vue umieszczonego w opakowaniu Frankensteina z zachowanymi wszystkimi unikalnymi klasami CSS.
Style dołączone do komponentu Alien Vue umieszczonego w opakowaniu Frankensteina z zachowanymi wszystkimi unikalnymi klasami CSS. (duży podgląd)

Wreszcie, style komponentu Vue znajdują się w naszym Shadow DOM. Jednocześnie Twoja aplikacja powinna wyglądać tak:

Komponent nagłówka zaczyna wyglądać bardziej tak, jak powinien. Jednak wciąż czegoś brakuje.
Komponent nagłówka zaczyna wyglądać bardziej tak, jak powinien. Jednak wciąż czegoś brakuje. (duży podgląd)

Zaczynamy uzyskiwać coś, co przypomina naszą aplikację Vue: style dołączone do komponentu są wstrzykiwane do Shadow DOM wrappera, ale komponent nadal nie wygląda tak, jak powinien. Powodem jest to, że w oryginalnej aplikacji Vue komponent jest stylizowany nie tylko za pomocą dołączonych stylów, ale także częściowo za pomocą stylów globalnych. Jednak przed naprawieniem globalnych stylów musimy doprowadzić naszą integrację React do tego samego stanu, co Vue.

Dołączone style w React i Shadow DOM

Ponieważ istnieje wiele sposobów stylizowania komponentu React, konkretne rozwiązanie naprawy komponentu Obcego w Migracji Frankensteina zależy od tego, w jaki sposób stylizujemy komponent. Omówmy pokrótce najczęściej używane alternatywy.

styled-components

styled-components to jeden z najpopularniejszych sposobów stylizowania komponentów React. W przypadku komponentu Header React styled-components są dokładnie takie, jak je stylizujemy. Ponieważ jest to klasyczne podejście CSS-in-JS, nie ma pliku z dedykowanym rozszerzeniem, do którego moglibyśmy podłączyć nasz bundler, tak jak robimy to w przypadku plików .css lub .js . Na szczęście styled-components umożliwiają wstrzykiwanie stylów komponentu do węzła niestandardowego (w naszym przypadku Shadow DOM) zamiast head dokumentu za pomocą komponentu pomocniczego StyleSheetManager . Jest to wstępnie zdefiniowany komponent, instalowany z pakietem styled-components , który akceptuje właściwość target , definiując „alternatywny węzeł DOM do wstrzykiwania informacji o stylach”. Dokładnie to, czego potrzebujemy! Co więcej, nie musimy nawet zmieniać konfiguracji naszego Webpacka: wszystko zależy od naszego wrappera Frankensteina.

Powinniśmy zaktualizować nasz Header-wrapper.js zawierający komponent React Alien o następujące wiersze:

 ... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...

Tutaj importujemy komponent StyleSheetManager (z Alien, a nie z Hosta) i opakowujemy nim nasz komponent React. Jednocześnie wysyłamy właściwość target wskazującą na nasz shadowRoot . Otóż ​​to. Jeśli zrestartujesz serwer, musisz zobaczyć coś takiego w swoich DevTools:

Style w pakiecie z komponentem React Alien umieszczonym w opakowaniu Frankensteina z zachowaniem wszystkich unikalnych klas CSS.
Style dołączone do komponentu React Alien umieszczonego w opakowaniu Frankensteina z zachowanymi wszystkimi unikalnymi klasami CSS. (duży podgląd)

Teraz style naszego komponentu są w Shadow DOM zamiast <head> . W ten sposób renderowanie naszej aplikacji przypomina teraz to, co widzieliśmy wcześniej w aplikacji Vue.

Po przeniesieniu dołączonych stylów do opakowania Frankensteina komponent Alien React zaczyna wyglądać lepiej. Jednak jeszcze nas tam nie ma.
Po przeniesieniu dołączonych stylów do opakowania Frankensteina komponent Alien React zaczyna wyglądać lepiej. Jednak jeszcze nas tam nie ma. (duży podgląd)

Ta sama historia: styled-components są odpowiedzialne tylko za połączoną część style komponentu React , a style globalne zarządzają pozostałymi bitami. Wracamy do stylów globalnych za chwilę po przyjrzeniu się jeszcze jednemu typowi elementów stylizacji.

Moduły CSS

Jeśli przyjrzysz się bliżej komponentowi Vue, który naprawiliśmy wcześniej, możesz zauważyć, że moduły CSS to dokładnie sposób, w jaki stylizujemy ten komponent. However, even if we style it with Scoped CSS (another recommended way of styling Vue components) the way we fix our unstyled component doesn't change: it is still up to vue-loader and vue-style-loader to handle it through shadowMode: true option.

When it comes to CSS Modules in React (or any other system using CSS Modules without any dedicated tools), things get a bit more complicated and less flexible, unfortunately.

Let's take a look at the same React component which we've just integrated, but this time styled with CSS Modules instead of styled-components. The main thing to note in this component is a separate import for stylesheet:

 import styles from './Header.module.css'

The .module.css extension is a standard way to tell React applications built with the create-react-app utility that the imported stylesheet is a CSS Module. The stylesheet itself is very basic and does precisely the same our styled-components do.

Integrating CSS modules into a Frankenstein wrapper consists of two parts:

  • Enabling CSS Modules in bundler,
  • Pushing resulting stylesheet into Shadow DOM.

I believe the first point is trivial: all you need to do is set { modules: true } for css-loader in your Webpack configuration. Since, in this particular case, we have a dedicated extension for our CSS Modules ( .module.css ), we can have a dedicated configuration block for it under the general .css configuration:

 { test: /\.css$/, oneOf: [ { test: /\.module\.css$/, use: [ ... { loader: 'css-loader', options: { modules: true, } } ] } ] }

Note : A modules option for css-loader is all we have to know about CSS Modules no matter whether it's React or any other system. When it comes to pushing resulting stylesheet into Shadow DOM, however, CSS Modules are no different from any other global stylesheet.

By now, we went through the ways of integrating bundled styles into Shadow DOM for the following conventional scenarios:

  • Vue components, styled with CSS Modules. Dealing with Scoped CSS in Vue components won't be any different;
  • React components, styled with styled-components;
  • Components styled with raw CSS Modules (without dedicated tools like those in Vue). For these, we have enabled support for CSS modules in Webpack configuration.

However, our components still don't look as they are supposed to because their styles partially come from global styles . Those global styles do not come to our Frankenstein wrappers automatically. Moreover, you might get into a situation in which your Alien components are styled exclusively with global styles without any bundled styles whatsoever. So let's finally fix this side of the story.

Global Styles And Shadow DOM

Having your components styled with global styles is neither wrong nor bad per se: every project has its requirements and limitations. However, the best you can do for your components if they rely on some global styles is to pull those styles into the component itself. This way, you have proper easy-to-maintain self-contained components with bundled styles.

Nevertheless, it's not always possible or reasonable to do so: several components might share some styling, or your whole styling architecture could be built using global stylesheets that are split into the modular structure, and so on.

So having an opportunity to pull in global styles into our Frankenstein wrappers wherever it's required is essential for the success of this type of migration. Before we get to an example, keep in mind that this part is the same for pretty much any framework of your choice — be it React, Vue or anything else using global stylesheets!

Let's get back to our Header component from the Vue application. Take a look at this import:

 import "todomvc-app-css/index.css";

This import is where we pull in the global stylesheet. In this case, we do it from the component itself. It's only one way of using global stylesheet to style your component, but it's not necessarily like this in your application.

Some parent module might add a global stylesheet like in our React application where we import index.css only in index.js , and then our components expect it to be available in the global scope. Your component's styling might even rely on a stylesheet, added with <style> or <link> to your index.html . It doesn't matter. What matters, however, is that you should expect to either import global stylesheets in your Alien component (if it doesn't harm the Alien application) or explicitly in the Frankenstein wrapper. Otherwise, the wrapper would not know that the Alien component needs any stylesheet other than the ones already bundled with it.

Caution . If there are many global stylesheets to be shared between Alien components and you have a lot of such components, this might harm the performance of your Host application under the migration period.

Here is how import of a global stylesheet, required for the Header component, is done in Frankenstein wrapper for React component:

 // we import directly from react/, not from Host import '../../react/node_modules/todomvc-app-css/index.css'

Nevertheless, by importing a stylesheet this way, we still bring the styles to the global scope of our Host, while what we need is to pull in the styles into our Shadow DOM. Jak to robimy?

Webpack configuration for global stylesheets & Shadow DOM

First of all, you might want to add an explicit test to make sure that we process only the stylesheets coming from our Alien. In case of our React migration, it will look similar to this:

 test: /\.css$/, oneOf: [ // this matches stylesheets coming from /react/ subfolder { test: /\/react\//, use: [] }, ... ]

In case of Vue application, obviously, you change test: /\/react\// with something like test: /\/vue\// . Apart from that, the configuration will be the same for any framework. Next, let's specify the required loaders for this block.

 ... use: [ { loader: 'style-loader', options: { ... } }, 'css-loader' ]

Two things to note. First, you have to specify modules: true in css-loader 's configuration if you're processing CSS Modules of your Alien application.

Second, we should convert styles into <style> tag before injecting those into Shadow DOM. In the case of Webpack, for that, we use style-loader . The default behavior for this loader is to insert styles into the document's head. Typically. And this is precisely what we don't want: our goal is to get stylesheets into Shadow DOM. However, in the same way we used target property for styled-components in React or shadowMode option for Vue components that allowed us to specify custom insertion point for our <style> tags, regular style-loader provides us with nearly same functionality for any stylesheet: the insert configuration option is exactly what helps us achieve our primary goal. Dobre wieści! Let's add it to our configuration.

 ... { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }

However, not everything is so smooth here with a couple of things to keep in mind.

Globalne arkusze stylów i opcja insert w style-loader

Jeśli sprawdzisz dokumentację tej opcji, zauważysz, że ta opcja wymaga jednego selektora na konfigurację. Oznacza to, że jeśli masz kilka komponentów Obcych wymagających globalnych stylów wciągniętych do opakowania Frankensteina, musisz określić style-loader dla każdego opakowania Frankensteina. W praktyce oznacza to, że prawdopodobnie będziesz musiał polegać na regule oneOf w bloku konfiguracyjnym, aby obsłużyć wszystkie opakowania.

 { test: /\/react\//, oneOf: [ { test: /1-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '1-frankenstein-wrapper' } }, `css-loader` ] }, { test: /2-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '2-frankenstein-wrapper' } }, `css-loader` ] }, // etc. ], }

Niezbyt elastyczny, zgadzam się. Niemniej jednak nie jest to wielka sprawa, o ile nie masz setek komponentów do migracji. W przeciwnym razie może to sprawić, że konfiguracja pakietu Webpack będzie trudna do utrzymania. Prawdziwym problemem jest jednak to, że nie możemy napisać selektora CSS dla Shadow DOM.

Próbując rozwiązać ten problem, możemy zauważyć, że opcja insert może również przyjmować funkcję zamiast zwykłego selektora, aby określić bardziej zaawansowaną logikę wstawiania. Dzięki temu możemy użyć tej opcji, aby wstawić arkusze stylów bezpośrednio do Shadow DOM! W uproszczonej formie może to wyglądać podobnie do tego:

 insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }

Kuszące, prawda? Jednak to nie zadziała w naszym scenariuszu lub będzie działać daleko od optymalnego. Nasz <frankenstein-header-wrapper> jest rzeczywiście dostępny z index.html (ponieważ dodaliśmy go w kroku 5.2). Ale kiedy Webpack przetwarza wszystkie zależności (w tym arkusze stylów) dla komponentu Alien lub wrappera Frankensteina, Shadow DOM nie jest jeszcze inicjowany w wrapperze Frankensteina: importy są przetwarzane przed tym. Dlatego skierowanie insert bezpośrednio do shadowRoot spowoduje błąd.

Jest tylko jeden przypadek, w którym możemy zagwarantować, że Shadow DOM zostanie zainicjowany zanim Webpack przetworzy naszą zależność arkusza stylów. Jeśli komponent Alien nie importuje samego arkusza stylów, a jego zaimportowanie jest uzależnione od opakowania Frankensteina, możemy zastosować import dynamiczny i zaimportować wymagany arkusz stylów po skonfigurowaniu Shadow DOM:

 this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');

To zadziała: taki import, w połączeniu z powyższą konfiguracją insert , rzeczywiście znajdzie poprawny Shadow DOM i wstawi do niego tag <style> . Niemniej jednak, pobranie i przetworzenie arkusza stylów zajmie trochę czasu, co oznacza, że ​​użytkownicy korzystający z wolnego połączenia lub wolnych urządzeń mogą napotkać na moment komponentu unstyled, zanim arkusz stylów znajdzie się na swoim miejscu w Shadow DOM wrappera.

Komponent Unstyled Alien jest renderowany przed zaimportowaniem globalnego arkusza stylów i dodaniem go do Shadow DOM.
Komponent Unstyled Alien jest renderowany przed zaimportowaniem globalnego arkusza stylów i dodaniem go do Shadow DOM. (duży podgląd)

Podsumowując, mimo że insert akceptuje funkcję, niestety to nam nie wystarcza i musimy sięgnąć do zwykłych selektorów CSS, takich jak frankenstein-header-wrapper . Nie powoduje to jednak automatycznego umieszczenia arkuszy stylów w Shadow DOM, a arkusze stylów znajdują się w <frankenstein-header-wrapper> poza Shadow DOM.

style-loader umieszcza importowany arkusz stylów w wrapperze Frankensteina, ale poza Shadow DOM.
style-loader umieszcza importowany arkusz stylów w wrapperze Frankensteina, ale poza Shadow DOM. (duży podgląd)

Potrzebujemy jeszcze jednego kawałka układanki.

Konfiguracja wrappera dla globalnych arkuszy stylów i Shadow DOM

Na szczęście poprawka jest dość prosta po stronie wrappera: kiedy Shadow DOM zostanie zainicjowany, musimy sprawdzić, czy w bieżącym opakowaniu znajdują się wszystkie oczekujące arkusze stylów i przeciągnąć je do Shadow DOM.

Aktualny stan importu globalnego arkusza stylów jest następujący:

  • Importujemy arkusz stylów, który należy dodać do Shadow DOM. Arkusz stylów można zaimportować w samym komponencie Alien lub wyraźnie w opakowaniu Frankensteina. Na przykład w przypadku migracji do Reacta import jest inicjowany z opakowania. Jednak podczas migracji do Vue podobny komponent sam importuje wymagany arkusz stylów i nie musimy importować niczego w opakowaniu.
  • Jak wskazano powyżej, kiedy Webpack przetwarza .css dla komponentu Alien, dzięki opcji insert w style-loader , arkusze stylów są wstrzykiwane do opakowania Frankensteina, ale poza Shadow DOM.

Uproszczona inicjalizacja Shadow DOM w wrapperze Frankensteina powinna obecnie (zanim wciągniemy jakiekolwiek arkusze stylów) wyglądać podobnie do tego:

 this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`

Teraz, aby uniknąć migotania komponentu unstyled, musimy teraz pobrać wszystkie wymagane arkusze stylów po zainicjowaniu Shadow DOM, ale przed renderowaniem komponentu Alien.

 this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})

To było długie wyjaśnienie z wieloma szczegółami, ale przede wszystkim wszystko, czego potrzeba, aby wciągnąć globalne arkusze stylów do Shadow DOM:

  • W konfiguracji Webpack dodaj style-loader z opcją insert wskazującą na wymagany wrapper Frankensteina.
  • W samym wrapperze wciągnij „oczekujące” arkusze stylów po zainicjowaniu Shadow DOM, ale przed renderowaniem komponentu Alien.

Po wdrożeniu tych zmian Twój komponent powinien mieć wszystko, czego potrzebuje. Jedyne, co możesz chcieć (nie jest to wymagane), aby dodać, to jakiś niestandardowy CSS w celu dostrojenia komponentu Alien w środowisku hosta. Możesz nawet całkowicie zmienić styl swojego komponentu Alien, gdy jest używany w Host. Wykracza poza główny punkt artykułu, ale przyglądasz się końcowemu kodowi wrappera, w którym znajdziesz przykłady, jak przesłonić proste style na poziomie wrappera.

  • Opakowanie Frankensteina dla komponentu React
  • Opakowanie Frankensteina dla komponentu Vue

Możesz również przyjrzeć się konfiguracji Webpacka na tym etapie migracji:

  • Migracja do React ze styled-components
  • Migracja do React z modułami CSS
  • Migracja do Vue

I wreszcie, nasze komponenty wyglądają dokładnie tak, jak chcieliśmy, aby wyglądały.

Wynik migracji komponentu Header napisanego za pomocą Vue i React. Lista rzeczy do zrobienia to nadal aplikacja jQuery.
Wynik migracji komponentu Header napisanego za pomocą Vue i React. Lista rzeczy do zrobienia to nadal aplikacja jQuery. (duży podgląd)

5.5. Podsumowanie stylów mocowania dla komponentu Alien

To świetny moment na podsumowanie tego, czego do tej pory dowiedzieliśmy się w tym rozdziale. Mogłoby się wydawać, że musieliśmy wykonać ogromną pracę, aby naprawić styl komponentu Alien; jednak wszystko sprowadza się do:

  • Naprawianie powiązanych stylów zaimplementowanych za pomocą styled-components w modułach React lub CSS oraz Scoped CSS w Vue jest tak proste, jak kilka linii w wrapperze Frankensteina lub konfiguracji Webpack.
  • Naprawianie stylów, zaimplementowane za pomocą modułów CSS, zaczyna się od zaledwie jednej linii w konfiguracji css-loader . Następnie moduły CSS są traktowane jako globalny arkusz stylów.
  • Naprawienie globalnych arkuszy stylów wymaga skonfigurowania pakietu style-loader z opcją insert w Webpack i aktualizacji wrappera Frankensteina, aby ściągał arkusze stylów do Shadow DOM w odpowiednim momencie cyklu życia wrappera.

W końcu mamy odpowiednio wystylizowany komponent Alien przeniesiony do Hosta. Jest jednak tylko jedna rzecz, która może Cię niepokoić lub nie, w zależności od tego, do jakiego frameworka migrujesz.

Najpierw dobre wieści: jeśli przeprowadzasz migrację do Vue , demo powinno działać dobrze i powinieneś być w stanie dodać nowe rzeczy do zrobienia z migrowanego komponentu Vue. Jeśli jednak przeprowadzasz migrację do React i próbujesz dodać nowy element do zrobienia, nie powiedzie się. Dodawanie nowych pozycji po prostu nie działa i żadne wpisy nie są dodawane do listy. Ale dlaczego? Jaki jest problem? Bez uprzedzeń, ale React ma własne zdanie na niektóre rzeczy.

5.6. Wydarzenia React i JS w Shadow DOM

Bez względu na to, co mówi dokumentacja Reacta, React nie jest zbyt przyjazny dla komponentów sieciowych. Prostota przykładu w dokumentacji nie podlega żadnej krytyce, a wszystko, co jest bardziej skomplikowane niż renderowanie łącza w komponencie sieciowym, wymaga pewnych badań i badań.

Jak widzieliście podczas naprawiania stylizacji dla naszego komponentu Alien, w przeciwieństwie do Vue, gdzie elementy pasują do komponentów sieciowych prawie po wyjęciu z pudełka, React nie jest tak gotowy do komponentów sieciowych. Na razie wiemy, jak sprawić, by komponenty React przynajmniej wyglądały dobrze w komponentach internetowych, ale jest też funkcjonalność i zdarzenia JavaScript do naprawienia.

Krótko mówiąc: Shadow DOM hermetyzuje zdarzenia i przekierowuje je, podczas gdy React nie obsługuje natywnie tego zachowania Shadow DOM , a zatem nie wychwytuje zdarzeń pochodzących z Shadow DOM. Istnieją głębsze powody takiego zachowania, a nawet jeśli chcesz zagłębić się w więcej szczegółów i dyskusji, w narzędziu do śledzenia błędów Reacta jest otwarty problem.

Na szczęście mądrzy ludzie przygotowali dla nas rozwiązanie. @josephnvu dostarczył podstawy rozwiązania, a Lukas Bombach przekształcił je w moduł npm react-shadow-dom-retarget-events . Możesz więc zainstalować pakiet, postępować zgodnie z instrukcjami na stronie pakietów, zaktualizować kod opakowania, a komponent Alien zacznie magicznie działać:

 import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);

Jeśli chcesz, aby był bardziej wydajny, możesz wykonać lokalną kopię pakietu (licencja MIT na to pozwala) i ograniczyć liczbę zdarzeń do odsłuchania, tak jak to się dzieje w repozytorium Frankenstein Demo. W tym przykładzie wiem, jakie zdarzenia muszę przekierować i określić tylko te.

Dzięki temu w końcu (wiem, że był to długi proces) skończyliśmy z właściwą migracją pierwszego stylizowanego i w pełni funkcjonalnego komponentu Alien. Kup sobie dobrego drinka. Zasługujesz na to!

6. Wypłucz i powtórz dla wszystkich komponentów

Po przeprowadzeniu migracji pierwszego komponentu powinniśmy powtórzyć proces dla wszystkich naszych komponentów. W przypadku Frankenstein Demo pozostało jednak tylko jedno: odpowiedzialne za renderowanie listy zadań do wykonania.

Nowe owijarki dla nowych komponentów

Zacznijmy od dodania nowego opakowania. Zgodnie z omówioną powyżej konwencją nazewnictwa (ponieważ nasz komponent React nazywa się MainSection.js ), odpowiedni wrapper podczas migracji do Reacta powinien nazywać się MainSection-wrapper.js . Jednocześnie podobny komponent w Vue nazywa się Listing.vue , stąd odpowiedni wrapper w migracji do Vue powinien nazywać się Listing-wrapper.js . Jednak bez względu na konwencję nazewnictwa, sam wrapper będzie prawie identyczny z tym, który już mamy:

  • Opakowanie na listę React
  • Opakowanie na aukcję Vue

Jest tylko jedna interesująca rzecz, którą wprowadzamy w tym drugim komponencie w aplikacji React. Czasami, z tego lub innego powodu, możesz chcieć użyć wtyczki jQuery w swoich komponentach. W przypadku naszego komponentu React wprowadziliśmy dwie rzeczy:

  • Wtyczka Tooltip z Bootstrap korzystająca z jQuery,
  • Przełącznik dla klas CSS, takich jak .addClass() i .removeClass() .

    Uwaga : to użycie jQuery do dodawania/usuwania klas ma charakter wyłącznie ilustracyjny. Nie używaj jQuery w tym scenariuszu w rzeczywistych projektach — zamiast tego polegaj na zwykłym JavaScript.

Oczywiście wprowadzenie jQuery w komponencie Alien może wyglądać dziwnie, gdy migrujemy z jQuery, ale Twój Host może być inny niż Host w tym przykładzie — możesz migrować z AngularJS lub czegokolwiek innego. Ponadto funkcjonalność jQuery w komponencie i globalne jQuery niekoniecznie są tym samym.

Jednak problem polega na tym, że nawet jeśli potwierdzisz, że komponent działa dobrze w kontekście twojej aplikacji Alien, po umieszczeniu go w Shadow DOM, twoje wtyczki jQuery i inny kod, który opiera się na jQuery, po prostu nie będą działać.

jQuery w cieniu DOM

Rzućmy okiem na ogólną inicjalizację losowej wtyczki jQuery:

 $('.my-selector').fancyPlugin();

W ten sposób wszystkie elementy z .my-selector będą przetwarzane przez fancyPlugin . Ta forma inicjalizacji zakłada, że .my-selector jest obecny w globalnym DOM. Jednak po umieszczeniu takiego elementu w Shadow DOM, podobnie jak w przypadku stylów, granice cienia zapobiegają wkradaniu się do niego jQuery. W rezultacie jQuery nie może znaleźć elementów w Shadow DOM.

Rozwiązaniem jest dostarczenie opcjonalnego drugiego parametru do selektora, który definiuje element główny, z którego ma odbywać się wyszukiwanie jQuery. I tutaj możemy dostarczyć nasz shadowRoot .

 $('.my-selector', this.shadowRoot).fancyPlugin();

W ten sposób selektory jQuery, a co za tym idzie wtyczki, będą działać dobrze.

Należy jednak pamiętać, że komponenty Obcego są przeznaczone do użycia zarówno w Obcym ​​bez shadow DOM, jak i w Host w Shadow DOM. Dlatego potrzebujemy bardziej zunifikowanego rozwiązania, które domyślnie nie zakładałoby obecności Shadow DOM.

Analizując komponent MainSection w naszej aplikacji React, stwierdzamy, że ustawia on właściwość documentRoot .

 ... this.documentRoot = this.props.root? this.props.root: document; ...

Tak więc sprawdzamy przekazaną właściwość root i jeśli istnieje, używamy jej jako documentRoot . W przeciwnym razie wracamy do document .

Oto inicjalizacja wtyczki podpowiedzi, która używa tej właściwości:

 $('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });

Jako bonus używamy tej samej właściwości root do zdefiniowania kontenera do wstrzykiwania podpowiedzi w tym przypadku.

Teraz, gdy komponent Alien jest gotowy do zaakceptowania właściwości root , aktualizujemy renderowanie komponentu w odpowiednim opakowaniu Frankensteina:

 // `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);

I to wszystko! Komponent działa równie dobrze w Shadow DOM, jak w globalnym DOM.

Konfiguracja Webpack dla scenariusza z wieloma opakowaniami

Ekscytująca część dzieje się w konfiguracji Webpacka podczas korzystania z kilku wrapperów. Nic się nie zmienia dla dołączonych stylów, takich jak moduły CSS w komponentach Vue lub styled-components w React. Jednak globalne style powinny teraz nieco się zmienić.

Pamiętaj, że powiedzieliśmy, że style-loader (odpowiedzialny za wstrzykiwanie globalnych arkuszy stylów do prawidłowego Shadow DOM) jest nieelastyczny, ponieważ zajmuje tylko jeden selektor na raz dla opcji insert . Oznacza to, że powinniśmy podzielić regułę .css w pakiecie Webpack, aby mieć jedną regułę podrzędną na opakowanie przy użyciu reguły oneOf lub podobnej, jeśli korzystasz z innego pakietu niż Webpack.

Zawsze łatwiej to wyjaśnić na przykładzie, więc porozmawiajmy o tym z migracji do Vue (jednak ten z migracji do Reacta jest prawie identyczny):

 ... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...

Wykluczyłem css-loader , ponieważ jego konfiguracja jest taka sama we wszystkich przypadkach. Zamiast tego porozmawiajmy o programie style-loader . W tej konfiguracji wstawiamy tag <style> do *-header-* lub *-listing-* , w zależności od nazwy pliku żądającego tego arkusza stylów (reguła issuer w pakiecie Webpack). Musimy jednak pamiętać, że globalny arkusz stylów wymagany do renderowania komponentu Alien może zostać zaimportowany w dwóch miejscach:

  • Sam składnik Obcy,
  • Opakowanie Frankensteina.

I tutaj powinniśmy docenić konwencję nazewnictwa opakowań, opisaną powyżej, gdy nazwa komponentu Alien i odpowiedni wrapper pasują do siebie. Jeśli, na przykład, mamy arkusz stylów, zaimportowany w komponencie Vue o nazwie Header.vue , otrzyma on poprawny *-header-* . Jednocześnie, jeśli zamiast tego importujemy arkusz stylów w wrapperze, taki arkusz stylów podlega dokładnie tej samej zasadzie, jeśli wrapper nazywa się Header-wrapper.js bez żadnych zmian w konfiguracji. To samo dotyczy składnika Listing.vue i odpowiadającego mu opakowania Listing-wrapper.js . Korzystając z tej konwencji nazewnictwa, zmniejszamy konfigurację w naszym pakiecie.

Po przeprowadzeniu migracji wszystkich komponentów nadszedł czas na ostatni etap migracji.

7. Przełącz się na obcego

W pewnym momencie dowiadujesz się, że komponenty, które zidentyfikowałeś na pierwszym etapie migracji, zostały zastąpione opakowaniami Frankensteina. Żadna aplikacja jQuery nie została tak naprawdę, a to, co masz, to w zasadzie aplikacja Alien, która jest sklejona za pomocą środków Hosta.

Na przykład część zawartości index.html w aplikacji jQuery — po migracji obu mikroserwisów — wygląda teraz mniej więcej tak:

 <section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>

W tej chwili nie ma sensu trzymać naszej aplikacji jQuery w pobliżu: zamiast tego powinniśmy przełączyć się na aplikację Vue i zapomnieć o wszystkich naszych wrapperach, Shadow DOM i wymyślnych konfiguracjach Webpack. W tym celu mamy eleganckie rozwiązanie.

Porozmawiajmy o żądaniach HTTP. Wspomnę tutaj o konfiguracji Apache, ale to tylko szczegół implementacji: wykonanie przełącznika w Nginx lub cokolwiek innego powinno być tak trywialne jak w Apache.

Wyobraź sobie, że Twoja witryna jest obsługiwana z folderu /var/www/html na serwerze. W takim przypadku Twój plik httpd.conf lub httpd-vhost.conf powinien mieć wpis wskazujący na ten folder, taki jak:

 DocumentRoot "/var/www/html"

Aby przełączyć aplikację po migracji Frankensteina z jQuery do React, wystarczy zaktualizować wpis DocumentRoot na coś takiego:

 DocumentRoot "/var/www/html/react/build"

Zbuduj aplikację Alien, uruchom ponownie serwer, a Twoja aplikacja będzie obsługiwana bezpośrednio z folderu Alien: aplikacja React obsługiwana z folderu react/ . Jednak to samo dotyczy oczywiście Vue lub każdego innego frameworka, który zmigrowałeś. Dlatego tak ważne jest, aby Host i Obcy byli całkowicie niezależni i funkcjonalni w dowolnym momencie, ponieważ na tym etapie Twój Obcy staje się Twoim Gospodarzem.

Teraz możesz bezpiecznie usunąć wszystko wokół folderu Obcego, w tym wszystkie elementy Shadow DOM, opakowania Frankensteina i wszelkie inne artefakty związane z migracją. Momentami była to trudna ścieżka, ale Twoja witryna została zmigrowana. Gratulacje!

Wniosek

W tym artykule zdecydowanie przeszliśmy przez nieco trudny teren. Jednak po tym, jak zaczęliśmy z aplikacją jQuery, udało nam się zmigrować ją zarówno do Vue, jak i React. Po drodze odkryliśmy kilka nieoczekiwanych i nie tak trywialnych problemów: musieliśmy naprawić stylizację, musieliśmy naprawić funkcjonalność JavaScript, wprowadzić pewne konfiguracje bundlerów i wiele więcej. Dało nam to jednak lepszy przegląd tego, czego możemy się spodziewać w rzeczywistych projektach. W końcu otrzymaliśmy współczesną aplikację bez żadnych pozostałych bitów z aplikacji jQuery, mimo że mieliśmy wszelkie prawa do sceptycznego podejścia do efektu końcowego w trakcie migracji.

Po przejściu na Obcego Frankenstein może przejść na emeryturę.
Po przejściu na Obcego Frankenstein może przejść na emeryturę. (duży podgląd)

Migracja Frankensteina nie jest ani srebrną kulą, ani nie powinna być przerażającym procesem. To po prostu zdefiniowany algorytm, mający zastosowanie w wielu projektach, który pomaga przekształcić projekty w coś nowego i solidnego w przewidywalny sposób.