Migrația Frankenstein: abordare agnostică a cadrului (Partea a 2-a)
Publicat: 2022-03-10În acest articol, vom pune toată teoria la încercare efectuând migrarea pas cu pas a unei aplicații, urmând recomandările din partea anterioară. Pentru a simplifica lucrurile, pentru a reduce incertitudinile, necunoscutele și ghicirile inutile, pentru exemplul practic de migrare, am decis să demonstrez practica pe o aplicație simplă de făcut.
În general, presupun că aveți o bună înțelegere a modului în care funcționează o aplicație generică de făcut. Acest tip de aplicație se potrivește foarte bine nevoilor noastre: este previzibilă, dar are un număr minim viabil de componente necesare pentru a demonstra diferite aspecte ale Frankenstein Migration. Cu toate acestea, indiferent de dimensiunea și complexitatea aplicației dvs. reale, abordarea este bine scalabilă și se presupune că este potrivită pentru proiecte de orice dimensiune.
Pentru acest articol, ca punct de plecare, am ales o aplicație jQuery din proiectul TodoMVC - un exemplu care poate fi deja familiar pentru mulți dintre voi. jQuery este suficient de moștenire, ar putea reflecta o situație reală cu proiectele dvs. și, cel mai important, necesită întreținere semnificativă și hack-uri pentru alimentarea unei aplicații dinamice moderne. (Acest lucru ar trebui să fie suficient pentru a lua în considerare migrarea către ceva mai flexibil.)
Ce este acest „mai flexibil” spre care vom migra atunci? Pentru a arăta un caz extrem de practic util în viața reală, a trebuit să aleg dintre cele mai populare două cadre în zilele noastre: React și Vue. Cu toate acestea, oricare aș alege, am rata unele aspecte din cealaltă direcție.
Deci, în această parte, vom parcurge ambele următoarele:
- O migrare a unei aplicații jQuery la React și
- O migrare a unei aplicații jQuery la Vue .
Arhivele de coduri
Tot codul menționat aici este disponibil public și îl puteți accesa oricând doriți. Există două depozite disponibile pentru a vă juca:
- Frankenstein TodoMVC
Acest depozit conține aplicații TodoMVC în diferite cadre/biblioteci. De exemplu, puteți găsi ramuri precumvue
,angularjs
,react
șijquery
în acest depozit. - Frankenstein Demo
Conține mai multe ramuri, fiecare dintre acestea reprezentând o anumită direcție de migrare între aplicații, disponibile în primul depozit. Există ramuri precummigration/jquery-to-react
șimigration/jquery-to-vue
, în special, pe care le vom acoperi mai târziu.
Ambele depozite sunt în curs de desfășurare, iar noi ramuri cu aplicații noi și direcții de migrare ar trebui adăugate în mod regulat. ( Sunteți liber să contribuiți și dvs.! ) Istoricul comenzilor în ramurile de migrație este bine structurat și ar putea servi ca documentație suplimentară cu chiar mai multe detalii decât aș putea acoperi în acest articol.
Acum, hai să ne murdărim mâinile! Avem un drum lung înainte, așa că nu vă așteptați să fie o călătorie lină. Depinde de dvs. să decideți cum doriți să urmați acest articol, dar puteți face următoarele:
- Clonați ramura
jquery
din depozitul Frankenstein TodoMVC și urmați cu strictețe toate instrucțiunile de mai jos. - Alternativ, puteți deschide o ramură dedicată fie migrării la React, fie migrării către Vue din depozitul Frankenstein Demo și urmăriți împreună cu istoricul comitărilor.
- Alternativ, vă puteți relaxa și continua să citiți, deoarece voi evidenția codul cel mai critic chiar aici și este mult mai important să înțelegeți mecanica procesului, mai degrabă decât codul real.
Aș dori să menționez încă o dată că vom urma cu strictețe pașii prezentați în prima parte teoretică a articolului.
Să ne scufundăm direct!
- Identificați microservicii
- Permite accesul de la gazdă la străin
- Scrieți un microserviciu/componentă extraterestră
- Scrieți Web Component Wrapper Around Alien Service
- Înlocuiți serviciul gazdă cu componenta web
- Clătiți și repetați pentru toate componentele dvs
- Comutați la Alien
1. Identificați microservicii
După cum sugerează Partea 1, în acest pas, trebuie să ne structurem aplicația în servicii mici , independente , dedicate unui anumit loc de muncă . Cititorul atent ar putea observa că aplicația noastră de făcut este deja mică și independentă și poate reprezenta un singur microserviciu pe cont propriu. Așa l-aș trata eu însumi dacă această aplicație ar trăi într-un context mai larg. Amintiți-vă, totuși, că procesul de identificare a microserviciilor este în întregime subiectiv și nu există un răspuns corect.
Deci, pentru a vedea procesul de migrare Frankenstein mai detaliat, putem face un pas mai departe și putem împărți această aplicație de făcut în două microservicii independente:
- Un câmp de intrare pentru adăugarea unui articol nou.
Acest serviciu poate conține și antetul aplicației, bazat exclusiv pe poziționarea proximității acestor elemente. - O listă de articole deja adăugate.
Acest serviciu este mai avansat și, împreună cu lista în sine, conține și acțiuni precum filtrarea, acțiunile elementului din listă și așa mai departe.
Sfat : pentru a verifica dacă serviciile selectate sunt cu adevărat independente, eliminați marcajul HTML, care reprezintă fiecare dintre aceste servicii. Asigurați-vă că funcțiile rămase funcționează în continuare. În cazul nostru, ar trebui să fie posibilă adăugarea de noi intrări în localStorage
(pe care această aplicație le folosește ca stocare) din câmpul de intrare fără listă, în timp ce lista redă în continuare intrările din localStorage
chiar dacă câmpul de intrare lipsește. Dacă aplicația dvs. generează erori atunci când eliminați marcajul pentru un potențial microserviciu, aruncați o privire la secțiunea „Refactorizați dacă este necesar” din partea 1 pentru un exemplu despre cum să rezolvați astfel de cazuri.
Desigur, am putea continua și împărțim al doilea serviciu și listarea articolelor și mai mult în microservicii independente pentru fiecare articol anume. Cu toate acestea, ar putea fi prea granular pentru acest exemplu. Deci, deocamdată, concluzionăm că aplicația noastră va avea două servicii; sunt independenți și fiecare dintre ei lucrează pentru propria sa sarcină particulară. Prin urmare, ne-am împărțit aplicația în microservicii .
2. Permite accesul de la gazdă la străin
Permiteți-mi să vă reamintesc pe scurt care sunt acestea.
- Gazdă
Așa se numește aplicația noastră actuală. Este scris cu cadrul de care suntem pe cale să ne îndepărtăm . În acest caz particular, aplicația noastră jQuery. - Străin
Mai simplu spus, aceasta este o rescrie treptată a Host pe noul cadru în care suntem pe cale să ne mutăm . Din nou, în acest caz particular, este o aplicație React sau Vue.
Regula de bază atunci când împărțiți Host și Alien este că ar trebui să puteți dezvolta și implementa oricare dintre ele fără a o rupe pe celălalt - în orice moment.
Menținerea gazdei și extratereștrilor independenți unul de celălalt este crucială pentru migrația Frankenstein. Cu toate acestea, acest lucru face ca aranjarea comunicării între cei doi să fie puțin dificilă. Cum permitem gazdei să acceseze Alien fără a le zdrobi pe cele două împreună?
Adăugarea Alienului ca submodul al gazdei dvs
Chiar dacă există mai multe modalități de a realiza configurarea de care avem nevoie, cea mai simplă formă de organizare a proiectului pentru a îndeplini acest criteriu este probabil submodulele git. Acesta este ceea ce vom folosi în acest articol. Vă las pe voi să citiți cu atenție cum funcționează submodulele din git pentru a înțelege limitările și problemele acestei structuri.
Principiile generale ale arhitecturii proiectului nostru cu submodule git ar trebui să arate astfel:
- Atât Host, cât și Alien sunt independente și sunt păstrate în depozite
git
separate; - Gazda face referire la Alien ca submodul. În această etapă, Host alege o anumită stare (commit) a Alien și o adaugă ca, ceea ce arată ca, un subdosar în structura folderului Host.
Procesul de adăugare a unui submodul este același pentru orice aplicație. Predarea git submodules
depășește domeniul de aplicare al acestui articol și nu este direct legată de Frankenstein Migration în sine. Deci, să aruncăm o scurtă privire la exemplele posibile.
În fragmentele de mai jos, folosim direcția React ca exemplu. Pentru orice altă direcție de migrare, înlocuiți react
cu numele unei sucursale de la Frankenstein TodoMVC sau ajustați la valorile personalizate acolo unde este necesar.
Dacă urmați folosind aplicația originală jQuery TodoMVC:
$ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i
Dacă urmați ramurile migration/jquery-to-react
(sau orice altă direcție de migrare) din depozitul Frankenstein Demo, aplicația Alien ar trebui să fie deja acolo ca git submodule
și ar trebui să vedeți un folder respectiv. Cu toate acestea, folderul este gol în mod implicit și trebuie să actualizați și să inițializați submodulele înregistrate.
De la rădăcina proiectului dvs. (gazdă):
$ git submodule update --init $ cd react $ npm i
Rețineți că în ambele cazuri instalăm dependențe pentru aplicația Alien, dar acestea devin sandbox în subfolder și nu vor polua gazda noastră.
După ce adăugați aplicația Alien ca submodul al gazdei dvs., obțineți aplicații Alien și Gazdă independente (în ceea ce privește microservicii). Cu toate acestea, Host consideră că Alien este un subdosar în acest caz și, evident, asta îi permite Gazdei să acceseze Alien fără probleme.
3. Scrieți un microserviciu/componentă extraterestră
La acest pas, trebuie să decidem ce microserviciu să migrem mai întâi și să-l scriem/utilizam de partea Alien. Să urmăm aceeași ordine a serviciilor pe care am identificat-o la Pasul 1 și să începem cu primul: câmpul de introducere pentru adăugarea unui nou articol. Cu toate acestea, înainte de a începe, să fim de acord că dincolo de acest punct, vom folosi o componentă de termen mai favorabilă în loc de microserviciu sau serviciu , pe măsură ce ne îndreptăm către premisele cadrelor frontend, iar termenul de componentă urmează definițiile aproape oricărui serviciu modern. cadru.
Ramurile depozitului Frankenstein TodoMVC conțin o componentă rezultată care reprezintă primul serviciu „Câmp de intrare pentru adăugarea unui articol nou” ca componentă Antet:
- Componenta antet în React
- Componenta antet în Vue
Scrierea componentelor în cadrul alegerii dvs. depășește domeniul de aplicare al acestui articol și nu face parte din Frankenstein Migration. Cu toate acestea, există câteva lucruri de reținut când scrieți o componentă Alien.
Independenţă
În primul rând, componentele din Alien ar trebui să urmeze același principiu de independență, stabilit anterior de partea Gazdei: componentele nu ar trebui să depindă în niciun fel de alte componente.
Interoperabilitate
Datorită independenței serviciilor, cel mai probabil, componentele din Gazda dumneavoastră comunică într-un mod bine stabilit, fie că este vorba de un sistem de management de stat, de comunicare prin intermediul unor stocări partajate sau, direct printr-un sistem de evenimente DOM. „Interoperabilitatea” componentelor Alien înseamnă că acestea ar trebui să se poată conecta la aceeași sursă de comunicare, stabilită de Gazdă, pentru a trimite informații despre modificările stării sale și pentru a asculta modificările altor componente. În practică, aceasta înseamnă că, dacă componentele din gazdă comunică prin evenimente DOM, construirea componentei Alien exclusiv având în vedere managementul de stat nu va funcționa perfect pentru acest tip de migrare, din păcate.
De exemplu, aruncați o privire la fișierul js/storage.js
care este principalul canal de comunicare pentru componentele noastre 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); }, ...
Aici, folosim localStorage
(deoarece acest exemplu nu este critic pentru securitate) pentru a stoca articolele noastre de făcut și, odată ce modificările aduse stocării sunt înregistrate, trimitem un eveniment DOM personalizat pe elementul de document
pe care orice componentă îl poate asculta.
În același timp, din partea Alienului (să zicem React) putem stabili o comunicare complexă de management de stat pe cât ne dorim. Cu toate acestea, probabil că este inteligent să-l păstrăm pentru viitor: pentru a integra cu succes componenta noastră Alien React în Host, trebuie să ne conectăm la același canal de comunicare folosit de Host. În acest caz, este localStorage
. Pentru a simplifica lucrurile, am copiat fișierul de stocare al lui Host în Alien și am conectat componentele noastre la el:
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); } } ... }
Acum, componentele noastre Alien pot vorbi aceeași limbă cu componentele Gazdă și invers.
4. Scrieți Web Component Wrapper Around Alien Service
Chiar dacă acum suntem abia la a patra treaptă, am realizat destul de multe:
- Am împărțit aplicația noastră Gazdă în servicii independente care sunt gata să fie înlocuite cu servicii Alien;
- Am configurat Host și Alien să fie complet independente unul de celălalt, dar foarte bine conectate prin
git submodules
; - Am scris prima noastră componentă Alien folosind noul cadru.
Acum este timpul să configurați o punte între Gazdă și Alien, astfel încât noua componentă Alien să poată funcționa în Gazdă.
Memento din partea 1 : asigurați-vă că gazda are disponibil un pachet de pachete. În acest articol, ne bazăm pe Webpack, dar asta nu înseamnă că tehnica nu va funcționa cu Rollup sau orice alt bundler la alegere. Cu toate acestea, las maparea din Webpack pe seama experimentelor dvs.
Convenția de denumire
După cum sa menționat în articolul anterior, vom folosi componentele web pentru a integra Alien în Host. Pe partea gazdei, creăm un fișier nou: js/frankenstein-wrappers/Header-wrapper.js
. (Va fi primul nostru wrapper Frankenstein.) Rețineți că este o idee bună să vă denumiți wrapper-urile la fel ca și componentele dvs. în aplicația Alien, de exemplu doar prin adăugarea unui sufix „ -wrapper
”. Veți vedea mai târziu de ce aceasta este o idee bună, dar pentru moment, să fim de acord că aceasta înseamnă că, dacă componenta Alien se numește Header.js
(în React) sau Header.vue
(în Vue), învelișul corespunzător de pe Partea gazdă ar trebui să se numească Header-wrapper.js
.
În primul nostru ambalaj, începem cu placa fundamentală pentru înregistrarea unui element personalizat:
class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);
Apoi, trebuie să inițializam Shadow DOM pentru acest element.
Vă rugăm să consultați Partea 1 pentru a înțelege motivul pentru care folosim Shadow DOM.
class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }
Cu aceasta, avem toate elementele esențiale ale componentei web configurate și este timpul să adăugăm componenta noastră Alien în amestec. În primul rând, la începutul ambalajului nostru Frankenstein, ar trebui să importam toți biții responsabili pentru randarea componentei Alien.
import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...
Aici trebuie să ne oprim o secundă. Rețineți că nu importăm dependențele lui Alien din node_modules
de la Host. Totul vine de la Alien însuși care se află în react/
subfolder. De aceea, Pasul 2 este atât de important și este crucial să vă asigurați că Gazda are acces deplin la activele Alien.
Acum, putem reda componenta noastră Alien în Shadow DOM al componentei web:
... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...
Notă : În acest caz, React nu are nevoie de nimic altceva. Cu toate acestea, pentru a reda componenta Vue, trebuie să adăugați un nod de împachetare care să conțină componenta Vue, după cum urmează:
... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...
Motivul pentru aceasta este diferența în modul de redare a componentelor React și Vue: React adaugă componenta la nodul DOM referit, în timp ce Vue înlocuiește nodul DOM referit cu componenta. Prin urmare, dacă facem .$mount(this.shadowRoot)
pentru Vue, în esență înlocuiește Shadow DOM.
Asta e tot ce avem de făcut cu ambalajul nostru deocamdată. Rezultatul curent pentru wrapper-ul Frankenstein în ambele direcții de migrare jQuery-to-React și jQuery-to-Vue poate fi găsit aici:
- Frankenstein Wrapper pentru componenta React
- Frankenstein Wrapper pentru componenta Vue
Pentru a rezuma mecanica ambalajului Frankenstein:
- Creați un element personalizat,
- Inițiază Shadow DOM,
- Importați tot ce este necesar pentru redarea unei componente Alien,
- Redați componenta Alien în Shadow DOM al elementului personalizat.
Cu toate acestea, acest lucru nu redă automat Alienul nostru în gazdă. Trebuie să înlocuim marcajul Host existent cu noul nostru wrapper Frankenstein.
Fixați-vă centurile de siguranță, s-ar putea să nu fie atât de simplu pe cât s-ar aștepta!
5. Înlocuiți Serviciul Gazdă cu Componenta Web
Să continuăm și să adăugăm noul nostru fișier Header-wrapper.js
la index.html
și să înlocuim marcajul de antet existent cu elementul personalizat <frankenstein-header-wrapper>
nou creat.
... <!-- <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>
Din păcate, acest lucru nu va funcționa atât de simplu. Dacă deschideți un browser și verificați consola, vă așteaptă un Uncaught SyntaxError
. În funcție de browser și de suportul acestuia pentru modulele ES6, acesta va fi legat fie de importurile ES6, fie de modul în care este redată componenta Alien. Oricum ar fi, trebuie să facem ceva în privința asta, dar problema și soluția ar trebui să fie familiare și clare pentru majoritatea cititorilor.
5.1. Actualizați Webpack și Babel acolo unde este necesar
Ar trebui să implicăm ceva magie Webpack și Babel înainte de a integra ambalajul nostru Frankenstein. Confruntarea acestor instrumente depășește domeniul de aplicare al articolului, dar puteți arunca o privire asupra comiterilor corespunzătoare din depozitul Frankenstein Demo:
- Configurare pentru migrarea la React
- Configurare pentru migrarea la Vue
În esență, am configurat procesarea fișierelor, precum și un nou punct de intrare Frankenstein în configurația Webpack pentru frankenstein
conține tot ce este legat de ambalajele Frankenstein într-un singur loc.
Odată ce Webpack în Host știe cum să proceseze componenta Alien și Componentele Web, suntem gata să înlocuim marcajul Host cu noul wrapper Frankenstein.
5.2. Înlocuirea efectivă a componentei
Înlocuirea componentei ar trebui să fie simplă acum. În index.html
al gazdei dvs., faceți următoarele:
- Înlocuiți elementul DOM
<header class="header">
cu<frankenstein-header-wrapper>
; - Adăugați un nou script
frankenstein.js
. Acesta este noul punct de intrare în Webpack care conține tot ce are legătură cu ambalajele Frankenstein.
... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>
Asta e! Reporniți serverul dacă este necesar și asistați la magia componentei Alien integrate în Host.
Cu toate acestea, ceva pare să fie încă lipsește. Componenta Alien în contextul Gazdă nu arată la fel ca în contextul aplicației independente Alien. Este pur și simplu fără stil.
De ce este așa? Nu ar trebui ca stilurile componentei să fie integrate automat cu componenta Alien în Host? Mi-aș dori, dar ca în prea multe situații, depinde. Ajungem la partea provocatoare a migrației Frankenstein.
5.3. Informații generale despre stilul componentei extraterestre
În primul rând, ironia este că nu există niciun bug în modul în care funcționează lucrurile. Totul este așa cum este proiectat să funcționeze. Pentru a explica acest lucru, să menționăm pe scurt diferite moduri de a coafa componentele.
Stiluri globale
Cu toții suntem familiarizați cu acestea: stilurile globale pot fi (și de obicei sunt) distribuite fără nicio componentă anume și pot fi aplicate întregii pagini. Stilurile globale afectează toate nodurile DOM cu selectori care se potrivesc.
Câteva exemple de stiluri globale sunt etichetele <style>
și <link rel="stylesheet">
găsite în index.html
. Alternativ, o foaie de stil globală poate fi importată într-un modul rădăcină JS, astfel încât toate componentele să poată avea acces la ea.
Problema stilării aplicațiilor în acest fel este evidentă: menținerea foilor de stil monolitice pentru aplicații mari devine foarte grea. De asemenea, așa cum am văzut în articolul anterior, stilurile globale pot rupe cu ușurință componente care sunt redate drept în arborele DOM principal, așa cum este în React sau Vue.
Stiluri grupate
Aceste stiluri sunt de obicei strâns cuplate cu o componentă în sine și sunt rareori distribuite fără componentă. Stilurile se află de obicei în același fișier cu componenta. Exemple bune de acest tip de stil sunt componentele cu stil în modulele React sau CSS și CSS Scoped în componentele unui singur fișier în Vue. Cu toate acestea, indiferent de varietatea de instrumente pentru scrierea stilurilor grupate, principiul de bază în majoritatea dintre ele este același: instrumentele oferă un mecanism de definire a domeniului pentru a bloca stilurile definite într-o componentă, astfel încât stilurile să nu distrugă alte componente sau globale. stiluri.
De ce ar putea stilurile acoperite să fie fragile?
În partea 1, când justificăm utilizarea Shadow DOM în Frankenstein Migration, am acoperit pe scurt subiectul scoping vs. Cu toate acestea, nu am explicat de ce instrumentele de scoping oferă un stil atât de fragil pentru componentele noastre, iar acum, când ne-am confruntat cu componenta Alien fără stil, aceasta devine esențială pentru înțelegere.
Toate instrumentele de definire a domeniului pentru cadrele moderne funcționează în mod similar:
- Scrieți stiluri pentru componenta dvs. într-un fel fără să vă gândiți prea mult la sfera de aplicare sau la încapsulare;
- Îți rulezi componentele cu foi de stil importate/încorporate printr-un sistem de grupare, cum ar fi Webpack sau Rollup;
- Gruparea generează clase CSS unice sau alte atribute, creând și injectând selectori individuali atât pentru HTML, cât și pentru foile de stil corespunzătoare;
- bundler-ul face o intrare
<style>
în<head>
documentului dvs. și pune acolo stilurile componentelor dvs. cu selectoare unice amestecate.
Cam asta e tot. Funcționează și funcționează bine în multe cazuri. Cu excepția cazurilor în care nu: atunci când stilurile pentru toate componentele trăiesc în domeniul de aplicare global al stilului, devine ușor să le spargeți, de exemplu, folosind o specificitate mai mare. Acest lucru explică fragilitatea potențială a instrumentelor de scoping, dar de ce componenta noastră Alien este complet nestilată?
Să aruncăm o privire la gazda actuală folosind DevTools. Când inspectăm învelișul Frankenstein nou adăugat cu componenta Alien React, de exemplu, putem vedea ceva de genul acesta:
Deci, Webpack generează clase CSS unice pentru componenta noastră. Grozav! Unde sunt stilurile atunci? Ei bine, stilurile sunt exact acolo unde sunt proiectate să fie - în <head>
documentului.
Deci totul funcționează așa cum ar trebui, iar aceasta este principala problemă. Deoarece componenta noastră Alien se află în Shadow DOM și, așa cum este explicat în Partea #1, Shadow DOM oferă încapsularea completă a componentelor din restul paginii și a stilurilor globale, inclusiv acele foi de stil nou generate pentru componenta care nu poate trece granița umbră și ajunge la componenta Alien. Prin urmare, componenta Alien este lăsată fără stil. Cu toate acestea, acum, tactica de rezolvare a problemei ar trebui să fie clară: ar trebui să plasăm cumva stilurile componentei în același Shadow DOM în care se află componenta noastră (în loc de <head>
al documentului).
5.4. Stiluri de remediere pentru componenta extraterestră
Până acum, procesul de migrare la orice cadru a fost același. Cu toate acestea, lucrurile încep să diverge aici: fiecare cadru are recomandările sale cu privire la modul de stilare a componentelor și, prin urmare, modalitățile de abordare a problemei diferă. Aici, discutăm cele mai frecvente cazuri, dar, dacă cadrul cu care lucrați folosește un mod unic de stilare a componentelor, trebuie să aveți în vedere tacticile de bază, cum ar fi punerea stilurilor componentei în Shadow DOM în loc de <head>
.
În acest capitol, acoperim remedieri pentru:
- Stiluri combinate cu module CSS în Vue (tacticile pentru Scoped CSS sunt aceleași);
- Stiluri grupate cu componente stilate în React;
- Module CSS generice și stiluri globale. Le combin pentru că modulele CSS, în general, sunt foarte asemănătoare cu foile de stil globale și pot fi importate de orice componentă, făcând stilurile deconectate de la orice componentă anume.
Constrângeri mai întâi: orice facem pentru a remedia stilul nu ar trebui să distrugă componenta Alien în sine . În caz contrar, ne pierdem independența sistemelor noastre Alien și Gazdă. Deci, pentru a aborda problema de stil, ne vom baza fie pe configurația bundler-ului, fie pe ambalajul Frankenstein.
Stiluri incluse în Vue și Shadow DOM
Dacă scrieți o aplicație Vue, atunci cel mai probabil utilizați componente cu un singur fișier. Dacă utilizați și Webpack, ar trebui să fiți familiarizat cu două încărcătoare vue-loader
și vue-style-loader
. Primul vă permite să scrieți acele componente unice de fișier, în timp ce cel de-al doilea injectează dinamic CSS-ul componentei într-un document ca etichetă <style>
. În mod implicit, vue-style-loader
injectează stilurile componente în <head>
documentului. Cu toate acestea, ambele pachete acceptă opțiunea shadowMode
în configurație, ceea ce ne permite să schimbăm cu ușurință comportamentul implicit și să injectăm stiluri (după cum sugerează și numele opțiunii) în Shadow DOM. Să-l vedem în acțiune.
Configurare Webpack
Cel puțin, fișierul de configurare Webpack ar trebui să conțină următoarele:
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() ] }
Într-o aplicație reală, blocul dvs. de test: /\.css$/
va fi mai sofisticat (care implică probabil regula oneOf
) pentru a lua în considerare atât configurațiile Gazdă, cât și cele Alien. Cu toate acestea, în acest caz, jQuery-ul nostru este stilat cu <link rel="stylesheet">
simplu în index.html
, așa că nu construim stiluri pentru Host prin Webpack și este sigur să ne asigurăm numai Alien.
Configurație Wrapper
În plus față de configurația Webpack, trebuie să ne actualizăm și wrapper-ul Frankenstein, arătând Vue către DOM-ul Shadow corect. În Header-wrapper.js
, redarea componentei Vue ar trebui să includă proprietatea shadowRoot
care duce la shadowRoot
a wrapper-ului nostru Frankenstein:
... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...
După ce actualizați fișierele și reporniți serverul, ar trebui să obțineți ceva de genul acesta în DevTools:
În cele din urmă, stilurile pentru componenta Vue se află în cadrul nostru Shadow DOM. În același timp, aplicația dvs. ar trebui să arate astfel:
Începem să obținem ceva asemănător aplicației noastre Vue: stilurile incluse cu componenta sunt injectate în Shadow DOM al wrapper-ului, dar componenta încă nu arată așa cum ar trebui să fie. Motivul este că, în aplicația originală Vue, componenta este stilată nu numai cu stilurile grupate, ci și parțial cu stilurile globale. Cu toate acestea, înainte de a remedia stilurile globale, trebuie să aducem integrarea noastră React în aceeași stare ca cea Vue.
Stiluri grupate în React și Shadow DOM
Deoarece există multe moduri prin care se poate stila o componentă React, soluția specială de a repara o componentă Alien în Frankenstein Migration depinde de modul în care stilăm componenta în primul rând. Să acoperim pe scurt alternativele cele mai frecvent utilizate.
componente-stilizate
styled-components este una dintre cele mai populare moduri de stilare a componentelor React. Pentru componenta Header React, componentele stilate este exact felul în care o stilăm. Deoarece aceasta este o abordare clasică CSS-in-JS, nu există niciun fișier cu o extensie dedicată la care să putem conecta bundler-ul, așa cum facem pentru fișierele .css
sau .js
, de exemplu. Din fericire, componentele stilizate permit injectarea stilurilor componente într-un nod personalizat (Shadow DOM în cazul nostru) în loc de head
documentului cu ajutorul componentei de ajutor StyleSheetManager
. Este o componentă predefinită, instalată cu pachetul styled-components
care acceptă proprietatea target
, definind „un nod DOM alternativ pentru a injecta informații despre stiluri”. Exact ce ne trebuie! Mai mult, nici măcar nu trebuie să ne schimbăm configurația Webpack: totul depinde de ambalajul nostru Frankenstein.
Ar trebui să actualizăm Header-wrapper.js
care conține componenta React Alien cu următoarele linii:
... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...
Aici, importăm componenta StyleSheetManager
(de la Alien, și nu de la Host) și împachetăm componenta React cu ea. În același timp, trimitem proprietatea target
care indică către shadowRoot
. Asta e. Dacă reporniți serverul, trebuie să vedeți ceva de genul acesta în DevTools:
Acum, stilurile componentelor noastre sunt în Shadow DOM în loc de <head>
. În acest fel, redarea aplicației noastre seamănă acum cu ceea ce am văzut anterior cu aplicația Vue.
Aceeași poveste: componentele cu stil sunt responsabile doar pentru partea grupată a stilurilor componentei React , iar stilurile globale gestionează biții rămași. Revenim la stilurile globale puțin după ce revizuim încă un tip de componente de styling.
Module CSS
Dacă aruncați o privire mai atentă la componenta Vue pe care am reparat-o mai devreme, s-ar putea să observați că Modulele CSS este tocmai modul în care stilăm acea componentă. 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. Cum facem asta?
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. Vesti bune! 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.
Foi de stil globale și opțiunea de insert
a style-loader
Dacă verificați documentația pentru această opțiune, observați că această opțiune necesită un selector per configurație. Aceasta înseamnă că, dacă aveți mai multe componente Alien care necesită stiluri globale introduse într-un înveliș Frankenstein, trebuie să specificați style-loader
pentru fiecare dintre ambalajele Frankenstein. În practică, aceasta înseamnă că, probabil, trebuie să vă bazați pe regula oneOf
din blocul dvs. de configurare pentru a servi tuturor wrapper-urilor.
{ 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. ], }
Nu foarte flexibil, sunt de acord. Cu toate acestea, nu este mare lucru atâta timp cât nu aveți sute de componente de migrat. În caz contrar, ar putea face configurația Webpack greu de întreținut. Problema reală este însă că nu putem scrie un selector CSS pentru Shadow DOM.
Încercând să rezolvăm acest lucru, am putea observa că opțiunea de insert
poate lua și o funcție în loc de un selector simplu pentru a specifica o logică mai avansată pentru inserare. Cu aceasta, putem folosi această opțiune pentru a insera foi de stil direct în Shadow DOM! Într-o formă simplificată, ar putea arăta similar cu acesta:
insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }
Tentant, nu-i așa? Cu toate acestea, acest lucru nu va funcționa pentru scenariul nostru sau va funcționa departe de a fi optim. <frankenstein-header-wrapper>
este într-adevăr disponibil de la index.html
(pentru că l-am adăugat în Pasul 5.2). Dar când Webpack procesează toate dependențele (inclusiv foile de stil) fie pentru o componentă Alien, fie pentru un wrapper Frankenstein, Shadow DOM nu este încă inițializat în wrapper-ul Frankenstein: importurile sunt procesate înainte de aceasta. Prin urmare, îndreptarea direct spre insert
va avea ca rezultat o eroare.
Există un singur caz în care putem garanta că Shadow DOM este inițializat înainte ca Webpack să proceseze dependența noastră de foaia de stil. Dacă componenta Alien nu importă o foaie de stil în sine și depinde de Frankenstein wrapper să o importe, am putea folosi importul dinamic și importam foaia de stil necesară după ce am configurat Shadow DOM:
this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');
Acest lucru va funcționa: un astfel de import, combinat cu configurația de insert
de mai sus, va găsi într-adevăr Shadow DOM corect și va insera eticheta <style>
în el. Cu toate acestea, obținerea și procesarea foii de stil va dura timp, ceea ce înseamnă că utilizatorii dvs. cu o conexiune lentă sau dispozitive lente s-ar putea confrunta cu un moment de componentă nestilată înainte ca foaia de stil să ajungă la locul său în Shadow DOM al wrapper-ului.
Deci, una peste alta, chiar dacă insert
acceptă funcția, din păcate, nu este suficient pentru noi și trebuie să ne întoarcem la selectori CSS simpli precum frankenstein-header-wrapper
. Acest lucru nu plasează automat foile de stil în Shadow DOM, iar foile de stil se află în <frankenstein-header-wrapper>
în afara Shadow DOM.
Mai avem nevoie de o piesă din puzzle.
Configurație Wrapper pentru foile de stil globale și Shadow DOM
Din fericire, remedierea este destul de simplă din partea wrapper-ului: când Shadow DOM este inițializat, trebuie să verificăm orice foi de stil în așteptare în wrapper-ul curent și să le tragem în Shadow DOM.
Starea actuală a importului foii de stil globale este următoarea:
- Importăm o foaie de stil care trebuie adăugată în Shadow DOM. Foaia de stil poate fi importată fie în componenta Alien în sine, fie, în mod explicit, în wrapper-ul Frankenstein. În cazul migrării către React, de exemplu, importul este inițializat din wrapper. Cu toate acestea, în migrarea către Vue, componenta similară în sine importă foaia de stil necesară și nu trebuie să importam nimic în wrapper.
- După cum sa menționat mai sus, atunci când Webpack procesează importurile
.css
pentru componenta Alien, datorită opțiunii deinsert
astyle-loader
, foile de stil sunt injectate într-un wrapper Frankenstein, dar în afara Shadow DOM.
Inițializarea simplificată a Shadow DOM în wrapper-ul Frankenstein, ar trebui să arate în prezent (înainte de a introduce orice foi de stil) similar cu acesta:
this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`
Acum, pentru a evita pâlpâirea componentei fără stil, ceea ce trebuie să facem acum este să introducem toate foile de stil necesare după inițializarea Shadow DOM, dar înainte de randarea componentei Alien.
this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})
A fost o explicație lungă, cu o mulțime de detalii, dar, în principal, tot ce este necesar pentru a introduce foile de stil globale în Shadow DOM:
- În configurația Webpack, adăugați
style-loader
cu opțiunea deinsert
care indică către ambalajul Frankenstein necesar. - În wrapper-ul în sine, trageți foile de stil „în așteptare” după inițializarea Shadow DOM, dar înainte de randarea componentei Alien.
După implementarea acestor modificări, componenta dvs. ar trebui să aibă tot ce are nevoie. Singurul lucru pe care ați putea dori (acesta nu este o cerință) să îl adăugați este niște CSS personalizate pentru a regla fin o componentă Alien în mediul Host. S-ar putea chiar să stilați componenta Alien complet diferit atunci când este folosită în Host. Depășește punctul principal al articolului, dar vă uitați la codul final pentru wrapper, unde puteți găsi exemple de cum să suprascrieți stilurile simple la nivel de wrapper.
- Ambalaj Frankenstein pentru componenta React
- Ambalaj Frankenstein pentru componenta Vue
De asemenea, puteți arunca o privire asupra configurației Webpack la acest pas de migrare:
- Migrare la React cu componente cu stil
- Migrare la React cu module CSS
- Migrare la Vue
Și, în sfârșit, componentele noastre arată exact așa cum ne-am propus să arate.
5.5. Rezumatul stilurilor de remediere pentru componenta Alien
Acesta este un moment grozav pentru a rezuma ceea ce am învățat în acest capitol până acum. S-ar putea să pară că a trebuit să facem o muncă enormă pentru a repara stilul componentei Alien; cu toate acestea, totul se rezumă la:
- Repararea stilurilor combinate implementate cu componente stilate în modulele React sau CSS și Scoped CSS în Vue este la fel de simplă ca câteva rânduri în configurația Wrapper Frankenstein sau Webpack.
- Remedierea stilurilor, implementate cu module CSS, începe cu o singură linie în configurația
css-loader
. După aceea, modulele CSS sunt tratate ca o foaie de stil globală. - Remedierea foilor de stil globale necesită configurarea pachetului
style-loader
cu opțiunea deinsert
în Webpack și actualizarea wrapper-ului Frankenstein pentru a introduce foile de stil în Shadow DOM la momentul potrivit al ciclului de viață al wrapper-ului.
La urma urmei, am migrat o componentă Alien corect stilată în gazdă. Totuși, există un singur lucru care ar putea sau nu să vă deranjeze, în funcție de cadru în care migrați.
În primul rând, vești bune: dacă migrați la Vue , demonstrația ar trebui să funcționeze bine și ar trebui să puteți adăuga noi elemente de făcut din componenta Vue migrată. Cu toate acestea, dacă migrați la React și încercați să adăugați un nou lucru de făcut, nu veți reuși. Adăugarea de elemente noi pur și simplu nu funcționează și nicio intrare nu este adăugată în listă. Dar de ce? Care este problema? Fără prejudecăți, dar React are propriile sale opinii asupra unor lucruri.
5.6. Evenimente React și JS în Shadow DOM
Indiferent de ce vă spune documentația React, React nu este foarte prietenos cu componentele web. Simplitatea exemplului din documentație nu suportă nicio critică și ceva mai complicat decât redarea unui link în Componenta Web necesită unele cercetări și investigații.
După cum ați văzut în timp ce remediați stilul pentru componenta noastră Alien, spre deosebire de Vue, unde lucrurile se potrivesc cu componentele web aproape din cutie, React nu este atât de pregătit pentru componente web. Deocamdată, înțelegem cum să facem ca componentele React să arate cel puțin bine în cadrul componentelor web, dar există și funcționalități și evenimente JavaScript de remediat.
Pe scurt: Shadow DOM încapsulează evenimentele și le redirecționează, în timp ce React nu acceptă acest comportament al Shadow DOM în mod nativ și, prin urmare, nu prinde evenimentele care provin din Shadow DOM. Există motive mai profunde pentru acest comportament și există chiar o problemă deschisă în instrumentul de urmărire a erorilor React, dacă doriți să explorați mai multe detalii și discuții.
Din fericire, oamenii deștepți au pregătit o soluție pentru noi. @josephnvu a oferit baza soluției, iar Lukas Bombach a convertit-o în modul react-shadow-dom-retarget-events
npm. Deci, puteți instala pachetul, urmați instrucțiunile de pe pagina pachetelor, actualizați codul wrapper-ului și componenta dvs. Alien va începe ca magic să funcționeze:
import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);
Dacă doriți să-l aveți mai performant, puteți face o copie locală a pachetului (licența MIT permite acest lucru) și puteți limita numărul de evenimente de ascultat așa cum se face în depozitul Frankenstein Demo. Pentru acest exemplu, știu ce evenimente trebuie să redirecționez și le specific doar pe acelea.
Cu aceasta, am terminat în sfârșit (știu că a fost un proces lung) migrarea corectă a primei componente Alien stilate și complet funcționale. Ia-ți o băutură bună. O meriți!
6. Clătiți și repetați pentru toate componentele dvs
După ce am migrat prima componentă, ar trebui să repetăm procesul pentru toate componentele noastre. În cazul lui Frankenstein Demo, a mai rămas însă doar unul: cel responsabil de redarea listei de lucruri de făcut.
Ambalaje noi pentru componente noi
Să începem cu adăugarea unui nou înveliș. Urmând convenția de denumire, discutată mai sus (deoarece componenta noastră React se numește MainSection.js
), wrapper-ul corespunzător în migrarea către React ar trebui să fie numit MainSection-wrapper.js
. În același timp, o componentă similară în Vue se numește Listing.vue
, prin urmare wrapper-ul corespunzător în migrarea către Vue ar trebui să fie numit Listing-wrapper.js
. Cu toate acestea, indiferent de convenția de denumire, wrapper-ul în sine va fi aproape identic cu cel pe care îl avem deja:
- Wrapper pentru listarea React
- Wrapper pentru listarea Vue
Există un singur lucru interesant pe care îl introducem în această a doua componentă a aplicației React. Uneori, din acest motiv sau altul, s-ar putea să doriți să utilizați un plugin jQuery în componentele dvs. În cazul componentei noastre React, am introdus două lucruri:
- Plugin Tooltip de la Bootstrap care folosește jQuery,
- O comutare pentru clasele CSS precum
.addClass()
și.removeClass()
.
Notă : Această utilizare a jQuery pentru adăugarea/eliminarea claselor este pur ilustrativă. Vă rugăm să nu utilizați jQuery pentru acest scenariu în proiecte reale - bazează-te pe JavaScript simplu.
Desigur, ar putea părea ciudat să introducem jQuery într-o componentă Alien atunci când migrăm de la jQuery, dar gazda dvs. ar putea fi diferită de gazda din acest exemplu - ați putea migra din AngularJS sau orice altceva. De asemenea, funcționalitatea jQuery într-o componentă și jQuery global nu sunt neapărat același lucru.
Cu toate acestea, problema este că, chiar dacă confirmați că acea componentă funcționează bine în contextul aplicației dvs. Alien, când o puneți în Shadow DOM, pluginurile jQuery și alt cod care se bazează pe jQuery pur și simplu nu vor funcționa.
jQuery în Shadow DOM
Să aruncăm o privire la o inițializare generală a unui plugin jQuery aleatoriu:
$('.my-selector').fancyPlugin();
În acest fel, toate elementele cu .my-selector
vor fi procesate de fancyPlugin
. Această formă de inițializare presupune că .my-selector
este prezent în DOM global. Cu toate acestea, odată ce un astfel de element este introdus în Shadow DOM, la fel ca în cazul stilurilor, limitele de umbră împiedică jQuery să se strecoare în el. Ca rezultat, jQuery nu poate găsi elemente în Shadow DOM.
Soluția este de a oferi un al doilea parametru opțional selectorului care definește elementul rădăcină din care jQuery să caute. Și aici putem furniza shadowRoot
-ul nostru.
$('.my-selector', this.shadowRoot).fancyPlugin();
În acest fel, selectoarele jQuery și, ca rezultat, pluginurile vor funcționa foarte bine.
Rețineți totuși că componentele Alien sunt destinate să fie utilizate atât: în Alien fără shadow DOM, cât și în Host în Shadow DOM. Prin urmare, avem nevoie de o soluție mai unificată care să nu asume prezența Shadow DOM în mod implicit.
Analizând componenta MainSection
din aplicația noastră React, constatăm că setează proprietatea documentRoot
.
... this.documentRoot = this.props.root? this.props.root: document; ...
Deci, verificăm proprietatea root
transmisă și, dacă există, aceasta este ceea ce folosim ca documentRoot
. În caz contrar, revenim la document
.
Iată inițializarea pluginului tooltip care utilizează această proprietate:
$('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });
Ca bonus, folosim aceeași proprietate root
pentru a defini un container pentru injectarea tooltip în acest caz.
Acum, când componenta Alien este gata să accepte proprietatea root
, actualizăm redarea componentei în wrapper-ul Frankenstein corespunzător:
// `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);
Si asta e! Componenta funcționează la fel de bine în Shadow DOM ca și în DOM global.
Configurare Webpack pentru scenariul multi-wrappers
Partea interesantă se întâmplă în configurația Webpack atunci când se folosesc mai multe wrapper-uri. Nimic nu se schimbă pentru stilurile incluse, cum ar fi acele module CSS din componentele Vue sau componentele cu stil din React. Cu toate acestea, stilurile globale ar trebui să se răsucească acum.
Amintiți-vă, am spus că style-loader
(responsabil pentru injectarea foilor de stil globale în Shadow DOM corect) este inflexibil, deoarece este nevoie de doar un selector la un moment dat pentru opțiunea de insert
. Aceasta înseamnă că ar trebui să împărțim regula .css
în Webpack pentru a avea o regulă secundară per wrapper, folosind regula oneOf
sau similar, dacă sunteți într-un bundler altul decât Webpack.
Este întotdeauna mai ușor de explicat folosind un exemplu, așa că haideți să vorbim despre cel de la migrarea la Vue de data aceasta (cel din migrarea către React, totuși, este aproape identic):
... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...
Am exclus css-loader
deoarece configurația sa este aceeași în toate cazurile. Să vorbim în schimb despre style-loader
. În această configurație, inserăm eticheta <style>
fie în *-header-*
, fie în *-listing- *-listing-*
, în funcție de numele fișierului care solicită acea foaie de stil (regula issuer
în Webpack). Dar trebuie să ne amintim că foaia de stil globală necesară pentru redarea unei componente Alien poate fi importată în două locuri:
- Componenta extraterestră în sine,
- Un ambalaj Frankenstein.
Și aici, ar trebui să apreciem convenția de denumire pentru wrapper, descrisă mai sus, atunci când numele unei componente Alien și un wrapper corespunzător se potrivesc. Dacă, de exemplu, avem o foaie de stil, importată într-o componentă Vue numită Header.vue
, aceasta ajunge să corecteze wrapper *-header-*
. În același timp, dacă importăm, în schimb, foaia de stil în wrapper, o astfel de foaie de stil urmează exact aceeași regulă dacă wrapper-ul se numește Header-wrapper.js
fără nicio modificare a configurației. Același lucru pentru componenta Listing.vue
și wrapper-ul corespunzător Listing-wrapper.js
. Folosind această convenție de denumire, reducem configurația din bundler-ul nostru.
După ce toate componentele dvs. au migrat, este timpul pentru etapa finală a migrării.
7. Comutați la Alien
La un moment dat, aflați că componentele pe care le-ați identificat chiar la primul pas al migrării sunt toate înlocuite cu ambalaje Frankenstein. Nicio aplicație jQuery nu a mai rămas cu adevărat și ceea ce aveți este, în esență, aplicația Alien care este lipită împreună folosind mijloacele Host.
De exemplu, partea de conținut a index.html
din aplicația jQuery — după migrarea ambelor microservicii — arată cam așa acum:
<section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>
În acest moment, nu are rost să păstrăm aplicația noastră jQuery: în schimb, ar trebui să trecem la aplicația Vue și să uităm de toate wrapper-urile noastre, Shadow DOM și configurațiile fantastice Webpack. Pentru a face acest lucru, avem o soluție elegantă.
Să vorbim despre solicitările HTTP. Voi menționa aici configurația Apache, dar acesta este doar un detaliu de implementare: efectuarea comutării în Nginx sau orice altceva ar trebui să fie la fel de banală ca și în Apache.
Imaginează-ți că site-ul tău este servit din folderul /var/www/html
de pe server. În acest caz, httpd.conf
sau httpd-vhost.conf
ar trebui să aibă o intrare care să indice acel folder, cum ar fi:
DocumentRoot "/var/www/html"
Pentru a vă schimba aplicația după migrarea Frankenstein de la jQuery la React, tot ce trebuie să faceți este să actualizați intrarea DocumentRoot
la ceva de genul:
DocumentRoot "/var/www/html/react/build"
Construiește-ți aplicația Alien, repornește-ți serverul, iar aplicația ta este servită direct din folderul Alien: aplicația React este servită din folderul react/
. Cu toate acestea, același lucru este valabil și pentru Vue, desigur, sau pentru orice alt cadru pe care l-ați migrat. Acesta este motivul pentru care este atât de vital să păstrați gazda și extraterestră complet independenți și funcționali în orice moment, deoarece extratereștriul dvs. devine gazdă la acest pas.
Acum puteți elimina în siguranță tot ce este în jurul dosarului dvs. Alien, inclusiv toate Shadow DOM, ambalajele Frankenstein și orice alt artefact legat de migrare. A fost o cale grea uneori, dar v-ați migrat site-ul. Felicitări!
Concluzie
Cu siguranță am trecut prin teren oarecum accidentat în acest articol. Cu toate acestea, după ce am început cu o aplicație jQuery, am reușit să o migrăm atât în Vue, cât și în React. Pe parcurs am descoperit câteva probleme neașteptate și nu atât de banale: a trebuit să reparăm stilul, a trebuit să reparăm funcționalitatea JavaScript, să introducem câteva configurații ale pachetelor și multe altele. Cu toate acestea, ne-a oferit o imagine de ansamblu mai bună asupra la ce să ne așteptăm în proiecte reale. În cele din urmă, avem o aplicație contemporană fără biți rămași din aplicația jQuery, deși aveam toate drepturile de a fi sceptici cu privire la rezultatul final în timp ce migrarea era în curs.
Migrația Frankenstein nu este nici un glonț de argint și nici nu ar trebui să fie un proces înfricoșător. Este doar algoritmul definit, aplicabil multor proiecte, care ajută la transformarea proiectelor în ceva nou și robust într-un mod previzibil.