Frankenstein-Migration: Framework-unabhängiger Ansatz (Teil 2)
Veröffentlicht: 2022-03-10In diesem Artikel stellen wir die gesamte Theorie auf die Probe, indem wir eine schrittweise Migration einer Anwendung gemäß den Empfehlungen aus dem vorherigen Teil durchführen. Um die Dinge einfach zu machen, Unsicherheiten, Unbekanntes und unnötiges Raten zu reduzieren, habe ich mich für das praktische Beispiel der Migration entschieden, die Praxis an einer einfachen To-do-Anwendung zu demonstrieren.
Im Allgemeinen gehe ich davon aus, dass Sie ein gutes Verständnis dafür haben, wie eine generische To-Do-Anwendung funktioniert. Diese Art von Anwendung passt sehr gut zu unseren Anforderungen: Sie ist vorhersehbar, hat aber dennoch eine minimale praktikable Anzahl erforderlicher Komponenten, um verschiedene Aspekte der Frankenstein-Migration zu demonstrieren. Unabhängig von der Größe und Komplexität Ihrer realen Anwendung ist der Ansatz jedoch gut skalierbar und soll für Projekte jeder Größe geeignet sein.
Für diesen Artikel habe ich als Ausgangspunkt eine jQuery-Anwendung aus dem TodoMVC-Projekt ausgewählt – ein Beispiel, das vielen von Ihnen vielleicht bereits bekannt ist. jQuery ist veraltet genug, spiegelt möglicherweise eine reale Situation in Ihren Projekten wider und erfordert vor allem erhebliche Wartungsarbeiten und Hacks, um eine moderne dynamische Anwendung zu betreiben. (Dies sollte ausreichen, um eine Migration zu etwas Flexiblerem in Betracht zu ziehen.)
Was ist dieses „Flexiblere“, zu dem wir dann migrieren werden? Um einen praxisnahen Fall zu zeigen, der im wirklichen Leben nützlich ist, musste ich mich zwischen den zwei beliebtesten Frameworks dieser Tage entscheiden: React und Vue. Wie auch immer ich mich entscheiden würde, wir würden einige Aspekte der anderen Richtung vermissen.
In diesem Teil werden wir also die beiden folgenden durchlaufen:
- Eine Migration einer jQuery-Anwendung zu React und
- Eine Migration einer jQuery-Anwendung zu Vue .
Code-Repositories
Der gesamte hier erwähnte Code ist öffentlich verfügbar und Sie können jederzeit darauf zugreifen. Es stehen Ihnen zwei Repositories zum Spielen zur Verfügung:
- Frankenstein TodoMVC
Dieses Repository enthält TodoMVC- Anwendungen in verschiedenen Frameworks/Bibliotheken. In diesem Repository finden Sie beispielsweise Zweige wievue
,angularjs
,react
undjquery
. - Frankenstein-Demo
Es enthält mehrere Zweige, von denen jeder eine bestimmte Migrationsrichtung zwischen Anwendungen darstellt, die im ersten Repository verfügbar sind. Es gibt insbesondere Zweige wiemigration/jquery-to-react
undmigration/jquery-to-vue
, die wir später behandeln werden.
Beide Repositories sind in Arbeit und es sollten regelmäßig neue Zweige mit neuen Anwendungen und Migrationsrichtungen hinzugefügt werden. (Es steht Ihnen auch frei, etwas beizutragen! ) Der Commit-Verlauf in Migrationszweigen ist gut strukturiert und könnte als zusätzliche Dokumentation mit noch mehr Details dienen, als ich in diesem Artikel behandeln könnte.
Jetzt machen wir uns die Hände schmutzig! Wir haben einen langen Weg vor uns, also erwartet nicht, dass es eine reibungslose Fahrt wird. Es liegt an Ihnen zu entscheiden, wie Sie diesem Artikel folgen möchten, aber Sie könnten Folgendes tun:
- Klonen Sie den
jquery
Zweig aus dem Frankenstein TodoMVC-Repository und befolgen Sie strikt alle nachstehenden Anweisungen. - Alternativ können Sie einen Zweig öffnen, der entweder der Migration zu React oder der Migration zu Vue aus dem Frankenstein-Demo-Repository gewidmet ist, und den Verlauf der Commits verfolgen.
- Alternativ können Sie sich entspannen und weiterlesen, da ich hier den kritischsten Code hervorheben werde und es viel wichtiger ist, die Mechanik des Prozesses zu verstehen, als den eigentlichen Code.
Ich möchte noch einmal erwähnen, dass wir uns strikt an die im theoretischen ersten Teil des Artikels vorgestellten Schritte halten werden.
Lass uns gleich eintauchen!
- Identifizieren Sie Microservices
- Host-zu-Alien-Zugriff zulassen
- Schreiben Sie einen fremden Microservice/eine fremde Komponente
- Schreiben Sie einen Webkomponenten-Wrapper um den Alien-Dienst
- Hostdienst durch Webkomponente ersetzen
- Spülen und wiederholen Sie dies für alle Ihre Komponenten
- Wechseln Sie zu Alien
1. Identifizieren Sie Microservices
Wie in Teil 1 vorgeschlagen, müssen wir in diesem Schritt unsere Anwendung in kleine , unabhängige Dienste strukturieren, die jeweils einem bestimmten Job gewidmet sind. Dem aufmerksamen Leser wird vielleicht auffallen, dass unsere To-do-Anwendung bereits klein und eigenständig ist und einen einzigen Microservice für sich darstellen kann. So würde ich es selbst behandeln, wenn diese Anwendung in einem breiteren Kontext stehen würde. Denken Sie jedoch daran, dass der Prozess der Identifizierung von Microservices völlig subjektiv ist und es nicht die eine richtige Antwort gibt.
Um den Prozess der Frankenstein-Migration genauer zu sehen, können wir also noch einen Schritt weiter gehen und diese To-Do-Anwendung in zwei unabhängige Microservices aufteilen:
- Ein Eingabefeld zum Hinzufügen eines neuen Elements.
Dieser Dienst kann auch den Header der Anwendung enthalten, basierend auf der reinen Positionierungsnähe dieser Elemente. - Eine Liste bereits hinzugefügter Elemente.
Dieser Dienst ist fortgeschrittener und enthält zusammen mit der Liste selbst auch Aktionen wie Filtern, Aktionen von Listenelementen und so weiter.
Tipp : Um zu überprüfen, ob die ausgewählten Dienste wirklich unabhängig sind, entfernen Sie das HTML-Markup, das jeden dieser Dienste darstellt. Stellen Sie sicher, dass die restlichen Funktionen noch funktionieren. In unserem Fall sollte es möglich sein, neue Einträge in localStorage
(den diese Anwendung als Speicher verwendet) aus dem Eingabefeld ohne die Liste hinzuzufügen, während die Liste weiterhin die Einträge aus localStorage
, selbst wenn das Eingabefeld fehlt. Wenn Ihre Anwendung Fehler auslöst, wenn Sie Markup für potenzielle Microservices entfernen, sehen Sie sich den Abschnitt „Umgestalten bei Bedarf“ in Teil 1 an, um ein Beispiel für den Umgang mit solchen Fällen zu erhalten.
Natürlich könnten wir weitermachen und den zweiten Dienst und die Auflistung der Artikel noch weiter in unabhängige Microservices für jeden einzelnen Artikel aufteilen. Für dieses Beispiel könnte es jedoch zu granular sein. Daher kommen wir vorerst zu dem Schluss, dass unsere Anwendung zwei Dienste haben wird; sie sind unabhängig, und jeder von ihnen arbeitet an seiner eigenen besonderen Aufgabe. Daher haben wir unsere Anwendung in Microservices aufgeteilt.
2. Host-zu-Alien-Zugriff zulassen
Lassen Sie mich kurz daran erinnern, welche das sind.
- Gastgeber
So heißt unsere aktuelle Bewerbung. Es ist mit dem Rahmen geschrieben, von dem wir uns gerade entfernen werden. In diesem speziellen Fall unsere jQuery-Anwendung. - Außerirdischer
Einfach ausgedrückt handelt es sich hier um eine schrittweise Umschreibung von Host auf das neue Framework, zu dem wir gerade wechseln werden . Auch in diesem speziellen Fall handelt es sich um eine React- oder Vue-Anwendung.
Die Faustregel beim Aufteilen von Host und Alien lautet, dass Sie in der Lage sein sollten, jeden von ihnen zu entwickeln und einzusetzen, ohne den anderen zu beschädigen – zu jedem Zeitpunkt.
Die Unabhängigkeit von Host und Alien ist für die Frankenstein-Migration von entscheidender Bedeutung. Dies macht es jedoch etwas schwierig, die Kommunikation zwischen den beiden zu arrangieren. Wie erlauben wir Host-Zugriff auf Alien, ohne die beiden zusammenzuschlagen?
Hinzufügen von Alien als Submodul Ihres Hosts
Obwohl es mehrere Möglichkeiten gibt, das benötigte Setup zu erreichen, ist die einfachste Form, Ihr Projekt zu organisieren, um dieses Kriterium zu erfüllen, wahrscheinlich Git-Submodule. Dies werden wir in diesem Artikel verwenden. Ich überlasse es Ihnen, sorgfältig zu lesen, wie Submodule in Git funktionieren, um die Einschränkungen und Fallstricke dieser Struktur zu verstehen.
Die allgemeinen Prinzipien der Architektur unseres Projekts mit Git-Submodulen sollten wie folgt aussehen:
- Sowohl Host als auch Alien sind unabhängig und werden in separaten
git
Repositories aufbewahrt; - Host verweist auf Alien als Submodul. In diesem Stadium wählt der Host einen bestimmten Status (Commit) von Alien aus und fügt ihn als, wie es aussieht, einen Unterordner in der Ordnerstruktur des Hosts hinzu.
Der Prozess zum Hinzufügen eines Submoduls ist für jede Anwendung gleich. Das Unterrichten von git submodules
würde den Rahmen dieses Artikels sprengen und steht nicht in direktem Zusammenhang mit der Frankenstein-Migration selbst. Schauen wir uns also kurz die möglichen Beispiele an.
In den folgenden Snippets verwenden wir die React-Richtung als Beispiel. Ersetzen Sie für jede andere Migrationsrichtung „ react
“ durch den Namen einer Verzweigung von Frankenstein TodoMVC oder passen Sie sie bei Bedarf an benutzerdefinierte Werte an.
Wenn Sie mit der ursprünglichen jQuery TodoMVC-Anwendung folgen:
$ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i
Wenn Sie dem migration/jquery-to-react
(oder einer anderen Migrationsrichtung) aus dem Frankenstein-Demo-Repository folgen, sollte die Alien-Anwendung dort bereits als git submodule
enthalten sein, und Sie sollten einen entsprechenden Ordner sehen. Der Ordner ist jedoch standardmäßig leer, und Sie müssen die registrierten Submodule aktualisieren und initialisieren.
Von der Wurzel Ihres Projekts (Ihrem Host):
$ git submodule update --init $ cd react $ npm i
Beachten Sie, dass wir in beiden Fällen Abhängigkeiten für die Alien-Anwendung installieren, diese jedoch in den Unterordner sandboxed werden und unseren Host nicht verschmutzen.
Nachdem Sie die Alien-Anwendung als Submodul Ihres Hosts hinzugefügt haben, erhalten Sie (in Bezug auf Microservices) unabhängige Alien- und Host-Anwendungen. Host betrachtet Alien in diesem Fall jedoch als Unterordner, und das ermöglicht Host offensichtlich, problemlos auf Alien zuzugreifen.
3. Schreiben Sie einen fremden Microservice/eine fremde Komponente
In diesem Schritt müssen wir entscheiden, welcher Microservice zuerst migriert werden soll, und ihn auf der Seite des Aliens schreiben/verwenden. Lassen Sie uns der gleichen Reihenfolge von Diensten folgen, die wir in Schritt 1 identifiziert haben, und beginnen Sie mit dem ersten: Eingabefeld zum Hinzufügen eines neuen Elements. Bevor wir beginnen, stimmen wir jedoch zu, dass wir über diesen Punkt hinaus einen günstigeren Begriff Komponente anstelle von Microservice oder Service verwenden werden, da wir uns in Richtung der Prämissen von Frontend-Frameworks bewegen und der Begriff Komponente den Definitionen von so ziemlich jedem modernen folgt Rahmen.
Zweige des Frankenstein TodoMVC-Repositorys enthalten eine resultierende Komponente, die den ersten Dienst „Eingabefeld zum Hinzufügen eines neuen Elements“ als Header-Komponente darstellt:
- Header-Komponente in React
- Header-Komponente in Vue
Das Schreiben von Komponenten im Rahmen Ihrer Wahl würde den Rahmen dieses Artikels sprengen und ist nicht Teil der Frankenstein-Migration. Beim Schreiben einer Alien-Komponente sind jedoch einige Dinge zu beachten.
Unabhängigkeit
Zunächst einmal sollten die Komponenten in Alien dem gleichen Prinzip der Unabhängigkeit folgen, das zuvor auf der Seite des Hosts eingerichtet wurde: Komponenten sollten in keiner Weise von anderen Komponenten abhängen.
Interoperabilität
Dank der Unabhängigkeit der Dienste kommunizieren Komponenten in Ihrem Host höchstwahrscheinlich auf etablierte Weise, sei es ein Zustandsverwaltungssystem, die Kommunikation über einen gemeinsam genutzten Speicher oder direkt über ein System von DOM-Ereignissen. „Interoperabilität“ von Alien-Komponenten bedeutet, dass sie in der Lage sein sollten, sich mit derselben vom Host eingerichteten Kommunikationsquelle zu verbinden, um Informationen über ihre Zustandsänderungen zu versenden und auf Änderungen in anderen Komponenten zu hören. In der Praxis bedeutet dies, dass, wenn Komponenten in Ihrem Host über DOM-Ereignisse kommunizieren, das Erstellen Ihrer Alien-Komponente ausschließlich mit Blick auf die Zustandsverwaltung für diese Art der Migration leider nicht fehlerfrei funktioniert.
Sehen Sie sich als Beispiel die Datei js/storage.js
an, die der primäre Kommunikationskanal für unsere jQuery-Komponenten ist:
... 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); }, ...
Hier verwenden wir localStorage
(da dieses Beispiel nicht sicherheitskritisch ist), um unsere To-Do-Elemente zu speichern, und sobald die Änderungen am Speicher aufgezeichnet wurden, senden wir ein benutzerdefiniertes DOM-Ereignis für das document
, auf das jede Komponente hören kann.
Gleichzeitig können wir auf der Seite des Aliens (sagen wir React) eine so komplexe State-Management-Kommunikation einrichten, wie wir wollen. Es ist jedoch wahrscheinlich klug, es für die Zukunft aufzubewahren: Um unsere Alien React-Komponente erfolgreich in Host zu integrieren, müssen wir uns mit demselben Kommunikationskanal verbinden, der von Host verwendet wird. In diesem Fall ist es localStorage
. Der Einfachheit halber haben wir einfach die Speicherdatei von Host in Alien kopiert und unsere Komponenten daran angeschlossen:
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); } } ... }
Jetzt können unsere Alien-Komponenten dieselbe Sprache mit Host-Komponenten sprechen und umgekehrt.
4. Schreiben Sie einen Webkomponenten-Wrapper um den Alien-Dienst
Auch wenn wir jetzt erst auf der vierten Stufe sind, haben wir schon einiges erreicht:
- Wir haben unsere Host-Anwendung in unabhängige Dienste aufgeteilt, die bereit sind, durch Alien-Dienste ersetzt zu werden;
- Wir haben Host und Alien so eingerichtet, dass sie völlig unabhängig voneinander sind, aber dennoch sehr gut über
git submodules
verbunden sind. - Wir haben unsere erste Alien-Komponente mit dem neuen Framework geschrieben.
Jetzt ist es an der Zeit, eine Brücke zwischen Host und Alien einzurichten, damit die neue Alien-Komponente im Host funktionieren kann.
Erinnerung aus Teil 1 : Stellen Sie sicher, dass Ihr Host einen Paket-Bundler zur Verfügung hat. In diesem Artikel verlassen wir uns auf Webpack, aber das bedeutet nicht, dass die Technik nicht mit Rollup oder einem anderen Bundler Ihrer Wahl funktioniert. Das Mapping von Webpack überlasse ich jedoch Ihren Experimenten.
Namenskonvention
Wie im vorherigen Artikel erwähnt, werden wir Webkomponenten verwenden, um Alien in Host zu integrieren. Auf der Seite des Hosts erstellen wir eine neue Datei: js/frankenstein-wrappers/Header-wrapper.js
. (Es wird unser erster Frankenstein-Wrapper.) Denken Sie daran, dass es eine gute Idee ist, Ihre Wrapper genauso zu benennen wie Ihre Komponenten in der Alien-Anwendung, zB einfach durch Hinzufügen eines „ -wrapper
“-Suffixes. Sie werden später sehen, warum dies eine gute Idee ist, aber stimmen wir zunächst einmal zu, dass dies bedeutet, dass, wenn die Alien-Komponente Header.js
(in React) oder Header.vue
(in Vue) heißt, der entsprechende Wrapper auf der Die Seite des Hosts sollte Header-wrapper.js
.
In unserem ersten Wrapper beginnen wir mit der grundlegenden Boilerplate für die Registrierung eines benutzerdefinierten Elements:
class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);
Als nächstes müssen wir Shadow DOM für dieses Element initialisieren.
Bitte beziehen Sie sich auf Teil 1, um zu begründen, warum wir Shadow DOM verwenden.
class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }
Damit haben wir alle wesentlichen Teile der Web-Komponente eingerichtet, und es ist an der Zeit, unsere Alien-Komponente in die Mischung aufzunehmen. Zunächst sollten wir zu Beginn unseres Frankenstein-Wrappers alle Bits importieren, die für das Rendern der Alien-Komponente verantwortlich sind.
import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...
Hier müssen wir kurz innehalten. Beachten Sie, dass wir die Abhängigkeiten von Alien nicht aus den node_modules
von Host importieren. Alles kommt vom Alien selbst, das sich im Unterordner „ react/
“ befindet. Aus diesem Grund ist Schritt 2 so wichtig, und es ist entscheidend sicherzustellen, dass der Host vollen Zugriff auf die Assets von Alien hat.
Jetzt können wir unsere Alien-Komponente im Shadow-DOM der Web-Komponente rendern:
... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...
Hinweis : In diesem Fall benötigt React nichts anderes. Um die Vue-Komponente zu rendern, müssen Sie jedoch einen Wrapping-Knoten hinzufügen, der Ihre Vue-Komponente wie den folgenden enthält:
... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...
Der Grund dafür ist der Unterschied, wie React und Vue Komponenten rendern: React hängt die Komponente an den referenzierten DOM-Knoten an, während Vue den referenzierten DOM-Knoten durch die Komponente ersetzt. Wenn wir also .$mount(this.shadowRoot)
für Vue machen, ersetzt es im Wesentlichen das Shadow DOM.
Das ist alles, was wir vorerst mit unserem Wrapper machen müssen. Das aktuelle Ergebnis für den Frankenstein-Wrapper sowohl in jQuery-to-React- als auch in jQuery-to-Vue-Migrationsrichtungen finden Sie hier:
- Frankenstein Wrapper für React-Komponente
- Frankenstein Wrapper für die Vue-Komponente
Um die Mechanik des Frankenstein-Wrappers zusammenzufassen:
- Erstellen Sie ein benutzerdefiniertes Element,
- Schatten-DOM starten,
- Importieren Sie alles, was zum Rendern einer Alien-Komponente benötigt wird,
- Rendern Sie die Alien-Komponente im Shadow-DOM des benutzerdefinierten Elements.
Dies rendert unser Alien in Host jedoch nicht automatisch. Wir müssen das vorhandene Host-Markup durch unseren neuen Frankenstein-Wrapper ersetzen.
Schnallen Sie sich an, es ist vielleicht nicht so einfach, wie man erwarten würde!
5. Ersetzen Sie den Hostdienst durch die Webkomponente
Fahren wir fort und fügen unsere neue Header-wrapper.js
Datei zu index.html
hinzu und ersetzen das vorhandene Header-Markup durch das neu erstellte benutzerdefinierte <frankenstein-header-wrapper>
-Element.
... <!-- <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>
Leider wird das so einfach nicht funktionieren. Wenn Sie einen Browser öffnen und die Konsole überprüfen, wartet dort der Uncaught SyntaxError
auf Sie. Abhängig vom Browser und seiner Unterstützung für ES6-Module hängt es entweder mit ES6-Importen zusammen oder mit der Art und Weise, wie die Alien-Komponente gerendert wird. So oder so, wir müssen etwas dagegen tun, aber das Problem und die Lösung sollten den meisten Lesern bekannt und klar sein.
5.1. Aktualisieren Sie Webpack und Babel bei Bedarf
Wir sollten etwas Webpack- und Babel-Magie einbeziehen, bevor wir unseren Frankenstein-Wrapper integrieren. Das Herumspielen mit diesen Tools würde den Rahmen dieses Artikels sprengen, aber Sie können sich die entsprechenden Commits im Frankenstein-Demo-Repository ansehen:
- Konfiguration für die Migration zu React
- Konfiguration für die Migration zu Vue
Im Wesentlichen haben wir die Verarbeitung der Dateien sowie einen neuen Einstiegspunkt frankenstein
in der Webpack-Konfiguration eingerichtet , um alles, was mit Frankenstein-Wrappern zu tun hat, an einem Ort zu enthalten.
Sobald Webpack in Host weiß, wie die Alien-Komponente und die Webkomponenten verarbeitet werden, sind wir bereit, das Markup von Host durch den neuen Frankenstein-Wrapper zu ersetzen.
5.2. Austausch der tatsächlichen Komponente
Der Austausch der Komponente sollte jetzt einfach sein. Gehen Sie in index.html
Ihres Hosts wie folgt vor:
- Ersetzen Sie
<header class="header">
DOM-Element durch<frankenstein-header-wrapper>
; - Fügen Sie ein neues Skript
frankenstein.js
. Dies ist der neue Einstiegspunkt in Webpack, der alles enthält, was mit Frankenstein-Wrappern zu tun hat.
... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>
Das ist es! Starten Sie Ihren Server bei Bedarf neu und werden Sie Zeuge der Magie der in Host integrierten Alien-Komponente.
Irgendetwas schien jedoch noch zu fehlen. Die Alien-Komponente im Host-Kontext sieht nicht so aus wie im Kontext der eigenständigen Alien-Anwendung. Es ist einfach ungestylt.
Wieso ist es so? Sollten die Stile der Komponente nicht automatisch mit der Alien-Komponente in Host integriert werden? Ich wünschte, sie würden es tun, aber wie in zu vielen Situationen kommt es darauf an. Wir kommen zum herausfordernden Teil der Frankenstein-Migration.
5.3. Allgemeine Informationen zum Styling der Alien-Komponente
Zunächst einmal ist die Ironie, dass es keinen Fehler in der Funktionsweise gibt. Alles ist so konzipiert, dass es funktioniert. Um dies zu erklären, lassen Sie uns kurz verschiedene Arten des Stylings von Komponenten erwähnen.
Globale Stile
Wir alle sind damit vertraut: Globale Stile können (und werden normalerweise) ohne eine bestimmte Komponente verteilt und auf die gesamte Seite angewendet. Globale Stile wirken sich auf alle DOM-Knoten mit übereinstimmenden Selektoren aus.
Einige Beispiele für globale Stile sind die Tags <style>
und <link rel="stylesheet">
in Ihrer index.html
. Alternativ kann ein globales Stylesheet in ein Root-JS-Modul importiert werden, sodass alle Komponenten ebenfalls darauf zugreifen können.
Das Problem, Anwendungen auf diese Weise zu gestalten, ist offensichtlich: Die Pflege monolithischer Stylesheets für große Anwendungen wird sehr schwierig. Wie wir im vorherigen Artikel gesehen haben, können globale Stile auch leicht Komponenten zerstören, die direkt im Haupt-DOM-Baum gerendert werden, wie es in React oder Vue der Fall ist.
Gebündelte Stile
Diese Stile sind normalerweise eng mit einer Komponente selbst gekoppelt und werden selten ohne die Komponente verteilt. Die Stile befinden sich normalerweise in derselben Datei wie die Komponente. Gute Beispiele für diese Art des Stylings sind gestylte Komponenten in React- oder CSS-Modulen und Scoped CSS in einzelnen Dateikomponenten in Vue. Unabhängig von der Vielfalt der Tools zum Schreiben von gebündelten Stilen ist das zugrunde liegende Prinzip in den meisten von ihnen dasselbe: Die Tools bieten einen Bereichsmechanismus zum Sperren von Stilen, die in einer Komponente definiert sind, sodass die Stile andere Komponenten oder global nicht beeinträchtigen Stile.
Warum könnten Scoped Styles zerbrechlich sein?
In Teil 1 haben wir bei der Begründung der Verwendung von Shadow DOM in der Frankenstein-Migration kurz das Thema Scoping vs. Encapsulation behandelt und wie sich die Kapselung von Shadow DOM von Scoping-Styling-Tools unterscheidet. Wir haben jedoch nicht erklärt, warum Scoping-Tools ein so zerbrechliches Design für unsere Komponenten bieten, und jetzt, als wir uns der nicht gestylten Alien-Komponente gegenübersahen, wurde sie für das Verständnis unerlässlich.
Alle Scoping-Tools für moderne Frameworks funktionieren ähnlich:
- Sie schreiben auf irgendeine Weise Stile für Ihre Komponente, ohne viel über Umfang oder Kapselung nachzudenken;
- Sie führen Ihre Komponenten mit importierten/eingebetteten Stylesheets über ein Bündelungssystem wie Webpack oder Rollup aus;
- Der Bundler generiert eindeutige CSS-Klassen oder andere Attribute, erstellt und fügt individuelle Selektoren sowohl für Ihr HTML als auch für entsprechende Stylesheets ein;
- Der Bundler erstellt einen
<style>
-Eintrag im<head>
Ihres Dokuments und fügt dort die Stile Ihrer Komponenten mit eindeutigen gemischten Selektoren ein.
Das wars so ziemlich. Es funktioniert und funktioniert in vielen Fällen gut. Außer wenn dies nicht der Fall ist: Wenn Stile für alle Komponenten im globalen Gestaltungsbereich vorhanden sind, wird es einfach, diese zu brechen, z. B. durch Verwendung einer höheren Spezifität. Dies erklärt die potenzielle Fragilität von Scoping-Tools, aber warum ist unsere Alien-Komponente völlig ungestylt?
Werfen wir einen Blick auf den aktuellen Host mithilfe von DevTools. Wenn wir beispielsweise den neu hinzugefügten Frankenstein-Wrapper mit der Alien React-Komponente untersuchen, können wir so etwas sehen:
Webpack generiert also eindeutige CSS-Klassen für unsere Komponente. Toll! Wo sind denn die Styles? Nun, die Stile sind genau dort, wo sie vorgesehen sind – im <head>
des Dokuments.
Also alles funktioniert wie es soll, und das ist das Hauptproblem. Da sich unsere Alien-Komponente in Shadow DOM befindet und wie in Teil 1 erläutert, bietet Shadow DOM eine vollständige Kapselung von Komponenten aus dem Rest der Seite und globalen Stilen, einschließlich der neu generierten Stylesheets für die Komponente, die die Schattengrenze nicht überschreiten kann und gelangen Sie zur Alien-Komponente. Daher bleibt die Alien-Komponente ungestylt. Jetzt sollte jedoch die Taktik zur Lösung des Problems klar sein: Wir sollten die Stile der Komponente irgendwie in demselben Shadow-DOM platzieren, in dem sich unsere Komponente befindet (anstelle des <head>
des Dokuments).
5.4. Festlegen von Stilen für die Alien-Komponente
Bis jetzt war der Migrationsprozess zu jedem Framework derselbe. Allerdings fangen die Dinge hier an zu divergieren: Jedes Framework hat seine Empfehlungen zum Stil von Komponenten, und daher unterscheiden sich die Wege, das Problem anzugehen. Hier besprechen wir die häufigsten Fälle, aber wenn das Framework, mit dem Sie arbeiten, eine einzigartige Methode zum Stylen von Komponenten verwendet, müssen Sie die grundlegenden Taktiken im Hinterkopf behalten, z. B. das Einfügen der Stile der Komponente in Shadow DOM anstelle von <head>
.
In diesem Kapitel behandeln wir Fehlerbehebungen für:
- Gebündelte Stile mit CSS-Modulen in Vue (Taktiken für Scoped CSS sind die gleichen);
- Gebündelte Stile mit Stilkomponenten in React;
- Generische CSS-Module und globale Stile. Ich kombiniere diese, weil CSS-Module im Allgemeinen den globalen Stylesheets sehr ähnlich sind und von jeder Komponente importiert werden können, wodurch die Stile von einer bestimmten Komponente getrennt werden.
Einschränkungen zuerst: Alles, was wir tun, um das Styling zu korrigieren, sollte die Alien-Komponente selbst nicht beschädigen . Andernfalls verlieren wir die Unabhängigkeit unserer Alien- und Host-Systeme. Um das Styling-Problem anzugehen, werden wir uns also entweder auf die Konfiguration des Bundlers oder den Frankenstein-Wrapper verlassen.
Gebündelte Stile in Vue und Shadow DOM
Wenn Sie eine Vue-Anwendung schreiben, verwenden Sie höchstwahrscheinlich einzelne Dateikomponenten. Wenn Sie auch Webpack verwenden, sollten Sie mit den beiden Loadern vue-loader
und vue-style-loader
vertraut sein. Ersteres ermöglicht es Ihnen, diese einzelnen Dateikomponenten zu schreiben, während letzteres das CSS der Komponente dynamisch als <style>
-Tag in ein Dokument einfügt. Standardmäßig vue-style-loader
die Stile der Komponente in den <head>
des Dokuments ein. Beide Pakete akzeptieren jedoch die Option shadowMode
in der Konfiguration, die es uns ermöglicht, das Standardverhalten einfach zu ändern und Stile (wie der Name der Option andeutet) in Shadow DOM einzufügen. Sehen wir es uns in Aktion an.
Webpack-Konfiguration
Die Webpack-Konfigurationsdatei sollte mindestens Folgendes enthalten:
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() ] }
In einer realen Anwendung wird Ihr test: /\.css$/
ausgefeilter sein (wahrscheinlich mit der oneOf
-Regel), um sowohl Host- als auch Alien-Konfigurationen zu berücksichtigen. In diesem Fall ist unsere jQuery jedoch mit einem einfachen <link rel="stylesheet">
in index.html
gestylt, sodass wir keine Styles für Host über Webpack erstellen und es sicher ist, nur für Alien zu sorgen.
Wrapper-Konfiguration
Zusätzlich zur Webpack-Konfiguration müssen wir auch unseren Frankenstein-Wrapper aktualisieren und Vue auf das richtige Shadow-DOM verweisen. In unserer Header-wrapper.js
sollte das Rendern der Vue-Komponente die Eigenschaft shadowRoot
enthalten, die zu shadowRoot
unseres Frankenstein-Wrappers führt:
... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...
Nachdem Sie die Dateien aktualisiert und Ihren Server neu gestartet haben, sollten Sie in Ihren DevTools so etwas erhalten:
Schließlich befinden sich Stile für die Vue-Komponente in unserem Shadow DOM. Gleichzeitig sollte Ihre Bewerbung so aussehen:
Wir erhalten etwas, das unserer Vue-Anwendung ähnelt: Stile, die mit der Komponente gebündelt sind, werden in das Shadow-DOM des Wrappers injiziert, aber die Komponente sieht immer noch nicht so aus, wie sie soll. Der Grund dafür ist, dass in der ursprünglichen Vue-Anwendung die Komponente nicht nur mit den gebündelten Stilen, sondern teilweise auch mit globalen Stilen gestylt wird. Bevor wir jedoch die globalen Stile reparieren, müssen wir unsere React-Integration in den gleichen Zustand wie die Vue-Integration bringen.
Gebündelte Stile in React und Shadow DOM
Da es viele Möglichkeiten gibt, eine React-Komponente zu gestalten, hängt die jeweilige Lösung zum Reparieren einer Alien-Komponente in Frankenstein Migration davon ab, wie wir die Komponente überhaupt gestalten. Lassen Sie uns kurz die am häufigsten verwendeten Alternativen behandeln.
gestylte Komponenten
styled-components ist eine der beliebtesten Arten, React-Komponenten zu stylen. Für die Header React-Komponente ist styled-components genau die Art, wie wir sie stylen. Da dies ein klassischer CSS-in-JS-Ansatz ist, gibt es keine Datei mit einer dedizierten Erweiterung, an der wir unseren Bundler anhängen könnten, wie wir es beispielsweise für .css
oder .js
Dateien tun. Glücklicherweise ermöglichen styled-components die Injektion von Komponentenstilen in einen benutzerdefinierten Knoten (Shadow DOM in unserem Fall) anstelle des head
des Dokuments mit Hilfe der StyleSheetManager
. Es handelt sich um eine vordefinierte Komponente, die mit dem Paket styled-components
installiert wird, das die Eigenschaft target
akzeptiert und „einen alternativen DOM-Knoten zum Einfügen von Stilinformationen“ definiert. Genau das, was wir brauchen! Außerdem müssen wir nicht einmal unsere Webpack-Konfiguration ändern: Alles hängt von unserem Frankenstein-Wrapper ab.
Wir sollten unsere Header-wrapper.js
, die die React Alien-Komponente enthält, mit den folgenden Zeilen aktualisieren:
... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...
Hier importieren wir die StyleSheetManager
Komponente (von Alien und nicht von Host) und packen unsere React-Komponente damit ein. Gleichzeitig senden wir die Eigenschaft target
, die auf unseren shadowRoot
. Das ist es. Wenn Sie den Server neu starten, müssen Sie in Ihren DevTools so etwas sehen:
Jetzt befinden sich die Stile unserer Komponente in Shadow DOM anstelle von <head>
. Auf diese Weise ähnelt das Rendering unserer App jetzt dem, was wir zuvor mit der Vue-App gesehen haben.
Gleiche Geschichte: Stilkomponenten sind nur für den gebündelten Teil der Stile der React-Komponente verantwortlich , und die globalen Stile verwalten die restlichen Teile. Wir kehren gleich zu den globalen Stilen zurück, nachdem wir eine weitere Art von Stilkomponenten besprochen haben.
CSS-Module
Wenn Sie sich die Vue-Komponente, die wir zuvor repariert haben, genauer ansehen, werden Sie vielleicht feststellen, dass CSS-Module genau die Art und Weise sind, wie wir diese Komponente gestalten. 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. Wie machen wir das?
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. Großartige Neuigkeiten! 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.
Globale Stylesheets und insert
des style-loader
Wenn Sie die Dokumentation für diese Option überprüfen, stellen Sie fest, dass diese Option einen Selektor pro Konfiguration benötigt. Das bedeutet, dass Sie, wenn Sie mehrere Alien-Komponenten haben, die globale Stile erfordern, die in einen Frankenstein-Wrapper gezogen werden müssen, style-loader
für jeden der Frankenstein-Wrapper angeben müssen. In der Praxis bedeutet dies, dass Sie sich wahrscheinlich auf die oneOf
-Regel in Ihrem Konfigurationsblock verlassen müssen, um alle Wrapper zu bedienen.
{ 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. ], }
Nicht sehr flexibel, da stimme ich zu. Trotzdem ist es keine große Sache, solange Sie nicht Hunderte von Komponenten migrieren müssen. Andernfalls könnte Ihre Webpack-Konfiguration schwer zu warten sein. Das eigentliche Problem ist jedoch, dass wir keinen CSS-Selektor für Shadow DOM schreiben können.
Beim Versuch, dies zu lösen, stellen wir möglicherweise fest, dass die insert
auch eine Funktion anstelle eines einfachen Selektors annehmen kann, um eine erweiterte Logik für das Einfügen anzugeben. Damit können wir diese Option verwenden, um Stylesheets direkt in Shadow DOM einzufügen! Vereinfacht könnte es so aussehen:
insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }
Verlockend, nicht wahr? Dies wird jedoch für unser Szenario nicht funktionieren oder bei weitem nicht optimal funktionieren. Unser <frankenstein-header-wrapper>
ist tatsächlich in index.html
verfügbar (weil wir ihn in Schritt 5.2 hinzugefügt haben). Aber wenn Webpack alle Abhängigkeiten (einschließlich der Stylesheets) entweder für eine Alien-Komponente oder einen Frankenstein-Wrapper verarbeitet, wird Shadow DOM noch nicht im Frankenstein-Wrapper initialisiert: Importe werden vorher verarbeitet. Daher führt das Zeigen von insert
direkt auf shadowRoot zu einem Fehler.
Es gibt nur einen Fall, in dem wir garantieren können, dass Shadow DOM initialisiert wird, bevor Webpack unsere Stylesheet-Abhängigkeit verarbeitet. Wenn die Alien-Komponente kein Stylesheet selbst importiert und es Aufgabe des Frankenstein-Wrappers ist, es zu importieren, können wir den dynamischen Import verwenden und das erforderliche Stylesheet importieren, nachdem wir Shadow DOM eingerichtet haben:
this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');
Dies wird funktionieren: Ein solcher Import, kombiniert mit der obigen insert
, wird tatsächlich das richtige Shadow DOM finden und das Tag <style>
darin einfügen. Das Abrufen und Verarbeiten des Stylesheets wird jedoch einige Zeit in Anspruch nehmen, was bedeutet, dass Ihre Benutzer mit einer langsamen Verbindung oder langsamen Geräten möglicherweise einen Moment mit der nicht formatierten Komponente konfrontiert werden, bevor Ihr Stylesheet seinen Platz im Shadow DOM des Wrappers einnimmt.
Alles in allem reicht uns also insert
Accepts Function leider nicht aus und wir müssen auf einfache CSS-Selektoren wie frankenstein-header-wrapper
zurückgreifen. Dies platziert Stylesheets jedoch nicht automatisch in Shadow DOM, und die Stylesheets befinden sich in <frankenstein-header-wrapper>
außerhalb von Shadow DOM.
Wir brauchen noch ein Puzzleteil.
Wrapper-Konfiguration für globale Stylesheets und Shadow DOM
Glücklicherweise ist die Lösung auf Seiten des Wrappers recht einfach: Wenn Shadow DOM initialisiert wird, müssen wir im aktuellen Wrapper nach ausstehenden Stylesheets suchen und sie in Shadow DOM ziehen.
Der aktuelle Status des Imports des globalen Stylesheets ist wie folgt:
- Wir importieren ein Stylesheet, das in Shadow DOM hinzugefügt werden muss. Das Stylesheet kann entweder in die Alien-Komponente selbst oder explizit in den Frankenstein-Wrapper importiert werden. Bei einer Migration zu React beispielsweise wird der Import aus dem Wrapper initialisiert. Bei der Migration zu Vue importiert die ähnliche Komponente jedoch selbst das erforderliche Stylesheet, und wir müssen nichts in den Wrapper importieren.
- Wie oben erwähnt, werden die Stylesheets, wenn Webpack
.css
-Importe für die Alien-Komponente verarbeitet, dank derinsert
vonstyle-loader
in einen Frankenstein-Wrapper eingefügt, jedoch außerhalb von Shadow DOM.
Vereinfachte Initialisierung von Shadow DOM im Frankenstein-Wrapper, sollte derzeit (bevor wir irgendwelche Stylesheets einziehen) ähnlich aussehen:
this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`
Um ein Flackern der ungestylten Komponente zu vermeiden, müssen wir jetzt alle erforderlichen Stylesheets nach der Initialisierung des Shadow-DOM, aber vor dem Rendern der Alien-Komponente einlesen.
this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})
Es war eine lange Erklärung mit vielen Details, aber vor allem alles, was man braucht, um globale Stylesheets in Shadow DOM zu ziehen:
- Fügen Sie in der Webpack-Konfiguration den
style-loader
mit derinsert
hinzu, die auf den erforderlichen Frankenstein-Wrapper verweist. - Ziehen Sie im Wrapper selbst „ausstehende“ Stylesheets nach der Initialisierung von Shadow DOM, aber vor dem Rendern der Alien-Komponente.
Nach der Implementierung dieser Änderungen sollte Ihre Komponente über alles verfügen, was sie benötigt. Das einzige, was Sie hinzufügen möchten (dies ist keine Voraussetzung), ist ein benutzerdefiniertes CSS, um eine Alien-Komponente in der Host-Umgebung zu optimieren. Sie können Ihre Alien-Komponente sogar völlig anders gestalten, wenn Sie sie in Host verwenden. Es geht über den Hauptpunkt des Artikels hinaus, aber Sie sehen sich den endgültigen Code für den Wrapper an, in dem Sie Beispiele finden, wie Sie einfache Stile auf Wrapper-Ebene überschreiben können.
- Frankenstein-Wrapper für die React-Komponente
- Frankenstein-Wrapper für die Vue-Komponente
Sie können sich bei diesem Migrationsschritt auch die Webpack-Konfiguration ansehen:
- Migration zu React mit styled-components
- Migration zu React mit CSS-Modulen
- Migration zu Vue
Und schließlich sehen unsere Komponenten genau so aus, wie wir sie uns vorgestellt haben.
5.5. Zusammenfassung der Befestigungsstile für die Alien-Komponente
Dies ist ein großartiger Moment, um zusammenzufassen, was wir bisher in diesem Kapitel gelernt haben. Es könnte so aussehen, als hätten wir enorme Arbeit leisten müssen, um das Styling der Alien-Komponente zu korrigieren; es läuft jedoch alles darauf hinaus:
- Das Korrigieren gebündelter Stile, die mit Stilkomponenten in React- oder CSS-Modulen und Scoped CSS in Vue implementiert wurden, ist so einfach wie ein paar Zeilen in der Frankenstein-Wrapper- oder Webpack-Konfiguration.
- Das Fixieren von Stilen, implementiert mit CSS-Modulen, beginnt mit nur einer Zeile in der CSS
css-loader
Konfiguration. Danach werden CSS-Module als globales Stylesheet behandelt. - Das Korrigieren globaler Stylesheets erfordert das Konfigurieren
style-loader
Pakets mit derinsert
in Webpack und das Aktualisieren des Frankenstein-Wrappers, um die Stylesheets im richtigen Moment des Wrapper-Lebenszyklus in Shadow DOM einzufügen.
Immerhin haben wir die richtig gestylte Alien-Komponente in den Host migriert. Es gibt jedoch nur eine Sache, die Sie stören könnte oder auch nicht, je nachdem, auf welches Framework Sie migrieren.
Die gute Nachricht zuerst: Wenn Sie zu Vue migrieren , sollte die Demo problemlos funktionieren, und Sie sollten in der Lage sein, neue Aufgaben aus der migrierten Vue-Komponente hinzuzufügen. Wenn Sie jedoch zu React migrieren und versuchen, ein neues Aufgabenelement hinzuzufügen, werden Sie keinen Erfolg haben. Das Hinzufügen neuer Elemente funktioniert einfach nicht, und der Liste werden keine Einträge hinzugefügt. Aber warum? Was ist das Problem? Keine Vorurteile, aber React hat zu manchen Dingen seine eigene Meinung.
5.6. Reagieren Sie und JS-Ereignisse in Shadow DOM
Egal, was Ihnen die React-Dokumentation sagt, React ist nicht sehr freundlich zu Webkomponenten. Die Einfachheit des Beispiels in der Dokumentation hält keiner Kritik stand, und alles Kompliziertere als das Rendern eines Links in Web Component erfordert einige Recherchen und Untersuchungen.
Wie Sie beim Korrigieren des Stils für unsere Alien-Komponente gesehen haben, ist React im Gegensatz zu Vue, wo die Dinge fast sofort in Webkomponenten passen, nicht so Webkomponenten-bereit. Im Moment haben wir ein Verständnis dafür, wie React-Komponenten in Webkomponenten zumindest gut aussehen, aber es gibt auch Funktionen und JavaScript-Ereignisse, die behoben werden müssen.
Lange Rede, kurzer Sinn: Shadow DOM kapselt Ereignisse und richtet sie neu aus, während React dieses Verhalten von Shadow DOM nicht nativ unterstützt und daher keine Ereignisse abfängt, die aus Shadow DOM kommen. Es gibt tiefere Gründe für dieses Verhalten, und es gibt sogar ein offenes Problem im Bugtracker von React, wenn Sie in weitere Details und Diskussionen eintauchen möchten.
Glücklicherweise haben kluge Leute eine Lösung für uns vorbereitet. @josephnvu lieferte die Basis für die Lösung, und Lukas Bombach konvertierte sie in das npm-Modul „ react-shadow-dom-retarget-events
“. Sie können also das Paket installieren, den Anweisungen auf der Paketseite folgen, den Code Ihres Wrappers aktualisieren und Ihre Alien-Komponente wird auf magische Weise anfangen zu arbeiten:
import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);
Wenn Sie es leistungsfähiger haben möchten, können Sie eine lokale Kopie des Pakets erstellen (die MIT-Lizenz erlaubt dies) und die Anzahl der anzuhörenden Ereignisse begrenzen, wie es im Frankenstein-Demo-Repository der Fall ist. Für dieses Beispiel weiß ich, welche Ereignisse ich neu ausrichten muss und gebe nur diese an.
Damit sind wir endlich (ich weiß, es war ein langer Prozess) mit der ordnungsgemäßen Migration der ersten gestylten und voll funktionsfähigen Alien-Komponente fertig. Holen Sie sich einen guten Drink. Du verdienst es!
6. Spülen und wiederholen Sie für alle Ihre Komponenten
Nachdem wir die erste Komponente migriert haben, sollten wir den Vorgang für alle unsere Komponenten wiederholen. Im Fall von Frankenstein Demo bleibt jedoch nur noch einer übrig: derjenige, der für die Darstellung der Liste der Aufgaben verantwortlich ist.
Neue Wrapper für neue Komponenten
Beginnen wir mit dem Hinzufügen eines neuen Wrappers. Der oben besprochenen Namenskonvention folgend (da unsere React-Komponente MainSection.js
heißt), sollte der entsprechende Wrapper bei der Migration zu React MainSection-wrapper.js
. Gleichzeitig heißt eine ähnliche Komponente in Vue Listing.vue
, daher sollte der entsprechende Wrapper bei der Migration zu Vue Listing-wrapper.js
. Unabhängig von der Namenskonvention wird der Wrapper selbst jedoch nahezu identisch mit dem sein, den wir bereits haben:
- Wrapper für die React-Auflistung
- Wrapper für Vue-Auflistung
Es gibt nur eine interessante Sache, die wir in dieser zweiten Komponente in der React-Anwendung einführen. Manchmal möchten Sie aus diesem oder einem anderen Grund vielleicht ein jQuery-Plugin in Ihren Komponenten verwenden. Im Falle unserer React-Komponente haben wir zwei Dinge eingeführt:
- Tooltip-Plugin von Bootstrap, das jQuery verwendet,
- Ein Schalter für CSS-Klassen wie
.addClass()
und.removeClass()
.
Hinweis : Diese Verwendung von jQuery zum Hinzufügen/Entfernen von Klassen dient lediglich der Veranschaulichung. Bitte verwenden Sie für dieses Szenario in realen Projekten kein jQuery – verlassen Sie sich stattdessen auf reines JavaScript.
Natürlich mag es seltsam aussehen, jQuery in eine Alien-Komponente einzuführen, wenn wir von jQuery weg migrieren, aber Ihr Host könnte sich von dem Host in diesem Beispiel unterscheiden – Sie könnten von AngularJS oder irgendetwas anderem weg migrieren. Außerdem sind die jQuery-Funktionalität in einer Komponente und die globale jQuery nicht unbedingt dasselbe.
Das Problem ist jedoch, dass selbst wenn Sie bestätigen, dass diese Komponente im Kontext Ihrer Alien-Anwendung einwandfrei funktioniert, Ihre jQuery-Plug-ins und anderer Code, der auf jQuery angewiesen ist, einfach nicht funktionieren, wenn Sie sie in Shadow DOM einfügen.
jQuery im Schatten-DOM
Werfen wir einen Blick auf eine allgemeine Initialisierung eines zufälligen jQuery-Plugins:
$('.my-selector').fancyPlugin();
Auf diese Weise werden alle Elemente mit .my-selector
von fancyPlugin
verarbeitet. Diese Form der Initialisierung setzt voraus, dass .my-selector
im globalen DOM vorhanden ist. Sobald ein solches Element jedoch in Shadow DOM eingefügt wird, verhindern Schattengrenzen, genau wie bei Stilen, jQuery daran, sich hineinzuschleichen. Infolgedessen kann jQuery keine Elemente in Shadow DOM finden.
Die Lösung besteht darin, dem Selektor einen optionalen zweiten Parameter bereitzustellen, der das Stammelement definiert, in dem jQuery suchen soll. Und hier können wir unseren shadowRoot
.
$('.my-selector', this.shadowRoot).fancyPlugin();
Auf diese Weise funktionieren jQuery-Selektoren und folglich die Plugins einwandfrei.
Beachten Sie jedoch, dass die Alien-Komponenten sowohl für die Verwendung in Alien ohne Schatten-DOM als auch in Host innerhalb von Schatten-DOM vorgesehen sind. Daher brauchen wir eine einheitlichere Lösung, die nicht standardmäßig das Vorhandensein von Shadow DOM voraussetzt.
Bei der Analyse MainSection
Komponente in unserer React-Anwendung stellen wir fest, dass sie die documentRoot
-Eigenschaft festlegt.
... this.documentRoot = this.props.root? this.props.root: document; ...
Also suchen wir nach übergebener root
Eigenschaft, und wenn sie existiert, verwenden wir diese als documentRoot
. Andernfalls greifen wir auf document
zurück.
Hier ist die Initialisierung des Tooltip-Plugins, das diese Eigenschaft verwendet:
$('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });
Als Bonus verwenden wir in diesem Fall dieselbe root
Eigenschaft, um einen Container zum Einfügen des Tooltips zu definieren.
Wenn die Alien-Komponente nun bereit ist, die root
Eigenschaft zu akzeptieren, aktualisieren wir das Rendering der Komponente im entsprechenden Frankenstein-Wrapper:
// `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);
Und das ist es! Die Komponente funktioniert im Shadow DOM genauso gut wie im globalen DOM.
Webpack-Konfiguration für Multi-Wrapper-Szenario
Der spannende Teil passiert in der Konfiguration von Webpack, wenn mehrere Wrapper verwendet werden. Für die gebündelten Stile wie diese CSS-Module in Vue-Komponenten oder Stilkomponenten in React ändert sich nichts. Allerdings sollten globale Stile jetzt eine kleine Wendung bekommen.
Denken Sie daran, dass wir gesagt haben, dass der style-loader
(der für das Einfügen globaler Stylesheets in das korrekte Shadow-DOM verantwortlich ist) unflexibel ist, da er jeweils nur einen Selektor für seine insert
benötigt. Das bedeutet, dass wir die .css
-Regel in Webpack aufteilen sollten, um eine Unterregel pro Wrapper mit der oneOf
-Regel oder ähnlichem zu haben, wenn Sie sich auf einem anderen Bundler als Webpack befinden.
Es ist immer einfacher, es anhand eines Beispiels zu erklären, also sprechen wir diesmal über die von der Migration zu Vue (die von der Migration zu React ist jedoch fast identisch):
... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...
Ich habe css-loader
ausgeschlossen, da seine Konfiguration in allen Fällen gleich ist. Lassen Sie uns stattdessen über style-loader
sprechen. In dieser Konfiguration fügen wir das Tag <style>
entweder in *-header-*
oder *-listing-*
ein, abhängig vom Namen der Datei, die dieses Stylesheet anfordert ( issuer
in Webpack). Wir müssen jedoch bedenken, dass das zum Rendern einer Alien-Komponente erforderliche globale Stylesheet an zwei Stellen importiert werden kann:
- Die Alien-Komponente selbst,
- Ein Frankenstein-Wrapper.
Und hier sollten wir die oben beschriebene Namenskonvention für Wrapper zu schätzen wissen, wenn der Name einer Alien-Komponente und eines entsprechenden Wrappers übereinstimmen. Wenn wir zum Beispiel ein Stylesheet haben, das in eine Vue-Komponente namens Header.vue
importiert wird, erhält es den korrekten *-header-*
Wrapper. Wenn wir stattdessen das Stylesheet in den Wrapper importieren, folgt dieses Stylesheet gleichzeitig genau derselben Regel, wenn der Wrapper ohne Änderungen in der Konfiguration Header-wrapper.js
heißt. Das Gleiche gilt für die Komponente Listing.vue
und den entsprechenden Wrapper Listing-wrapper.js
. Mit dieser Namenskonvention reduzieren wir die Konfiguration in unserem Bundler.
Nachdem alle Ihre Komponenten migriert sind, ist es Zeit für den letzten Schritt der Migration.
7. Wechseln Sie zu Alien
Irgendwann stellen Sie fest, dass die Komponenten, die Sie im allerersten Schritt der Migration identifiziert haben, alle durch Frankenstein-Wrapper ersetzt wurden. Es bleibt keine jQuery-Anwendung übrig, und was Sie haben, ist im Wesentlichen die Alien-Anwendung, die mit Hilfe von Host zusammengeklebt wird.
Beispielsweise sieht der Inhaltsteil von index.html
in der jQuery-Anwendung – nach der Migration beider Microservices – jetzt etwa so aus:
<section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>
Im Moment macht es keinen Sinn, unsere jQuery-Anwendung beizubehalten: Stattdessen sollten wir zur Vue-Anwendung wechseln und all unsere Wrapper, Shadow DOM und ausgefallenen Webpack-Konfigurationen vergessen. Dafür haben wir eine elegante Lösung.
Lassen Sie uns über HTTP-Anforderungen sprechen. Ich werde hier die Apache-Konfiguration erwähnen, aber dies ist nur ein Implementierungsdetail: Die Umstellung in Nginx oder irgendetwas anderem sollte so trivial sein wie in Apache.
Stellen Sie sich vor, Ihre Site wird aus dem Ordner /var/www/html
auf Ihrem Server bereitgestellt. In diesem Fall sollte Ihre httpd.conf
oder httpd-vhost.conf
einen Eintrag haben, der auf diesen Ordner zeigt, wie etwa:
DocumentRoot "/var/www/html"
Um Ihre Anwendung nach der Frankenstein-Migration von jQuery auf React umzustellen, müssen Sie lediglich den DocumentRoot
-Eintrag auf etwas wie das folgende aktualisieren:
DocumentRoot "/var/www/html/react/build"
Erstellen Sie Ihre Alien-Anwendung, starten Sie Ihren Server neu und Ihre Anwendung wird direkt aus dem Alien-Ordner bedient: die React-Anwendung aus dem React react/
-Ordner. Dasselbe gilt natürlich auch für Vue oder jedes andere Framework, das Sie ebenfalls migriert haben. Aus diesem Grund ist es so wichtig, Host und Alien zu jedem Zeitpunkt vollständig unabhängig und funktionsfähig zu halten, da Ihr Alien in diesem Schritt zu Ihrem Host wird.
Jetzt können Sie alles rund um den Ordner Ihres Aliens sicher entfernen, einschließlich aller Shadow DOM-, Frankenstein-Wrapper und aller anderen migrationsbezogenen Artefakte. Es war zeitweise ein holpriger Weg, aber Sie haben Ihre Website migriert. Glückwünsche!
Fazit
Wir sind in diesem Artikel definitiv durch etwas unwegsames Gelände gegangen. Nachdem wir jedoch mit einer jQuery-Anwendung begonnen haben, haben wir es geschafft, sie sowohl auf Vue als auch auf React zu migrieren. Wir haben dabei einige unerwartete und nicht so triviale Probleme entdeckt: Wir mussten das Styling korrigieren, wir mussten die JavaScript-Funktionalität korrigieren, einige Bundler-Konfigurationen einführen und vieles mehr. Es gab uns jedoch einen besseren Überblick darüber, was uns in realen Projekten erwartet. Am Ende haben wir eine zeitgemäße Anwendung ohne Reste der jQuery-Anwendung erhalten, obwohl wir das Endergebnis während der Migration mit Recht skeptisch beurteilten.
Die Frankenstein-Migration ist weder eine Wunderwaffe noch sollte es ein beängstigender Prozess sein. Es ist nur der definierte Algorithmus, der auf viele Projekte anwendbar ist, der hilft, Projekte auf vorhersehbare Weise in etwas Neues und Robustes zu verwandeln.