Migrazione di Frankenstein: approccio indipendente dal framework (parte 2)
Pubblicato: 2022-03-10In questo articolo, metteremo alla prova tutta la teoria eseguendo la migrazione passo passo di un'applicazione, seguendo i consigli della parte precedente. Per semplificare le cose, ridurre le incertezze, le incognite e le supposizioni non necessarie, per l'esempio pratico della migrazione, ho deciso di dimostrare la pratica su una semplice applicazione da fare.
In generale, presumo che tu abbia una buona comprensione di come funziona un'applicazione generica da fare. Questo tipo di applicazione si adatta molto bene alle nostre esigenze: è prevedibile, ma ha un numero minimo praticabile di componenti richiesti per dimostrare diversi aspetti della migrazione di Frankenstein. Tuttavia, indipendentemente dalle dimensioni e dalla complessità della tua applicazione reale, l'approccio è ben scalabile e dovrebbe essere adatto a progetti di qualsiasi dimensione.
Per questo articolo, come punto di partenza, ho scelto un'applicazione jQuery dal progetto TodoMVC, un esempio che potrebbe già essere familiare a molti di voi. jQuery è abbastanza legacy, potrebbe riflettere una situazione reale con i tuoi progetti e, soprattutto, richiede una manutenzione significativa e hack per alimentare una moderna applicazione dinamica. (Questo dovrebbe essere sufficiente per considerare la migrazione a qualcosa di più flessibile.)
Cos'è questo "più flessibile" a cui migreremo allora? Per mostrare un caso molto pratico e utile nella vita reale, ho dovuto scegliere tra i due framework più popolari in questi giorni: React e Vue. Tuttavia, qualunque cosa io scelgo, perderemmo alcuni aspetti dell'altra direzione.
Quindi, in questa parte, analizzeremo entrambi i seguenti:
- Una migrazione di un'applicazione jQuery a React e
- Una migrazione di un'applicazione jQuery a Vue .
Repository di codice
Tutto il codice menzionato qui è disponibile pubblicamente e puoi accedervi quando vuoi. Sono disponibili due repository con cui giocare:
- Frankenstein Todo MVC
Questo repository contiene applicazioni TodoMVC in diversi framework/librerie. Ad esempio, puoi trovare rami comevue
,angularjs
,react
ejquery
in questo repository. - Demo di Frankenstein
Contiene diversi rami, ognuno dei quali rappresenta una particolare direzione di migrazione tra le applicazioni, disponibile nel primo repository. Ci sono rami comemigration/jquery-to-react
emigration/jquery-to-vue
, in particolare, che tratteremo in seguito.
Entrambi i repository sono in corso di lavorazione e dovrebbero essere aggiunti regolarmente nuovi rami con nuove applicazioni e direzioni di migrazione. ( Anche tu sei libero di contribuire! ) La cronologia dei commit nei rami di migrazione è ben strutturata e potrebbe fungere da documentazione aggiuntiva con ancora più dettagli di quelli che potrei coprire in questo articolo.
Ora, sporchiamoci le mani! Abbiamo molta strada da fare, quindi non aspettarti che sia un giro tranquillo. Sta a te decidere come vuoi seguire questo articolo, ma potresti fare quanto segue:
- Clona il ramo
jquery
dal repository Frankenstein TodoMVC e segui rigorosamente tutte le istruzioni seguenti. - In alternativa, puoi aprire un ramo dedicato alla migrazione a React o alla migrazione a Vue dal repository Demo di Frankenstein e seguire la cronologia dei commit.
- In alternativa, puoi rilassarti e continuare a leggere perché evidenzierò il codice più critico proprio qui ed è molto più importante comprendere i meccanismi del processo piuttosto che il codice vero e proprio.
Vorrei ricordare ancora una volta che seguiremo rigorosamente i passaggi presentati nella prima parte teorica dell'articolo.
Entriamo subito!
- Identificare i microservizi
- Consenti l'accesso da host a alieno
- Scrivere un microservizio/componente alieno
- Scrivi un wrapper di componenti Web attorno al servizio alieno
- Sostituisci il servizio host con il componente Web
- Risciacqua e ripeti per tutti i tuoi componenti
- Passa ad alieno
1. Identificare i microservizi
Come suggerisce la Parte 1, in questo passaggio dobbiamo strutturare la nostra applicazione in piccoli servizi indipendenti dedicati a un particolare lavoro . Il lettore attento potrebbe notare che la nostra applicazione da fare è già piccola e indipendente e può rappresentare un singolo microservizio da solo. Questo è il modo in cui lo tratterei io stesso se questa applicazione vivesse in un contesto più ampio. Ricorda, tuttavia, che il processo di identificazione dei microservizi è del tutto soggettivo e non esiste una risposta corretta.
Quindi, per vedere il processo di migrazione di Frankenstein in modo più dettagliato, possiamo fare un ulteriore passo avanti e dividere questa applicazione da fare in due microservizi indipendenti:
- Un campo di input per aggiungere un nuovo elemento.
Questo servizio può contenere anche l'intestazione dell'applicazione, basata esclusivamente sul posizionamento in prossimità di questi elementi. - Un elenco di elementi già aggiunti.
Questo servizio è più avanzato e, insieme all'elenco stesso, contiene anche azioni come il filtro, le azioni degli elementi dell'elenco e così via.
Suggerimento : per verificare se i servizi selezionati sono realmente indipendenti, rimuovere il markup HTML, che rappresenta ciascuno di questi servizi. Assicurati che le restanti funzioni funzionino ancora. Nel nostro caso, dovrebbe essere possibile aggiungere nuove voci in localStorage
(che questa applicazione sta utilizzando come memoria) dal campo di input senza l'elenco, mentre l'elenco esegue il rendering delle voci da localStorage
anche se il campo di input è mancante. Se la tua applicazione genera errori quando rimuovi il markup per un potenziale microservizio, dai un'occhiata alla sezione "Refactor se necessario" nella parte 1 per un esempio di come gestire questi casi.
Naturalmente, potremmo continuare e suddividere ulteriormente il secondo servizio e l'elenco degli elementi in microservizi indipendenti per ogni particolare elemento. Tuttavia, potrebbe essere troppo granulare per questo esempio. Quindi, per ora, concludiamo che la nostra applicazione avrà due servizi; sono indipendenti e ciascuno lavora per il proprio compito particolare. Pertanto, abbiamo suddiviso la nostra applicazione in microservizi .
2. Consenti l'accesso da host a alieno
Lascia che ti ricordi brevemente di cosa si tratta.
- Ospite
Questo è il nome della nostra attuale applicazione. È scritto con il framework da cui stiamo per allontanarci . In questo caso particolare, la nostra applicazione jQuery. - Alieno
In poche parole, questa è una riscrittura graduale di Host sul nuovo framework in cui stiamo per passare . Anche in questo caso, in questo caso particolare, è un'applicazione React o Vue.
La regola pratica quando si divide Host e Alien è che dovresti essere in grado di svilupparne e distribuirne uno senza rompere l'altro, in qualsiasi momento.
Mantenere Host e Alien indipendenti l'uno dall'altro è fondamentale per Frankenstein Migration. Tuttavia, questo rende un po' difficile organizzare la comunicazione tra i due. Come possiamo consentire all'Host di accedere ad Alien senza distruggere i due insieme?
Aggiunta di Alien come sottomodulo del tuo host
Anche se ci sono diversi modi per ottenere la configurazione di cui abbiamo bisogno, la forma più semplice per organizzare il tuo progetto per soddisfare questo criterio è probabilmente git submodules. Questo è ciò che useremo in questo articolo. Lascio a te leggere attentamente come funzionano i sottomoduli in git per comprendere i limiti e i trucchi di questa struttura.
I principi generali dell'architettura del nostro progetto con i sottomoduli git dovrebbero assomigliare a questo:
- Sia Host che Alien sono indipendenti e sono mantenuti in repository
git
separati; - L'host fa riferimento ad Alien come sottomodulo. A questo punto, Host seleziona uno stato particolare (commit) di Alien e lo aggiunge come, come appare, una sottocartella nella struttura delle cartelle di Host.
Il processo di aggiunta di un sottomodulo è lo stesso per qualsiasi applicazione. L'insegnamento dei git submodules
va oltre lo scopo di questo articolo e non è direttamente correlato alla migrazione di Frankenstein stessa. Quindi diamo solo una breve occhiata ai possibili esempi.
Negli snippet seguenti, utilizziamo la direzione Reagire come esempio. Per qualsiasi altra direzione di migrazione, sostituisci react
con il nome di un ramo di Frankenstein TodoMVC o regola i valori personalizzati dove necessario.
Se segui l'applicazione originale jQuery TodoMVC:
$ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i
Se segui il ramo migration/jquery-to-react
(o qualsiasi altra direzione di migrazione) dal repository Frankenstein Demo, l'applicazione Alien dovrebbe essere già presente come git submodule
e dovresti vedere una rispettiva cartella. Tuttavia, la cartella è vuota per impostazione predefinita ed è necessario aggiornare e inizializzare i sottomoduli registrati.
Dalla radice del tuo progetto (il tuo host):
$ git submodule update --init $ cd react $ npm i
Nota che in entrambi i casi installiamo le dipendenze per l'applicazione Alien, ma quelle diventano sandbox nella sottocartella e non inquineranno il nostro Host.
Dopo aver aggiunto l'applicazione Alien come sottomodulo del tuo Host, ottieni applicazioni Alien e Host indipendenti (in termini di microservizi). Tuttavia, Host considera Alien una sottocartella in questo caso e, ovviamente, ciò consente a Host di accedere ad Alien senza problemi.
3. Scrivere un Microservizio/Componente Alien
A questo punto, dobbiamo decidere quale microservizio migrare per primo e scriverlo/utilizzarlo dalla parte di Alien. Seguiamo lo stesso ordine di servizi che abbiamo individuato nel passaggio 1 e iniziamo con il primo: campo di input per l'aggiunta di un nuovo elemento. Tuttavia, prima di iniziare, concordiamo sul fatto che, oltre questo punto, useremo un termine più favorevole invece di microservizio o servizio mentre ci stiamo muovendo verso le premesse dei framework frontend e il termine componente segue le definizioni di quasi tutti i moderni struttura.
I rami del repository Frankenstein TodoMVC contengono un componente risultante che rappresenta il primo servizio "Campo di input per l'aggiunta di un nuovo elemento" come componente di intestazione:
- Componente di intestazione in React
- Componente di intestazione in Vue
La scrittura di componenti nel quadro di tua scelta esula dallo scopo di questo articolo e non fa parte di Frankenstein Migration. Tuttavia, ci sono un paio di cose da tenere a mente durante la scrittura di un componente Alien.
Indipendenza
Innanzitutto, i componenti di Alien dovrebbero seguire lo stesso principio di indipendenza, precedentemente impostato dalla parte dell'Host: i componenti non dovrebbero dipendere in alcun modo da altri componenti.
Interoperabilità
Grazie all'indipendenza dei servizi, molto probabilmente, i componenti del tuo Host comunicano in un modo ben consolidato sia che si tratti di un sistema di gestione dello stato, della comunicazione attraverso alcuni storage condivisi o, direttamente tramite un sistema di eventi DOM. "Interoperabilità" dei componenti Alien significa che dovrebbero essere in grado di connettersi alla stessa fonte di comunicazione, stabilita da Host, per inviare informazioni sui suoi cambiamenti di stato e ascoltare i cambiamenti in altri componenti. In pratica, ciò significa che se i componenti del tuo Host comunicano tramite eventi DOM, la creazione del tuo componente Alien esclusivamente tenendo presente la gestione dello stato non funzionerà perfettamente per questo tipo di migrazione, sfortunatamente.
Ad esempio, dai un'occhiata al file js/storage.js
che è il canale di comunicazione principale per i nostri componenti 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); }, ...
Qui, utilizziamo localStorage
(poiché questo esempio non è critico per la sicurezza) per archiviare le nostre attività e, una volta registrate le modifiche allo spazio di archiviazione, inviamo un evento DOM personalizzato sull'elemento del document
che qualsiasi componente può ascoltare.
Allo stesso tempo, da parte di Alien (diciamo React) possiamo impostare una comunicazione di gestione dello stato complessa come vogliamo. Tuttavia, è probabilmente intelligente tenerlo per il futuro: per integrare con successo il nostro componente Alien React in Host, dobbiamo connetterci allo stesso canale di comunicazione utilizzato da Host. In questo caso, è localStorage
. Per semplificare le cose, abbiamo semplicemente copiato il file di archiviazione di Host in Alien e abbiamo collegato i nostri componenti ad esso:
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); } } ... }
Ora, i nostri componenti Alien possono parlare la stessa lingua con i componenti Host e viceversa.
4. Scrivere il wrapper di componenti Web attorno al servizio alieno
Anche se ora siamo solo al quarto gradino, abbiamo ottenuto parecchio:
- Abbiamo suddiviso la nostra applicazione Host in servizi indipendenti che sono pronti per essere sostituiti dai servizi Alien;
- Abbiamo impostato Host e Alien in modo che siano completamente indipendenti l'uno dall'altro, ma molto ben collegati tramite
git submodules
; - Abbiamo scritto il nostro primo componente Alien utilizzando il nuovo framework.
Ora è il momento di creare un ponte tra Host e Alien in modo che il nuovo componente Alien possa funzionare nell'Host.
Promemoria dalla parte 1 : assicurati che il tuo host abbia un bundler di pacchetti disponibile. In questo articolo ci affidiamo a Webpack, ma ciò non significa che la tecnica non funzionerà con Rollup o qualsiasi altro bundler di tua scelta. Tuttavia, lascio la mappatura da Webpack ai tuoi esperimenti.
Convenzione di denominazione
Come accennato nell'articolo precedente, utilizzeremo i componenti Web per integrare Alien in Host. Dal lato dell'Host, creiamo un nuovo file: js/frankenstein-wrappers/Header-wrapper.js
. (Sarà il nostro primo wrapper di Frankenstein.) Tieni presente che è una buona idea nominare i tuoi wrapper come i tuoi componenti nell'applicazione Alien, ad esempio semplicemente aggiungendo un suffisso " -wrapper
". Vedrai più avanti perché questa è una buona idea, ma per ora siamo d'accordo sul fatto che questo significa che se il componente Alien è chiamato Header.js
(in React) o Header.vue
(in Vue), il corrispondente wrapper sul Il lato host dovrebbe essere chiamato Header-wrapper.js
.
Nel nostro primo wrapper, iniziamo con il boilerplate fondamentale per la registrazione di un elemento personalizzato:
class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);
Successivamente, dobbiamo inizializzare Shadow DOM per questo elemento.
Si prega di fare riferimento alla Parte 1 per ottenere un ragionamento sul motivo per cui utilizziamo Shadow DOM.
class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }
Con questo, abbiamo impostato tutti i bit essenziali del componente Web ed è ora di aggiungere il nostro componente Alien al mix. Innanzitutto, all'inizio del nostro wrapper di Frankenstein, dovremmo importare tutti i bit responsabili del rendering del componente Alien.
import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...
Qui dobbiamo fermarci un secondo. Nota che non importiamo le dipendenze di Alien da node_modules
di Host. Tutto viene dall'alieno stesso che si trova nella sottocartella react/
. Ecco perché il passaggio 2 è così importante ed è fondamentale assicurarsi che l'host abbia pieno accesso alle risorse di Alien.
Ora possiamo eseguire il rendering del nostro componente Alien all'interno dello Shadow DOM del componente Web:
... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...
Nota : in questo caso, React non ha bisogno di nient'altro. Tuttavia, per eseguire il rendering del componente Vue, è necessario aggiungere un nodo di wrapping per contenere il componente Vue come il seguente:
... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...
La ragione di ciò è la differenza nel modo in cui React e Vue eseguono il rendering dei componenti: React aggiunge il componente al nodo DOM di riferimento, mentre Vue sostituisce il nodo DOM di riferimento con il componente. Quindi, se eseguiamo .$mount(this.shadowRoot)
per Vue, essenzialmente sostituisce Shadow DOM.
Questo è tutto ciò che dobbiamo fare per il nostro wrapper per ora. Il risultato corrente per il wrapper Frankenstein nelle direzioni di migrazione jQuery-to-React e jQuery-to-Vue può essere trovato qui:
- Involucro di Frankenstein per il componente React
- Involucro di Frankenstein per il componente Vue
Per riassumere la meccanica dell'involucro di Frankenstein:
- Crea un elemento personalizzato,
- Avvia Shadow DOM,
- Importa tutto il necessario per il rendering di un componente Alien,
- Rendering del componente Alien all'interno dello Shadow DOM dell'elemento personalizzato.
Tuttavia, questo non rende automaticamente il nostro Alien in Host. Dobbiamo sostituire il markup Host esistente con il nostro nuovo wrapper Frankenstein.
Allacciate le cinture di sicurezza, potrebbe non essere così semplice come ci si aspetterebbe!
5. Sostituire il servizio host con il componente Web
Continuiamo e aggiungiamo il nostro nuovo file Header-wrapper.js
a index.html
e sostituiamo il markup dell'intestazione esistente con l'elemento personalizzato <frankenstein-header-wrapper>
appena creato.
... <!-- <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>
Sfortunatamente, questo non funzionerà così semplice. Se apri un browser e controlli la console, c'è Uncaught SyntaxError
che ti aspetta. A seconda del browser e del suo supporto per i moduli ES6, sarà correlato alle importazioni ES6 o al modo in cui viene eseguito il rendering del componente Alien. In ogni caso, dobbiamo fare qualcosa al riguardo, ma il problema e la soluzione dovrebbero essere familiari e chiari alla maggior parte dei lettori.
5.1. Aggiorna Webpack e Babel dove necessario
Dovremmo coinvolgere un po' di Webpack e magia Babel prima di integrare il nostro wrapper Frankenstein. La disputa di questi strumenti va oltre lo scopo dell'articolo, ma puoi dare un'occhiata ai commit corrispondenti nel repository demo di Frankenstein:
- Configurazione per la migrazione a React
- Configurazione per la migrazione a Vue
In sostanza, abbiamo impostato l'elaborazione dei file e un nuovo punto di ingresso frankenstein
nella configurazione di Webpack per contenere tutto ciò che riguarda i wrapper di Frankenstein in un unico posto.
Una volta che Webpack in Host sa come elaborare il componente Alien e i componenti Web, siamo pronti per sostituire il markup di Host con il nuovo wrapper Frankenstein.
5.2. Sostituzione del componente effettivo
La sostituzione del componente dovrebbe essere semplice ora. In index.html
del tuo host, procedi come segue:
- Sostituisci
<header class="header">
elemento DOM con<frankenstein-header-wrapper>
; - Aggiungi un nuovo script
frankenstein.js
. Questo è il nuovo punto di ingresso in Webpack che contiene tutto ciò che riguarda i wrapper di Frankenstein.
... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>
Questo è tutto! Riavvia il tuo server se necessario e assisti alla magia del componente Alien integrato in Host.
Tuttavia, qualcosa sembrava ancora mancare. Il componente Alien nel contesto Host non ha lo stesso aspetto che ha nel contesto dell'applicazione Alien standalone. È semplicemente senza stile.
Perché è così? Gli stili del componente non dovrebbero essere integrati automaticamente con il componente Alien in Host? Vorrei che lo facessero, ma come in troppe situazioni, dipende. Stiamo arrivando alla parte impegnativa della migrazione di Frankenstein.
5.3. Informazioni generali sullo stile del componente alieno
Prima di tutto, l'ironia è che non ci sono bug nel modo in cui funzionano le cose. Tutto è come è progettato per funzionare. Per spiegare questo, menzioniamo brevemente i diversi modi di acconciare i componenti.
Stili globali
Conosciamo tutti questi aspetti: gli stili globali possono essere (e di solito lo sono) distribuiti senza alcun componente particolare e applicati all'intera pagina. Gli stili globali influiscono su tutti i nodi DOM con selettori corrispondenti.
Alcuni esempi di stili globali sono i <style>
e <link rel="stylesheet">
trovati nel tuo index.html
. In alternativa, un foglio di stile globale può essere importato in qualche modulo JS root in modo che anche tutti i componenti possano accedervi.
Il problema dello styling delle applicazioni in questo modo è ovvio: mantenere fogli di stile monolitici per applicazioni di grandi dimensioni diventa molto difficile. Inoltre, come abbiamo visto nell'articolo precedente, gli stili globali possono facilmente rompere i componenti che vengono visualizzati direttamente nell'albero DOM principale come in React o Vue.
Stili in bundle
Questi stili di solito sono strettamente accoppiati con un componente stesso e raramente sono distribuiti senza il componente. Gli stili in genere risiedono nello stesso file con il componente. Buoni esempi di questo tipo di stile sono i componenti con stile in React o Moduli CSS e CSS con ambito in componenti a file singolo in Vue. Tuttavia, indipendentemente dalla varietà di strumenti per la scrittura di stili in bundle, il principio alla base della maggior parte di essi è lo stesso: gli strumenti forniscono un meccanismo di definizione dell'ambito per bloccare gli stili definiti in un componente in modo che gli stili non interrompano altri componenti o stili.
Perché gli stili con ambito possono essere fragili?
Nella parte 1, quando si giustifica l'uso di Shadow DOM nella migrazione di Frankenstein, abbiamo brevemente trattato l'argomento dell'ambito rispetto all'incapsulamento) e di come l'incapsulamento di Shadow DOM sia diverso dagli strumenti di stile dell'ambito. Tuttavia, non abbiamo spiegato perché gli strumenti di scoping forniscono uno stile così fragile per i nostri componenti e ora, quando abbiamo affrontato il componente Alien senza stile, diventa essenziale per la comprensione.
Tutti gli strumenti di scoping per i framework moderni funzionano in modo simile:
- Scrivi stili per il tuo componente in qualche modo senza pensare molto all'ambito o all'incapsulamento;
- Esegui i tuoi componenti con fogli di stile importati/incorporati attraverso un sistema di raggruppamento, come Webpack o Rollup;
- Il bundler genera classi CSS uniche o altri attributi, creando e inserendo selettori individuali sia per il tuo HTML che per i fogli di stile corrispondenti;
- Il bundler crea una voce
<style>
nella<head>
del tuo documento e inserisce gli stili dei tuoi componenti con selettori misti univoci.
Questo è praticamente tutto. Funziona e funziona bene in molti casi. Tranne quando non è così: quando gli stili per tutti i componenti risiedono nell'ambito dello stile globale, diventa facile romperli, ad esempio, utilizzando una specificità maggiore. Questo spiega la potenziale fragilità degli strumenti di scoping, ma perché il nostro componente Alien è completamente privo di stile?
Diamo un'occhiata all'host corrente utilizzando DevTools. Quando ispezioniamo il wrapper Frankenstein appena aggiunto con il componente Alien React, ad esempio, possiamo vedere qualcosa del genere:
Quindi, Webpack genera classi CSS uniche per il nostro componente. Grande! Dove sono gli stili allora? Ebbene, gli stili sono esattamente dove sono progettati per essere: nel <head>
del documento.
Quindi tutto funziona come dovrebbe, e questo è il problema principale. Poiché il nostro componente Alien risiede in Shadow DOM e, come spiegato nella Parte #1, Shadow DOM fornisce l'incapsulamento completo dei componenti del resto della pagina e degli stili globali, inclusi i fogli di stile appena generati per il componente che non possono attraversare il confine dell'ombra e arriva al componente Alien. Quindi, il componente Alien è lasciato senza stile. Tuttavia, ora, le tattiche per risolvere il problema dovrebbero essere chiare: dovremmo in qualche modo posizionare gli stili del componente nello stesso Shadow DOM in cui risiede il nostro componente (invece del <head>
del documento).
5.4. Stili di fissaggio per il componente alieno
Finora, il processo di migrazione a qualsiasi framework era lo stesso. Tuttavia, qui le cose iniziano a divergere: ogni framework ha le sue raccomandazioni su come definire lo stile dei componenti e, quindi, i modi per affrontare il problema differiscono. Qui discutiamo i casi più comuni ma, se il framework con cui lavori utilizza un modo unico per definire lo stile dei componenti, devi tenere a mente le tattiche di base come inserire gli stili del componente in Shadow DOM invece di <head>
.
In questo capitolo tratteremo le correzioni per:
- Stili in bundle con i moduli CSS in Vue (le tattiche per CSS con ambito sono le stesse);
- Stili in bundle con componenti di stile in React;
- Moduli CSS generici e stili globali. Li combino perché i moduli CSS, in generale, sono molto simili ai fogli di stile globali e possono essere importati da qualsiasi componente rendendo gli stili disconnessi da qualsiasi componente particolare.
Prima i vincoli: qualsiasi cosa facciamo per correggere lo stile non dovrebbe rompere il componente Alien stesso . Altrimenti, perdiamo l'indipendenza dei nostri sistemi Alien e Host. Quindi, per affrontare il problema dello stile, faremo affidamento sulla configurazione del bundler o sul wrapper di Frankenstein.
Stili in bundle in Vue e Shadow DOM
Se stai scrivendo un'applicazione Vue, molto probabilmente stai utilizzando componenti a file singolo. Se stai usando anche Webpack, dovresti avere familiarità con due caricatori vue-loader
e vue-style-loader
. Il primo ti consente di scrivere quei componenti di file singoli mentre il secondo inietta dinamicamente il CSS del componente in un documento come tag <style>
. Per impostazione predefinita, vue-style-loader
inserisce gli stili del componente nel <head>
del documento. Tuttavia, entrambi i pacchetti accettano l'opzione shadowMode
nella configurazione che ci consente di modificare facilmente il comportamento predefinito e di inserire gli stili (come suggerisce il nome dell'opzione) in Shadow DOM. Vediamolo in azione.
Configurazione del pacchetto web
Come minimo, il file di configurazione di Webpack dovrebbe contenere quanto segue:
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 un'applicazione reale, il tuo test: /\.css$/
block sarà più sofisticato (probabilmente coinvolgendo la regola oneOf
) per tenere conto delle configurazioni sia Host che Alien. Tuttavia, in questo caso, il nostro jQuery ha uno stile con un semplice <link rel="stylesheet">
in index.html
, quindi non creiamo stili per Host tramite Webpack ed è sicuro soddisfare solo Alien.
Configurazione dell'involucro
Oltre alla configurazione del Webpack, dobbiamo anche aggiornare il nostro wrapper Frankenstein, puntando Vue al corretto Shadow DOM. Nel nostro Header-wrapper.js
, il rendering del componente Vue dovrebbe includere la proprietà shadowRoot
che porta a shadowRoot
del nostro wrapper Frankenstein:
... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...
Dopo aver aggiornato i file e riavviato il server, dovresti ricevere qualcosa di simile nel tuo DevTools:
Infine, gli stili per il componente Vue sono all'interno del nostro Shadow DOM. Allo stesso tempo, la tua applicazione dovrebbe apparire così:
Iniziamo a ottenere qualcosa di simile alla nostra applicazione Vue: gli stili in bundle con il componente vengono iniettati nello Shadow DOM del wrapper, ma il componente non sembra ancora come dovrebbe. Il motivo è che nell'applicazione Vue originale, il componente ha uno stile non solo con gli stili in bundle ma anche in parte con gli stili globali. Tuttavia, prima di correggere gli stili globali, dobbiamo portare la nostra integrazione React allo stesso stato di quella Vue.
Stili in bundle in React e Shadow DOM
Poiché ci sono molti modi in cui è possibile definire lo stile di un componente React, la soluzione particolare per correggere un componente Alien in Frankenstein Migration dipende in primo luogo dal modo in cui stiliamo il componente. Analizziamo brevemente le alternative più comunemente utilizzate.
componenti in stile
styled-components è uno dei modi più popolari per lo styling dei componenti React. Per il componente Header React, styled-components è esattamente il modo in cui lo stiliamo. Poiché si tratta di un classico approccio CSS-in-JS, non esiste un file con un'estensione dedicata a cui potremmo agganciare il nostro bundler come facciamo per i file .css
o .js
, ad esempio. Fortunatamente, i componenti di stile consentono l'iniezione degli stili dei componenti in un nodo personalizzato (Shadow DOM nel nostro caso) invece della head
del documento con l'aiuto del componente di aiuto StyleSheetManager
. È un componente predefinito, installato con il pacchetto styled-components
che accetta la proprietà target
, definendo "un nodo DOM alternativo per iniettare informazioni sugli stili". Esattamente quello di cui abbiamo bisogno! Inoltre, non abbiamo nemmeno bisogno di modificare la nostra configurazione del Webpack: tutto dipende dal nostro wrapper Frankenstein.
Dovremmo aggiornare il nostro Header-wrapper.js
che contiene il componente React Alien con le seguenti righe:
... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...
Qui importiamo il componente StyleSheetManager
(da Alien e non da Host) e con esso avvolgiamo il nostro componente React. Allo stesso tempo, inviamo la proprietà target
che punta al nostro shadowRoot
. Questo è tutto. Se riavvii il server, devi vedere qualcosa di simile nel tuo DevTools:
Ora, gli stili del nostro componente sono in Shadow DOM invece di <head>
. In questo modo, il rendering della nostra app ora assomiglia a quello che abbiamo visto in precedenza con l'app Vue.
Stessa storia: i componenti con stile sono responsabili solo della parte in bundle degli stili del componente React e gli stili globali gestiscono i bit rimanenti. Torniamo agli stili globali tra un po' dopo aver esaminato un altro tipo di componenti di styling.
Moduli CSS
Se dai un'occhiata più da vicino al componente Vue che abbiamo corretto in precedenza, potresti notare che i moduli CSS sono esattamente il modo in cui stiliamo quel componente. 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. Come facciamo questo?
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. Grandi notizie! 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.
Fogli di stile globali e opzione di insert
del style-loader
Se controlli la documentazione per questa opzione, noterai che questa opzione richiede un selettore per configurazione. Ciò significa che se hai diversi componenti Alien che richiedono stili globali inseriti in un wrapper di Frankenstein, devi specificare style-loader
per ciascuno dei wrapper di Frankenstein. In pratica, questo significa che, probabilmente, devi fare affidamento sulla regola oneOf
nel tuo blocco di configurazione per servire a tutti i wrapper.
{ 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. ], }
Non molto flessibile, sono d'accordo. Tuttavia, non è un grosso problema finché non hai centinaia di componenti da migrare. In caso contrario, potrebbe rendere difficile la manutenzione della configurazione del Webpack. Il vero problema, però, è che non possiamo scrivere un selettore CSS per Shadow DOM.
Cercando di risolvere questo problema, potremmo notare che l'opzione di insert
può anche accettare una funzione invece di un semplice selettore per specificare una logica più avanzata per l'inserimento. Con questo, possiamo usare questa opzione per inserire fogli di stile direttamente in Shadow DOM! In forma semplificata potrebbe sembrare simile a questo:
insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }
Allettante, non è vero? Tuttavia, questo non funzionerà per il nostro scenario o funzionerà lontano dall'essere ottimale. Il nostro <frankenstein-header-wrapper>
è effettivamente disponibile da index.html
(perché l'abbiamo aggiunto nel passaggio 5.2). Ma quando Webpack elabora tutte le dipendenze (inclusi i fogli di stile) per un componente Alien o un wrapper Frankenstein, Shadow DOM non è ancora inizializzato nel wrapper Frankenstein: le importazioni vengono elaborate prima. Quindi, puntare insert
direttamente su shadowRoot risulterà in un errore.
C'è solo un caso in cui possiamo garantire che Shadow DOM sia inizializzato prima che Webpack elabori la nostra dipendenza dal foglio di stile. Se il componente Alien non importa un foglio di stile stesso e spetta al wrapper Frankenstein importarlo, potremmo utilizzare l'importazione dinamica e importare il foglio di stile richiesto dopo aver impostato Shadow DOM:
this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');
Questo funzionerà: tale importazione, combinata con la configurazione di insert
sopra, troverà effettivamente il DOM Shadow corretto e inserirà il tag <style>
in esso. Tuttavia, il recupero e l'elaborazione del foglio di stile richiederanno tempo, il che significa che i tuoi utenti con una connessione lenta o dispositivi lenti potrebbero dover affrontare un momento del componente senza stile prima che il foglio di stile si inserisca all'interno dello Shadow DOM del wrapper.
Quindi, tutto sommato, anche se insert
accetta la funzione, sfortunatamente non è abbastanza per noi e dobbiamo ricorrere a semplici selettori CSS come frankenstein-header-wrapper
. Tuttavia, ciò non inserisce automaticamente i fogli di stile in Shadow DOM e i fogli di stile risiedono in <frankenstein-header-wrapper>
al di fuori di Shadow DOM.
Abbiamo bisogno di un altro pezzo del puzzle.
Configurazione wrapper per fogli di stile globali e Shadow DOM
Fortunatamente, la soluzione è abbastanza semplice dal lato del wrapper: quando Shadow DOM viene inizializzato, dobbiamo controllare eventuali fogli di stile in sospeso nel wrapper corrente e inserirli in Shadow DOM.
Lo stato attuale dell'importazione del foglio di stile globale è il seguente:
- Importiamo un foglio di stile che deve essere aggiunto in Shadow DOM. Il foglio di stile può essere importato nel componente Alien stesso o, esplicitamente, nel wrapper Frankenstein. Nel caso di migrazione a React, ad esempio, l'importazione viene inizializzata dal wrapper. Tuttavia, durante la migrazione a Vue, il componente simile importa il foglio di stile richiesto e non è necessario importare nulla nel wrapper.
- Come sottolineato in precedenza, quando Webpack elabora le importazioni
.css
per il componente Alien, grazie all'opzione diinsert
distyle-loader
, i fogli di stile vengono iniettati in un wrapper Frankenstein, ma al di fuori di Shadow DOM.
L'inizializzazione semplificata di Shadow DOM nel wrapper di Frankenstein dovrebbe attualmente (prima di inserire qualsiasi foglio di stile) essere simile a questa:
this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`
Ora, per evitare lo sfarfallio del componente senza stile, ciò che dobbiamo fare ora è inserire tutti i fogli di stile richiesti dopo l'inizializzazione di Shadow DOM, ma prima del rendering del componente Alien.
this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})
È stata una lunga spiegazione con molti dettagli, ma principalmente tutto ciò che serve per inserire fogli di stile globali in Shadow DOM:
- Nella configurazione di Webpack aggiungi
style-loader
con l'opzione diinsert
che punta al wrapper Frankenstein richiesto. - Nel wrapper stesso, inserisci i fogli di stile "in sospeso" dopo l'inizializzazione di Shadow DOM, ma prima del rendering del componente Alien.
Dopo aver implementato queste modifiche, il tuo componente dovrebbe avere tutto ciò di cui ha bisogno. L'unica cosa che potresti voler aggiungere (questo non è un requisito) è un CSS personalizzato per mettere a punto un componente Alien nell'ambiente di Host. Potresti persino dare uno stile al tuo componente Alien completamente diverso se utilizzato in Host. Va oltre il punto principale dell'articolo, ma guardi il codice finale per il wrapper, dove puoi trovare esempi di come sovrascrivere stili semplici a livello di wrapper.
- Involucro di Frankenstein per il componente React
- Wrapper di Frankenstein per il componente Vue
Puoi anche dare un'occhiata alla configurazione del Webpack in questo passaggio della migrazione:
- Migrazione a React con componenti in stile
- Migrazione per reagire con i moduli CSS
- Migrazione a Vue
E infine, i nostri componenti hanno esattamente l'aspetto che ci aspettavamo.
5.5. Riepilogo degli stili di fissaggio per il componente Alien
Questo è un ottimo momento per riassumere ciò che abbiamo imparato in questo capitolo finora. Potrebbe sembrare che abbiamo dovuto fare un lavoro enorme per correggere lo stile del componente Alien; tuttavia, tutto si riduce a:
- La correzione degli stili in bundle implementati con i componenti di stile nei moduli React o CSS e CSS con ambito in Vue è semplice come un paio di righe nel wrapper di Frankenstein o nella configurazione di Webpack.
- Gli stili di correzione, implementati con i moduli CSS, iniziano con una sola riga nella configurazione
css-loader
. Successivamente, i moduli CSS vengono trattati come un foglio di stile globale. - La correzione dei fogli di stile globali richiede la configurazione del pacchetto
style-loader
con l'opzione diinsert
in Webpack e l'aggiornamento del wrapper Frankenstein per inserire i fogli di stile in Shadow DOM al momento giusto del ciclo di vita del wrapper.
Dopotutto, abbiamo migrato nell'Host il componente Alien con lo stile corretto. Tuttavia, c'è solo una cosa che potrebbe infastidirti o meno a seconda del framework in cui esegui la migrazione.
Buone notizie innanzitutto: se stai migrando a Vue , la demo dovrebbe funzionare bene e dovresti essere in grado di aggiungere nuove cose da fare dal componente Vue migrato. Tuttavia, se stai migrando a React e provi ad aggiungere un nuovo elemento da fare, non avrai successo. L'aggiunta di nuovi elementi semplicemente non funziona e nessuna voce viene aggiunta all'elenco. Ma perché? Qual è il problema? Nessun pregiudizio, ma React ha le sue opinioni su alcune cose.
5.6. Reagire e gli eventi JS in Shadow DOM
Indipendentemente da ciò che la documentazione di React ti dice, React non è molto amichevole con i componenti Web. La semplicità dell'esempio nella documentazione non sopporta alcuna critica, e qualsiasi cosa più complicata del rendere un collegamento in Web Component richiede alcune ricerche e indagini.
Come hai visto durante la correzione dello stile per il nostro componente Alien, contrariamente a Vue dove le cose si adattano a Web Components quasi fuori dagli schemi, React non è pronto per Web Components. Per ora, abbiamo una comprensione di come rendere i componenti React almeno belli all'interno dei componenti Web, ma ci sono anche funzionalità ed eventi JavaScript da correggere.
Per farla breve: Shadow DOM incapsula gli eventi e li reindirizza, mentre React non supporta questo comportamento di Shadow DOM in modo nativo e quindi non cattura gli eventi provenienti dall'interno di Shadow DOM. Ci sono ragioni più profonde per questo comportamento, e c'è anche un problema aperto nel bug tracker di React se vuoi approfondire più dettagli e discussioni.
Fortunatamente, le persone intelligenti hanno preparato una soluzione per noi. @josephnvu ha fornito le basi per la soluzione e Lukas Bombach l'ha convertita nel modulo react-shadow-dom-retarget-events
npm. Quindi puoi installare il pacchetto, seguire le istruzioni nella pagina dei pacchetti, aggiornare il codice del tuo wrapper e il tuo componente Alien inizierà magicamente a funzionare:
import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);
Se vuoi renderlo più performante, puoi fare una copia locale del pacchetto (la licenza MIT lo consente) e limitare il numero di eventi da ascoltare come avviene nel repository Demo di Frankenstein. Per questo esempio, so quali eventi ho bisogno di retargeting e specificare solo quelli.
Con questo, abbiamo finalmente (so che è stato un lungo processo) terminato con la corretta migrazione del primo componente Alien in stile e completamente funzionante. Fatti un buon drink. Te lo meriti!
6. Risciacqua e ripeti per tutti i tuoi componenti
Dopo aver migrato il primo componente, dovremmo ripetere il processo per tutti i nostri componenti. Nel caso di Frankenstein Demo, però, ne è rimasto solo uno: quello, responsabile del rendering dell'elenco delle cose da fare.
Nuovi wrapper per nuovi componenti
Iniziamo con l'aggiunta di un nuovo wrapper. Seguendo la convenzione di denominazione, discussa sopra (poiché il nostro componente React è chiamato MainSection.js
), il corrispondente wrapper in migrazione a React dovrebbe essere chiamato MainSection-wrapper.js
. Allo stesso tempo, un componente simile in Vue è chiamato Listing.vue
, quindi il wrapper corrispondente nella migrazione a Vue dovrebbe essere chiamato Listing-wrapper.js
. Tuttavia, indipendentemente dalla convenzione di denominazione, il wrapper stesso sarà quasi identico a quello che abbiamo già:
- Wrapper per l'elenco React
- Wrapper per l'elenco Vue
C'è solo una cosa interessante che introduciamo in questo secondo componente dell'applicazione React. A volte, per questo o un altro motivo, potresti voler utilizzare alcuni plugin jQuery nei tuoi componenti. Nel caso del nostro componente React, abbiamo introdotto due cose:
- Plugin Tooltip di Bootstrap che utilizza jQuery,
- Un interruttore per classi CSS come
.addClass()
e.removeClass()
.
Nota : questo uso di jQuery per aggiungere/rimuovere classi è puramente illustrativo. Si prega di non utilizzare jQuery per questo scenario in progetti reali: affidarsi invece a JavaScript semplice.
Ovviamente, potrebbe sembrare strano introdurre jQuery in un componente Alien quando migriamo lontano da jQuery, ma il tuo host potrebbe essere diverso dall'host in questo esempio: potresti migrare lontano da AngularJS o qualsiasi altra cosa. Inoltre, la funzionalità jQuery in un componente e jQuery globale non sono necessariamente la stessa cosa.
Tuttavia, il problema è che anche se confermi che il componente funziona bene nel contesto della tua applicazione Alien, quando lo inserisci in Shadow DOM, i tuoi plugin jQuery e altro codice che si basa su jQuery semplicemente non funzioneranno.
jQuery In Shadow DOM
Diamo un'occhiata a un'inizializzazione generale di un plugin jQuery casuale:
$('.my-selector').fancyPlugin();
In questo modo, tutti gli elementi con .my-selector
verranno elaborati da fancyPlugin
. Questa forma di inizializzazione presuppone che .my-selector
sia presente nel DOM globale. Tuttavia, una volta che un tale elemento è stato inserito in Shadow DOM, proprio come con gli stili, i confini dell'ombra impediscono a jQuery di intrufolarsi al suo interno. Di conseguenza, jQuery non riesce a trovare elementi all'interno di Shadow DOM.
La soluzione è fornire un secondo parametro opzionale al selettore che definisce l'elemento radice da cui jQuery deve eseguire la ricerca. Ed è qui che possiamo fornire il nostro shadowRoot
.
$('.my-selector', this.shadowRoot).fancyPlugin();
In questo modo, i selettori jQuery e, di conseguenza, i plugin funzioneranno perfettamente.
Tieni presente, tuttavia, che i componenti Alien sono pensati per essere utilizzati sia: in Alien senza DOM ombra, sia in Host all'interno di DOM ombra. Quindi abbiamo bisogno di una soluzione più unificata che non presuppone la presenza di Shadow DOM per impostazione predefinita.
Analizzando il componente MainSection
nella nostra applicazione React, scopriamo che imposta la proprietà documentRoot
.
... this.documentRoot = this.props.root? this.props.root: document; ...
Quindi, controlliamo la proprietà root
passata e, se esiste, questo è ciò che usiamo come documentRoot
. Altrimenti, torniamo a document
.
Ecco l'inizializzazione del plugin tooltip che utilizza questa proprietà:
$('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });
Come bonus, in questo caso utilizziamo la stessa proprietà root
per definire un contenitore per l'inserimento della descrizione comando.
Ora, quando il componente Alien è pronto per accettare la proprietà root
, aggiorniamo il rendering del componente nel corrispondente wrapper di Frankenstein:
// `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);
E questo è tutto! Il componente funziona bene in Shadow DOM come nel DOM globale.
Configurazione Webpack per scenario multi-wrapper
La parte eccitante sta accadendo nella configurazione di Webpack quando si utilizzano diversi wrapper. Non cambia nulla per gli stili in bundle come quei moduli CSS nei componenti Vue o i componenti con stile in React. Tuttavia, gli stili globali dovrebbero avere una piccola svolta ora.
Ricorda, abbiamo detto che style-loader
(responsabile dell'iniezione di fogli di stile globali nel corretto Shadow DOM) è inflessibile poiché richiede un solo selettore alla volta per la sua opzione di insert
. Ciò significa che dovremmo dividere la regola .css
in Webpack per avere una sottoregola per wrapper usando una regola oneOf
o simile, se sei su un bundler diverso da Webpack.
È sempre più facile da spiegare usando un esempio, quindi parliamo di quello dalla migrazione a Vue questa volta (quello in migrazione a React, tuttavia, è quasi identico):
... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...
Ho escluso css-loader
poiché la sua configurazione è la stessa in tutti i casi. Parliamo invece di style-loader
. In questa configurazione, inseriamo il tag <style>
in *-header-*
o *-listing-*
, a seconda del nome del file che richiede quel foglio di stile ( regola issuer
in Webpack). Ma dobbiamo ricordare che il foglio di stile globale richiesto per il rendering di un componente Alien potrebbe essere importato in due posti:
- La stessa componente aliena,
- Un involucro di Frankenstein.
E qui, dovremmo apprezzare la convenzione di denominazione per i wrapper, descritta sopra, quando il nome di un componente Alien e un corrispondente wrapper corrispondono. Se, ad esempio, abbiamo un foglio di stile, importato in un componente Vue chiamato Header.vue
, corregge *-header-*
wrapper. Allo stesso tempo, se importiamo invece il foglio di stile nel wrapper, tale foglio di stile segue esattamente la stessa regola se il wrapper si chiama Header-wrapper.js
senza alcuna modifica nella configurazione. Stessa cosa per il componente Listing.vue
e il relativo wrapper Listing-wrapper.js
. Usando questa convenzione di denominazione, riduciamo la configurazione nel nostro bundler.
Dopo la migrazione di tutti i componenti, è il momento del passaggio finale della migrazione.
7. Passa ad Alien
Ad un certo punto, scopri che i componenti che hai identificato al primo passaggio della migrazione vengono tutti sostituiti con i wrapper di Frankenstein. Nessuna applicazione jQuery è rimasta in realtà e quello che hai è, essenzialmente, l'applicazione Alien che è incollata insieme usando i mezzi di Host.
Ad esempio, la parte del contenuto di index.html
nell'applicazione jQuery, dopo la migrazione di entrambi i microservizi, ora è simile a questa:
<section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>
In questo momento, non ha senso tenere in giro la nostra applicazione jQuery: invece, dovremmo passare all'applicazione Vue e dimenticare tutti i nostri wrapper, Shadow DOM e le fantasiose configurazioni di Webpack. Per fare questo, abbiamo una soluzione elegante.
Parliamo di richieste HTTP. Citerò qui la configurazione di Apache, ma questo è solo un dettaglio di implementazione: fare lo switch in Nginx o qualsiasi altra cosa dovrebbe essere banale come in Apache.
Immagina di avere il tuo sito servito dalla cartella /var/www/html
sul tuo server. In questo caso, il tuo httpd.conf
o httpd-vhost.conf
dovrebbe avere una voce che punta a quella cartella come:
DocumentRoot "/var/www/html"
Per cambiare la tua applicazione dopo la migrazione di Frankenstein da jQuery a React, tutto ciò che devi fare è aggiornare la voce DocumentRoot
a qualcosa di simile:
DocumentRoot "/var/www/html/react/build"
Crea la tua applicazione Alien, riavvia il server e la tua applicazione viene servita direttamente dalla cartella di Alien: l'applicazione React servita dalla cartella react/
. Tuttavia, lo stesso vale per Vue, ovviamente, o per qualsiasi altro framework che hai migrato. Questo è il motivo per cui è così fondamentale mantenere Host e Alien completamente indipendenti e funzionali in qualsiasi momento perché il tuo Alien diventa il tuo Host in questo passaggio.
Ora puoi rimuovere in sicurezza tutto ciò che si trova intorno alla cartella di Alien, inclusi tutti gli Shadow DOM, i wrapper di Frankenstein e qualsiasi altro artefatto relativo alla migrazione. A volte è stato un percorso difficile, ma hai migrato il tuo sito. Congratulazioni!
Conclusione
Abbiamo sicuramente attraversato un terreno piuttosto accidentato in questo articolo. Tuttavia, dopo aver iniziato con un'applicazione jQuery, siamo riusciti a migrarla sia su Vue che su React. Abbiamo scoperto alcuni problemi inaspettati e non così banali lungo la strada: dovevamo correggere lo stile, dovevamo correggere la funzionalità JavaScript, introdurre alcune configurazioni del bundler e molto altro ancora. Tuttavia, ci ha fornito una panoramica migliore di cosa aspettarsi nei progetti reali. Alla fine, abbiamo un'applicazione contemporanea senza bit rimanenti dall'applicazione jQuery anche se avevamo tutti i diritti per essere scettici sul risultato finale mentre la migrazione era in corso.
La migrazione di Frankenstein non è né un proiettile d'argento né dovrebbe essere un processo spaventoso. È solo l'algoritmo definito, applicabile a molti progetti, che aiuta a trasformare i progetti in qualcosa di nuovo e robusto in modo prevedibile.