Migration de Frankenstein : Approche indépendante du cadre (Partie 2)
Publié: 2022-03-10Dans cet article, nous allons mettre toute la théorie à l'épreuve en procédant pas à pas à la migration d'une application, en suivant les recommandations de la partie précédente. Pour simplifier les choses, réduire les incertitudes, les inconnues et les devinettes inutiles, pour l'exemple pratique de la migration, j'ai décidé de démontrer la pratique sur une simple application de tâches.
En général, je suppose que vous avez une bonne compréhension du fonctionnement d'une application de tâches générique. Ce type d'application répond très bien à nos besoins : elle est prévisible, mais possède un nombre minimum viable de composants requis pour démontrer différents aspects de la migration Frankenstein. Cependant, quelles que soient la taille et la complexité de votre application réelle, l'approche est bien évolutive et est censée convenir à des projets de toute taille.
Pour cet article, comme point de départ, j'ai choisi une application jQuery du projet TodoMVC - un exemple qui est peut-être déjà familier à beaucoup d'entre vous. jQuery est suffisamment hérité, peut refléter une situation réelle avec vos projets et, plus important encore, nécessite une maintenance et des hacks importants pour alimenter une application dynamique moderne. (Cela devrait être suffisant pour envisager la migration vers quelque chose de plus flexible.)
Vers quoi va-t-on migrer alors ? Pour montrer un cas très pratique utile dans la vraie vie, j'ai dû choisir parmi les deux frameworks les plus populaires de nos jours : React et Vue. Cependant, quel que soit mon choix, nous manquerions certains aspects de l'autre direction.
Donc, dans cette partie, nous allons parcourir les deux éléments suivants :
- Une migration d'une application jQuery vers React , et
- Une migration d'une application jQuery vers Vue .
Référentiels de code
Tout le code mentionné ici est accessible au public et vous pouvez y accéder quand vous le souhaitez. Deux référentiels sont disponibles pour jouer avec :
- Frankenstein TodoMVC
Ce référentiel contient des applications TodoMVC dans différents frameworks/bibliothèques. Par exemple, vous pouvez trouver des branches commevue
,angularjs
,react
etjquery
dans ce référentiel. - Démo Frankenstein
Il contient plusieurs branches, dont chacune représente un sens particulier de migration entre applications, disponibles dans le premier référentiel. Il existe des branches commemigration/jquery-to-react
etmigration/jquery-to-vue
, en particulier, que nous aborderons plus tard.
Les deux référentiels sont en cours de développement et de nouvelles branches avec de nouvelles applications et directions de migration devraient leur être ajoutées régulièrement. ( Vous êtes également libre de contribuer ! ) L'historique des commits dans les branches de migration est bien structuré et pourrait servir de documentation supplémentaire avec encore plus de détails que je ne pourrais couvrir dans cet article.
Maintenant, mettons-nous la main à la pâte ! Nous avons un long chemin à parcourir, alors ne vous attendez pas à ce que ce soit un parcours en douceur. C'est à vous de décider comment vous voulez suivre cet article, mais vous pouvez faire ce qui suit :
- Clonez la branche
jquery
du référentiel Frankenstein TodoMVC et suivez strictement toutes les instructions ci-dessous. - Alternativement, vous pouvez ouvrir une branche dédiée à la migration vers React ou à la migration vers Vue à partir du référentiel Frankenstein Demo et suivre l'historique des commits.
- Alternativement, vous pouvez vous détendre et continuer à lire car je vais mettre en évidence le code le plus critique ici, et il est beaucoup plus important de comprendre les mécanismes du processus plutôt que le code réel.
Je voudrais mentionner une fois de plus que nous suivrons strictement les étapes présentées dans la première partie théorique de l'article.
Plongeons dedans !
- Identifier les microservices
- Autoriser l'accès hôte à extraterrestre
- Écrire un microservice/composant extraterrestre
- Écrire un wrapper de composant Web autour d'un service extraterrestre
- Remplacer le service hôte par un composant Web
- Rincer et répéter pour tous vos composants
- Passer à l'extraterrestre
1. Identifier les microservices
Comme le suggère la partie 1, dans cette étape, nous devons structurer notre application en petits services indépendants dédiés à un travail particulier . Le lecteur attentif remarquera peut-être que notre application de tâches est déjà petite et indépendante et peut représenter un seul microservice à lui seul. C'est ainsi que je le traiterais moi-même si cette application vivait dans un contexte plus large. Rappelez-vous, cependant, que le processus d'identification des microservices est entièrement subjectif et qu'il n'y a pas une seule bonne réponse.
Ainsi, afin de voir le processus de migration Frankenstein plus en détail, nous pouvons aller plus loin et diviser cette application de tâches en deux microservices indépendants :
- Un champ de saisie pour ajouter un nouvel élément.
Ce service peut également contenir l'en-tête de l'application, basé uniquement sur la proximité de positionnement de ces éléments. - Une liste des éléments déjà ajoutés.
Ce service est plus avancé et, avec la liste elle-même, il contient également des actions telles que le filtrage, les actions des éléments de la liste, etc.
Astuce : Pour vérifier si les services sélectionnés sont réellement indépendants, supprimez le balisage HTML représentant chacun de ces services. Assurez-vous que les fonctions restantes fonctionnent toujours. Dans notre cas, il devrait être possible d'ajouter de nouvelles entrées dans localStorage
(que cette application utilise comme stockage) à partir du champ d'entrée sans la liste, tandis que la liste restitue toujours les entrées de localStorage
même si le champ d'entrée est manquant. Si votre application génère des erreurs lorsque vous supprimez le balisage pour un microservice potentiel, consultez la section "Refactoriser si nécessaire" dans la partie 1 pour un exemple de la façon de traiter de tels cas.
Bien sûr, nous pourrions continuer et diviser encore plus le deuxième service et la liste des articles en microservices indépendants pour chaque article particulier. Cependant, il peut être trop granulaire pour cet exemple. Donc, pour l'instant, nous concluons que notre application va avoir deux services ; ils sont indépendants et chacun d'eux travaille à sa tâche particulière. Par conséquent, nous avons divisé notre application en microservices .
2. Autoriser l'accès hôte à extraterrestre
Permettez-moi de vous rappeler brièvement de quoi il s'agit.
- Héberger
C'est ainsi que s'appelle notre application actuelle. Il est écrit avec le cadre dont nous sommes sur le point de nous éloigner . Dans ce cas particulier, notre application jQuery. - Extraterrestre
En termes simples, celui-ci est une réécriture progressive de Host sur le nouveau framework vers lequel nous sommes sur le point de passer . Encore une fois, dans ce cas particulier, il s'agit d'une application React ou Vue.
La règle de base lors de la séparation de Host et Alien est que vous devriez être en mesure de développer et de déployer l'un d'eux sans casser l'autre - à tout moment.
Garder Host et Alien indépendants l'un de l'autre est crucial pour Frankenstein Migration. Cependant, cela rend l'organisation de la communication entre les deux un peu difficile. Comment permettons-nous à Host d'accéder à Alien sans briser les deux ensemble ?
Ajout d'Alien en tant que sous-module de votre hôte
Même s'il existe plusieurs façons de réaliser la configuration dont nous avons besoin, la forme la plus simple d'organisation de votre projet pour répondre à ce critère est probablement les sous-modules git. C'est ce que nous allons utiliser dans cet article. Je vous laisse le soin de lire attentivement le fonctionnement des sous-modules dans git afin de comprendre les limites et les pièges de cette structure.
Les principes généraux de l'architecture de notre projet avec les sous-modules git devraient ressembler à ceci :
- Host et Alien sont indépendants et sont conservés dans des référentiels
git
distincts ; - Host fait référence à Alien en tant que sous-module. À ce stade, Host choisit un état particulier (commit) d'Alien et l'ajoute comme, ce qui ressemble, à un sous-dossier dans la structure de dossiers de Host.
Le processus d'ajout d'un sous-module est le même pour n'importe quelle application. L'enseignement des sous- git submodules
dépasse le cadre de cet article et n'est pas directement lié à Frankenstein Migration lui-même. Jetons donc un bref coup d'œil aux exemples possibles.
Dans les extraits ci-dessous, nous utilisons la direction React comme exemple. Pour toute autre direction de migration, remplacez react
par le nom d'une branche de Frankenstein TodoMVC ou ajustez les valeurs personnalisées si nécessaire.
Si vous suivez en utilisant l'application jQuery TodoMVC d'origine :
$ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i
Si vous suivez la branche migration/jquery-to-react
(ou toute autre direction de migration) du référentiel Frankenstein Demo, l'application Alien devrait déjà s'y trouver en tant que sous- git submodule
et vous devriez voir un dossier respectif. Cependant, le dossier est vide par défaut et vous devez mettre à jour et initialiser les sous-modules enregistrés.
Depuis la racine de votre projet (votre Host) :
$ git submodule update --init $ cd react $ npm i
Notez que dans les deux cas, nous installons des dépendances pour l'application Alien, mais celles-ci deviennent sandbox dans le sous-dossier et ne pollueront pas notre hôte.
Après avoir ajouté l'application Alien en tant que sous-module de votre Host, vous obtenez des applications Alien et Host indépendantes (en termes de microservices). Cependant, Host considère Alien comme un sous-dossier dans ce cas, et évidemment, cela permet à Host d'accéder à Alien sans problème.
3. Écrire un microservice/composant extraterrestre
À cette étape, nous devons décider quel microservice migrer en premier et l'écrire/l'utiliser du côté de l'Alien. Suivons le même ordre de services que nous avons identifié à l'étape 1 et commençons par le premier : champ de saisie pour ajouter un nouvel élément. Cependant, avant de commencer, convenons qu'au-delà de ce point, nous allons utiliser un composant de terme plus favorable au lieu de microservice ou de service, car nous nous dirigeons vers les prémisses des frameworks frontaux et le terme composant suit les définitions de presque tous les modernes. cadre.
Les branches du référentiel Frankenstein TodoMVC contiennent un composant résultant qui représente le premier service "Champ de saisie pour l'ajout d'un nouvel élément" en tant que composant d'en-tête :
- Composant d'en-tête dans React
- Composant d'en-tête dans Vue
L'écriture de composants dans le cadre de votre choix dépasse le cadre de cet article et ne fait pas partie de Frankenstein Migration. Cependant, il y a quelques points à garder à l'esprit lors de l'écriture d'un composant Alien.
Indépendance
Tout d'abord, les composants d'Alien doivent suivre le même principe d'indépendance, précédemment mis en place du côté de l'hôte : les composants ne doivent en aucun cas dépendre d'autres composants.
Interopérabilité
Grâce à l'indépendance des services, très probablement, les composants de votre hôte communiquent d'une manière bien établie, qu'il s'agisse d'un système de gestion d'état, d'une communication via un stockage partagé ou directement via un système d'événements DOM. "L'interopérabilité" des composants Alien signifie qu'ils doivent pouvoir se connecter à la même source de communication, établie par l'hôte, pour envoyer des informations sur ses changements d'état et écouter les changements dans d'autres composants. En pratique, cela signifie que si les composants de votre hôte communiquent via des événements DOM, la construction de votre composant Alien exclusivement avec la gestion de l'état à l'esprit ne fonctionnera malheureusement pas parfaitement pour ce type de migration.
À titre d'exemple, jetez un œil au fichier js/storage.js
qui est le principal canal de communication pour nos composants 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); }, ...
Ici, nous utilisons localStorage
(car cet exemple n'est pas critique pour la sécurité) pour stocker nos tâches, et une fois que les modifications apportées au stockage sont enregistrées, nous envoyons un événement DOM personnalisé sur l'élément de document
que n'importe quel composant peut écouter.
En même temps, du côté de l'Alien (disons React), nous pouvons mettre en place une communication de gestion d'état aussi complexe que nous le souhaitons. Cependant, il est probablement judicieux de le conserver pour l'avenir : pour intégrer avec succès notre composant Alien React dans Host, nous devons nous connecter au même canal de communication utilisé par Host. Dans ce cas, il s'agit de localStorage
. Pour simplifier les choses, nous avons simplement copié le fichier de stockage de Host dans Alien et y avons connecté nos composants :
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); } } ... }
Désormais, nos composants Alien peuvent parler le même langage avec les composants Host et vice versa.
4. Écrire un wrapper de composant Web autour d'un service extraterrestre
Même si nous n'en sommes qu'à la quatrième étape, nous avons beaucoup accompli :
- Nous avons divisé notre application Host en services indépendants qui sont prêts à être remplacés par des services Alien ;
- Nous avons configuré Host et Alien pour qu'ils soient complètement indépendants l'un de l'autre, mais très bien connectés via des sous-
git submodules
; - Nous avons écrit notre premier composant Alien en utilisant le nouveau framework.
Il est maintenant temps de mettre en place un pont entre Host et Alien afin que le nouveau composant Alien puisse fonctionner dans l'hôte.
Rappel de la partie 1 : Assurez-vous que votre hébergeur dispose d'un package bundler. Dans cet article, nous nous appuyons sur Webpack, mais cela ne signifie pas que la technique ne fonctionnera pas avec Rollup ou tout autre bundler de votre choix. Cependant, je laisse le mappage de Webpack à vos expériences.
Convention de dénomination
Comme mentionné dans l'article précédent, nous allons utiliser des composants Web pour intégrer Alien dans Host. Du côté de l'hôte, nous créons un nouveau fichier : js/frankenstein-wrappers/Header-wrapper.js
. (Ce sera notre premier wrapper Frankenstein.) Gardez à l'esprit que c'est une bonne idée de nommer vos wrappers de la même manière que vos composants dans l'application Alien, par exemple en ajoutant simplement un suffixe « -wrapper
». Vous verrez plus tard pourquoi c'est une bonne idée, mais pour l'instant, convenons que cela signifie que si le composant Alien s'appelle Header.js
(dans React) ou Header.vue
(dans Vue), le wrapper correspondant sur le Le côté de l'hôte doit s'appeler Header-wrapper.js
.
Dans notre premier wrapper, nous commençons par le passe-partout fondamental pour enregistrer un élément personnalisé :
class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);
Ensuite, nous devons initialiser Shadow DOM pour cet élément.
Veuillez vous référer à la partie 1 pour comprendre pourquoi nous utilisons Shadow DOM.
class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }
Avec cela, nous avons tous les éléments essentiels du composant Web configurés, et il est temps d'ajouter notre composant Alien dans le mélange. Tout d'abord, au début de notre wrapper Frankenstein, nous devons importer tous les bits responsables du rendu du composant Alien.
import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...
Ici, nous devons nous arrêter une seconde. Notez que nous n'importons pas les dépendances d'Alien à partir des node_modules
de Host. Tout vient de l'Alien lui-même qui se trouve dans le sous-dossier react react/
. C'est pourquoi l'étape 2 est si importante, et il est crucial de s'assurer que l'hôte a un accès complet aux actifs d'Alien.
Maintenant, nous pouvons rendre notre composant Alien dans le Shadow DOM de Web Component :
... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...
Note : Dans ce cas, React n'a besoin de rien d'autre. Cependant, pour rendre le composant Vue, vous devez ajouter un nœud d'encapsulation pour contenir votre composant Vue comme suit :
... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...
La raison en est la différence dans la façon dont React et Vue rendent les composants : React ajoute le composant au nœud DOM référencé, tandis que Vue remplace le nœud DOM référencé par le composant. Par conséquent, si nous faisons .$mount(this.shadowRoot)
pour Vue, cela remplace essentiellement le DOM Shadow.
C'est tout ce que nous avons à faire à notre emballage pour le moment. Le résultat actuel pour le wrapper Frankenstein dans les directions de migration jQuery-to-React et jQuery-to-Vue peut être trouvé ici :
- Frankenstein Wrapper pour le composant React
- Frankenstein Wrapper pour le composant Vue
Pour résumer la mécanique de l'emballage Frankenstein :
- Créer un élément personnalisé,
- Lancer Shadow DOM,
- Importez tout le nécessaire pour le rendu d'un composant Alien,
- Effectuez le rendu du composant Alien dans le Shadow DOM de l'élément personnalisé.
Cependant, cela ne rend pas automatiquement notre Alien dans l'hôte. Nous devons remplacer le balisage Host existant par notre nouveau wrapper Frankenstein.
Attachez vos ceintures, ce n'est peut-être pas aussi simple qu'on pourrait s'y attendre !
5. Remplacer le service hôte par un composant Web
Continuons et ajoutons notre nouveau fichier Header-wrapper.js
à index.html
et remplaçons le balisage d'en-tête existant par l'élément personnalisé <frankenstein-header-wrapper>
nouvellement créé.
... <!-- <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>
Malheureusement, cela ne fonctionnera pas aussi simplement que cela. Si vous ouvrez un navigateur et vérifiez la console, l' Uncaught SyntaxError
attend. Selon le navigateur et sa prise en charge des modules ES6, cela sera soit lié aux importations ES6, soit à la façon dont le composant Alien est rendu. Quoi qu'il en soit, nous devons faire quelque chose à ce sujet, mais le problème et la solution doivent être familiers et clairs pour la plupart des lecteurs.
5.1. Mettez à jour Webpack et Babel si nécessaire
Nous devrions impliquer un peu de magie Webpack et Babel avant d'intégrer notre wrapper Frankenstein. La manipulation de ces outils dépasse le cadre de l'article, mais vous pouvez jeter un œil aux commits correspondants dans le référentiel Frankenstein Demo :
- Configuration pour la migration vers React
- Configuration pour la migration vers Vue
Essentiellement, nous avons mis en place le traitement des fichiers ainsi qu'un nouveau point d'entrée frankenstein
dans la configuration de Webpack pour contenir tout ce qui concerne les wrappers Frankenstein en un seul endroit.
Une fois que Webpack dans Host sait comment traiter le composant Alien et les composants Web, nous sommes prêts à remplacer le balisage de Host par le nouveau wrapper Frankenstein.
5.2. Remplacement du composant réel
Le remplacement du composant devrait être simple maintenant. Dans index.html
de votre hébergeur, procédez comme suit :
- Remplacez l'élément
<header class="header">
DOM par<frankenstein-header-wrapper>
; - Ajoutez un nouveau script
frankenstein.js
. C'est le nouveau point d'entrée dans Webpack qui contient tout ce qui concerne les wrappers Frankenstein.
... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>
C'est ça! Redémarrez votre serveur si nécessaire et assistez à la magie du composant Alien intégré à Host.
Cependant, quelque chose semblait encore manquer. Le composant Alien dans le contexte Host n'a pas la même apparence que dans le contexte de l'application Alien autonome. C'est tout simplement sans style.
Pourquoi en est-il ainsi ? Les styles du composant ne devraient-ils pas être automatiquement intégrés au composant Alien dans Host ? J'aimerais qu'ils le fassent, mais comme dans trop de situations, cela dépend. Nous arrivons à la partie difficile de Frankenstein Migration.
5.3. Informations générales sur le style du composant Alien
Tout d'abord, l'ironie est qu'il n'y a pas de bug dans la façon dont les choses fonctionnent. Tout est tel qu'il est conçu pour fonctionner. Pour expliquer cela, mentionnons brièvement différentes manières de styliser les composants.
Styles globaux
Nous les connaissons tous : les styles globaux peuvent être (et sont généralement) distribués sans aucun composant particulier et appliqués à l'ensemble de la page. Les styles globaux affectent tous les nœuds DOM avec des sélecteurs correspondants.
Quelques exemples de styles globaux sont les balises <style>
et <link rel="stylesheet">
trouvées dans votre index.html
. Alternativement, une feuille de style globale peut être importée dans un module JS racine afin que tous les composants puissent également y accéder.
Le problème de styler les applications de cette manière est évident : maintenir des feuilles de style monolithiques pour de grandes applications devient très difficile. De plus, comme nous l'avons vu dans l'article précédent, les styles globaux peuvent facilement casser des composants rendus directement dans l'arborescence principale du DOM, comme c'est le cas dans React ou Vue.
Styles groupés
Ces styles sont généralement étroitement associés à un composant lui-même et sont rarement distribués sans le composant. Les styles résident généralement dans le même fichier avec le composant. De bons exemples de ce type de style sont les composants stylés dans React ou les modules CSS et le CSS Scoped dans les composants de fichier unique dans Vue. Cependant, quelle que soit la variété des outils d'écriture de styles groupés, le principe sous-jacent dans la plupart d'entre eux est le même : les outils fournissent un mécanisme de portée pour verrouiller les styles définis dans un composant afin que les styles ne cassent pas d'autres composants ou modes.
Pourquoi les styles de portée peuvent-ils être fragiles ?
Dans la partie 1, lors de la justification de l'utilisation de Shadow DOM dans Frankenstein Migration, nous avons brièvement abordé le sujet de la portée par rapport à l'encapsulation) et comment l'encapsulation de Shadow DOM est différente des outils de style de portée. Cependant, nous n'avons pas expliqué pourquoi les outils de cadrage fournissent un style si fragile à nos composants, et maintenant, lorsque nous sommes confrontés au composant Alien non stylé, cela devient essentiel pour comprendre.
Tous les outils de cadrage pour les frameworks modernes fonctionnent de la même manière :
- Vous écrivez des styles pour votre composant d'une manière ou d'une autre sans trop penser à la portée ou à l'encapsulation ;
- Vous exécutez vos composants avec des feuilles de style importées/intégrées via un système de regroupement, comme Webpack ou Rollup ;
- Le bundler génère des classes CSS uniques ou d'autres attributs, créant et injectant des sélecteurs individuels pour votre code HTML et les feuilles de style correspondantes ;
- Le bundler crée une entrée
<style>
dans le<head>
de votre document et y place les styles de vos composants avec des sélecteurs mélangés uniques.
C'est à peu près tout. Cela fonctionne et fonctionne bien dans de nombreux cas. Sauf dans les cas où ce n'est pas le cas : lorsque les styles de tous les composants vivent dans la portée de style globale, il devient facile de les casser, par exemple en utilisant une spécificité plus élevée. Cela explique la fragilité potentielle des outils de cadrage, mais pourquoi notre composant Alien n'a-t-il aucun style ?
Jetons un coup d'œil à l'hôte actuel à l'aide de DevTools. Lors de l'inspection du wrapper Frankenstein nouvellement ajouté avec le composant Alien React, par exemple, nous pouvons voir quelque chose comme ceci :
Ainsi, Webpack génère des classes CSS uniques pour notre composant. Génial! Où sont les styles alors ? Eh bien, les styles sont précisément là où ils sont conçus - dans le <head>
du document.
Donc, tout fonctionne comme il se doit, et c'est le principal problème. Étant donné que notre composant Alien réside dans Shadow DOM, et comme expliqué dans la partie 1, Shadow DOM fournit une encapsulation complète des composants du reste de la page et des styles globaux, y compris les feuilles de style nouvellement générées pour le composant qui ne peut pas traverser la frontière d'ombre et accéder au composant Alien. Par conséquent, le composant Alien n'a pas de style. Cependant, maintenant, la tactique pour résoudre le problème devrait être claire : nous devrions en quelque sorte placer les styles du composant dans le même DOM Shadow où réside notre composant (au lieu du document <head>
).
5.4. Correction des styles pour le composant extraterrestre
Jusqu'à présent, le processus de migration vers n'importe quel framework était le même. Cependant, les choses commencent à diverger ici : chaque framework a ses recommandations sur la façon de styliser les composants, et par conséquent, les façons de résoudre le problème diffèrent. Ici, nous discutons des cas les plus courants, mais si le framework avec lequel vous travaillez utilise une manière unique de styliser les composants, vous devez garder à l'esprit les tactiques de base telles que mettre les styles du composant dans Shadow DOM au lieu de <head>
.
Dans ce chapitre, nous couvrons les correctifs pour :
- Styles groupés avec les modules CSS dans Vue (les tactiques pour le CSS Scoped sont les mêmes) ;
- Styles groupés avec des composants stylés dans React ;
- Modules CSS génériques et styles globaux. Je les combine car les modules CSS, en général, sont très similaires aux feuilles de style globales et peuvent être importés par n'importe quel composant, ce qui déconnecte les styles de tout composant particulier.
Les contraintes d'abord : tout ce que nous faisons pour corriger le style ne doit pas casser le composant Alien lui-même . Sinon, nous perdons l'indépendance de nos systèmes Alien et Host. Donc, pour résoudre le problème de style, nous allons nous appuyer sur la configuration du bundler ou sur le wrapper Frankenstein.
Styles groupés dans Vue et Shadow DOM
Si vous écrivez une application Vue, vous utilisez très probablement des composants de fichier unique. Si vous utilisez également Webpack, vous devez être familiarisé avec deux chargeurs vue-loader
et vue-style-loader
. Le premier vous permet d'écrire ces composants de fichier uniques tandis que le second injecte dynamiquement le CSS du composant dans un document en tant que <style>
. Par défaut, vue-style-loader
injecte les styles du composant dans le <head>
du document. Cependant, les deux packages acceptent l'option shadowMode
dans la configuration, ce qui nous permet de modifier facilement le comportement par défaut et d'injecter des styles (comme le nom de l'option l'indique) dans Shadow DOM. Voyons-le en action.
Configuration Webpack
Au strict minimum, le fichier de configuration Webpack doit contenir les éléments suivants :
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() ] }
Dans une application réelle, votre bloc test: /\.css$/
sera plus sophistiqué (impliquant probablement la règle oneOf
) pour tenir compte à la fois des configurations Host et Alien. Cependant, dans ce cas, notre jQuery est stylisé avec un simple <link rel="stylesheet">
dans index.html
, donc nous ne construisons pas de styles pour Host via Webpack, et il est sûr de ne prendre en charge que Alien.
Configuration de l'encapsuleur
En plus de la configuration de Webpack, nous devons également mettre à jour notre wrapper Frankenstein, en pointant Vue vers le bon DOM Shadow. Dans notre Header-wrapper.js
, le rendu du composant Vue doit inclure la propriété shadowRoot
menant à shadowRoot
de notre wrapper Frankenstein :
... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...
Après avoir mis à jour les fichiers et redémarré votre serveur, vous devriez obtenir quelque chose comme ceci dans vos DevTools :
Enfin, les styles du composant Vue se trouvent dans notre Shadow DOM. En même temps, votre application devrait ressembler à ceci :
Nous commençons à obtenir quelque chose qui ressemble à notre application Vue : les styles fournis avec le composant sont injectés dans le DOM Shadow de l'encapsuleur, mais le composant n'a toujours pas l'apparence qu'il est censé avoir. La raison en est que dans l'application Vue d'origine, le composant est stylisé non seulement avec les styles groupés, mais aussi partiellement avec les styles globaux. Cependant, avant de fixer les styles globaux, nous devons mettre notre intégration React dans le même état que celle de Vue.
Styles groupés dans React et Shadow DOM
Parce qu'il existe de nombreuses façons de styliser un composant React, la solution particulière pour réparer un composant Alien dans Frankenstein Migration dépend de la façon dont nous stylisons le composant en premier lieu. Passons brièvement en revue les alternatives les plus couramment utilisées.
composants de style
styled-components est l'un des moyens les plus populaires de styliser les composants React. Pour le composant Header React, les composants de style sont précisément la façon dont nous le stylisons. Puisqu'il s'agit d'une approche CSS-in-JS classique, il n'y a pas de fichier avec une extension dédiée à laquelle nous pourrions accrocher notre bundler comme nous le faisons pour les fichiers .css
ou .js
, par exemple. Heureusement, les composants stylés permettent l'injection des styles du composant dans un nœud personnalisé (Shadow DOM dans notre cas) au lieu de l'en- head
du document à l'aide du composant d'aide StyleSheetManager
. Il s'agit d'un composant prédéfini, installé avec le package styled-components
qui accepte la propriété target
, définissant "un nœud DOM alternatif pour injecter des informations sur les styles". Exactement ce dont nous avons besoin ! De plus, nous n'avons même pas besoin de modifier notre configuration Webpack : tout dépend de notre wrapper Frankenstein.
Nous devrions mettre à jour notre Header-wrapper.js
qui contient le composant React Alien avec les lignes suivantes :
... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...
Ici, nous importons le composant StyleSheetManager
(de Alien, et non de Host) et enveloppons notre composant React avec. En même temps, nous envoyons la propriété target
pointant vers notre shadowRoot
. C'est ça. Si vous redémarrez le serveur, vous devez voir quelque chose comme ceci dans vos DevTools :
Maintenant, les styles de notre composant sont dans Shadow DOM au lieu de <head>
. De cette façon, le rendu de notre application ressemble maintenant à ce que nous avons vu avec l'application Vue précédemment.
Même histoire : les composants stylés ne sont responsables que de la partie groupée des styles du composant React , et les styles globaux gèrent les bits restants. Nous revenons un peu aux styles globaux après avoir passé en revue un autre type de composants de style.
Modules CSS
Si vous regardez de plus près le composant Vue que nous avons corrigé précédemment, vous remarquerez peut-être que les modules CSS sont précisément la façon dont nous stylisons ce composant. 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
. Cela n'a pas d'importance. 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. Comment faisons-nous cela?
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. Bonne nouvelle! 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.
Feuilles de style globales et option d' insert
de style-loader
Si vous consultez la documentation de cette option, vous remarquerez que cette option prend un sélecteur par configuration. Cela signifie que si vous avez plusieurs composants Alien nécessitant des styles globaux extraits dans un wrapper Frankenstein, vous devez spécifier style-loader
pour chacun des wrappers Frankenstein. En pratique, cela signifie que vous devez probablement vous fier à la règle oneOf
dans votre bloc de configuration pour servir tous les wrappers.
{ 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. ], }
Pas très souple, j'en conviens. Néanmoins, ce n'est pas grave tant que vous n'avez pas des centaines de composants à migrer. Sinon, cela pourrait rendre votre configuration Webpack difficile à maintenir. Le vrai problème, cependant, est que nous ne pouvons pas écrire un sélecteur CSS pour Shadow DOM.
En essayant de résoudre ce problème, nous pourrions noter que l'option d' insert
peut également prendre une fonction au lieu d'un simple sélecteur pour spécifier une logique plus avancée pour l'insertion. Avec cela, nous pouvons utiliser cette option pour insérer des feuilles de style directement dans Shadow DOM ! Sous une forme simplifiée, cela pourrait ressembler à ceci :
insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }
Tentant, n'est-ce pas ? Cependant, cela ne fonctionnera pas pour notre scénario ou fonctionnera loin d'être optimal. Notre <frankenstein-header-wrapper>
est en effet disponible depuis index.html
(car nous l'avons ajouté à l'étape 5.2). Mais lorsque Webpack traite toutes les dépendances (y compris les feuilles de style) pour un composant Alien ou un wrapper Frankenstein, Shadow DOM n'est pas encore initialisé dans le wrapper Frankenstein : les importations sont traitées avant cela. Par conséquent, pointer insert
directement sur shadowRoot entraînera une erreur.
Il n'y a qu'un seul cas où nous pouvons garantir que Shadow DOM est initialisé avant que Webpack ne traite notre dépendance de feuille de style. Si le composant Alien n'importe pas lui-même une feuille de style et qu'il incombe à l'encapsuleur Frankenstein de l'importer, nous pouvons utiliser l'importation dynamique et importer la feuille de style requise après avoir configuré Shadow DOM :
this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');
Cela fonctionnera : une telle importation, combinée à la configuration d' insert
ci-dessus, trouvera en effet le DOM Shadow correct et y insèrera la <style>
. Néanmoins, l'obtention et le traitement de la feuille de style prendront du temps, ce qui signifie que vos utilisateurs disposant d'une connexion lente ou d'appareils lents pourraient être confrontés un moment au composant non stylisé avant que votre feuille de style ne prenne sa place dans le DOM Shadow de wrapper.
Donc, dans l'ensemble, même si insert
accepte la fonction, malheureusement, ce n'est pas suffisant pour nous, et nous devons nous rabattre sur des sélecteurs CSS simples comme frankenstein-header-wrapper
. Cependant, cela ne place pas automatiquement les feuilles de style dans Shadow DOM, et les feuilles de style résident dans <frankenstein-header-wrapper>
en dehors de Shadow DOM.
Nous avons besoin d'une pièce de plus du puzzle.
Configuration du wrapper pour les feuilles de style globales et Shadow DOM
Heureusement, le correctif est assez simple du côté de l'encapsuleur : lorsque Shadow DOM est initialisé, nous devons vérifier les feuilles de style en attente dans l'encapsuleur actuel et les extraire dans Shadow DOM.
L'état actuel de l'importation de la feuille de style globale est le suivant :
- Nous importons une feuille de style qui doit être ajoutée dans Shadow DOM. La feuille de style peut être importée soit dans le composant Alien lui-même, soit explicitement dans le wrapper Frankenstein. Dans le cas d'une migration vers React par exemple, l'import est initialisé depuis le wrapper. Cependant, lors de la migration vers Vue, le composant similaire importe lui-même la feuille de style requise, et nous n'avons rien à importer dans le wrapper.
- Comme indiqué ci-dessus, lorsque Webpack traite
.css
importations .css pour le composant Alien, grâce à l'option d'insert
destyle-loader
, les feuilles de style sont injectées dans un wrapper Frankenstein, mais en dehors de Shadow DOM.
L'initialisation simplifiée de Shadow DOM dans le wrapper Frankenstein devrait actuellement (avant que nous n'utilisions des feuilles de style) ressembler à ceci :
this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`
Maintenant, pour éviter le scintillement du composant sans style, nous devons maintenant extraire toutes les feuilles de style requises après l'initialisation du DOM Shadow, mais avant le rendu du composant Alien.
this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})
C'était une longue explication avec beaucoup de détails, mais surtout, tout ce qu'il faut pour insérer des feuilles de style globales dans Shadow DOM :
- Dans la configuration Webpack, ajoutez
style-loader
avec l'option d'insert
pointant vers le wrapper Frankenstein requis. - Dans le wrapper lui-même, récupérez les feuilles de style « en attente » après l'initialisation de Shadow DOM, mais avant le rendu du composant Alien.
Après avoir implémenté ces changements, votre composant devrait avoir tout ce dont il a besoin. La seule chose que vous voudrez peut-être (ce n'est pas une exigence) ajouter est un CSS personnalisé pour affiner un composant Alien dans l'environnement de l'hôte. Vous pouvez même donner un style complètement différent à votre composant Alien lorsqu'il est utilisé dans Host. Cela va au-delà du point principal de l'article, mais vous regardez le code final du wrapper, où vous pouvez trouver des exemples de la façon de remplacer des styles simples au niveau du wrapper.
- Wrapper Frankenstein pour le composant React
- Wrapper Frankenstein pour le composant Vue
Vous pouvez également consulter la configuration Webpack à cette étape de la migration :
- Migration vers React avec des composants de style
- Migration vers React avec les modules CSS
- Migration vers Vue
Et enfin, nos composants ressemblent exactement à ce que nous voulions qu'ils ressemblent.
5.5. Résumé des styles de fixation pour le composant Alien
C'est le moment idéal pour résumer ce que nous avons appris jusqu'à présent dans ce chapitre. Il pourrait sembler que nous devions faire un travail énorme pour corriger le style du composant Alien ; cependant, tout se résume à:
- La correction des styles groupés implémentés avec des composants stylés dans les modules React ou CSS et Scoped CSS dans Vue est aussi simple que quelques lignes dans le wrapper Frankenstein ou la configuration Webpack.
- La correction des styles, implémentée avec les modules CSS, commence par une seule ligne dans la configuration
css-loader
. Après cela, les modules CSS sont traités comme une feuille de style globale. - La correction des feuilles de style globales nécessite la configuration du package
style-loader
avec l'option d'insert
dans Webpack et la mise à jour de l'encapsuleur Frankenstein pour extraire les feuilles de style dans Shadow DOM au bon moment du cycle de vie de l'encapsuleur.
Après tout, nous avons migré le composant Alien correctement stylé vers l'hôte. Cependant, il y a juste une chose qui pourrait ou non vous déranger selon le framework vers lequel vous migrez.
Bonne nouvelle d'abord : si vous migrez vers Vue , la démo devrait fonctionner correctement et vous devriez pouvoir ajouter de nouvelles tâches à partir du composant Vue migré. Cependant, si vous migrez vers React et essayez d'ajouter un nouvel élément de tâche, vous ne réussirez pas. L'ajout de nouveaux éléments ne fonctionne tout simplement pas et aucune entrée n'est ajoutée à la liste. Mais pourquoi? Quel est le problème? Aucun préjugé, mais React a ses propres opinions sur certaines choses.
5.6. Événements React et JS dans Shadow DOM
Peu importe ce que la documentation React vous dit, React n'est pas très convivial pour les composants Web. La simplicité de l'exemple dans la documentation ne supporte aucune critique, et tout ce qui est plus compliqué que le rendu d'un lien dans Web Component nécessite des recherches et des investigations.
Comme vous l'avez vu lors de la correction du style de notre composant Alien, contrairement à Vue où les composants Web sont presque prêts à l'emploi, React n'est pas prêt pour les composants Web. Pour l'instant, nous savons comment faire en sorte que les composants React soient au moins beaux dans les composants Web, mais il existe également des fonctionnalités et des événements JavaScript à corriger.
Pour faire court : Shadow DOM encapsule les événements et les recible, tandis que React ne prend pas en charge nativement ce comportement de Shadow DOM et n'intercepte donc pas les événements provenant de Shadow DOM. Il y a des raisons plus profondes à ce comportement, et il y a même un problème ouvert dans le traqueur de bogues de React si vous voulez plonger dans plus de détails et de discussions.
Heureusement, des gens intelligents ont préparé une solution pour nous. @josephnvu a fourni la base de la solution et Lukas Bombach l'a convertie en module npm react react-shadow-dom-retarget-events
. Vous pouvez donc installer le package, suivre les instructions sur la page des packages, mettre à jour le code de votre wrapper et votre composant Alien commencera comme par magie à fonctionner :
import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);
Si vous voulez qu'il soit plus performant, vous pouvez faire une copie locale du package (la licence MIT le permet) et limiter le nombre d'événements à écouter comme cela se fait dans le référentiel Frankenstein Demo. Pour cet exemple, je sais quels événements je dois recibler et je ne spécifie que ceux-là.
Avec cela, nous avons enfin (je sais que ce fut un long processus) terminé la migration appropriée du premier composant Alien stylé et entièrement fonctionnel. Offrez-vous une bonne boisson. Vous le méritez!
6. Rincez et répétez pour tous vos composants
Après avoir migré le premier composant, nous devons répéter le processus pour tous nos composants. Dans le cas de Frankenstein Demo, il n'en reste cependant qu'un : celui, chargé de restituer la liste des choses à faire.
De nouveaux wrappers pour de nouveaux composants
Commençons par ajouter un nouveau wrapper. Conformément à la convention de dénomination décrite ci-dessus (puisque notre composant React s'appelle MainSection.js
), le wrapper correspondant dans la migration vers React doit s'appeler MainSection-wrapper.js
. Dans le même temps, un composant similaire dans Vue s'appelle Listing.vue
, donc le wrapper correspondant dans la migration vers Vue devrait s'appeler Listing-wrapper.js
. Cependant, quelle que soit la convention de nommage, le wrapper lui-même sera presque identique à celui que nous avons déjà :
- Wrapper pour la liste React
- Wrapper pour la liste Vue
Il y a juste une chose intéressante que nous introduisons dans ce deuxième composant de l'application React. Parfois, pour cette raison ou une autre, vous voudrez peut-être utiliser un plugin jQuery dans vos composants. Dans le cas de notre composant React, nous avons introduit deux choses :
- Plugin d'info-bulle de Bootstrap qui utilise jQuery,
- Une bascule pour les classes CSS comme
.addClass()
et.removeClass()
.
Note : Cette utilisation de jQuery pour ajouter/supprimer des classes est purement illustrative. S'il vous plaît, n'utilisez pas jQuery pour ce scénario dans des projets réels - utilisez plutôt du JavaScript simple.
Bien sûr, cela peut sembler bizarre d'introduire jQuery dans un composant Alien lorsque nous migrons loin de jQuery, mais votre hôte peut être différent de l'hôte dans cet exemple - vous pouvez migrer loin d'AngularJS ou de toute autre chose. De plus, la fonctionnalité jQuery dans un composant et jQuery global n'est pas nécessairement la même chose.
Cependant, le problème est que même si vous confirmez que le composant fonctionne correctement dans le contexte de votre application Alien, lorsque vous le placez dans Shadow DOM, vos plugins jQuery et tout autre code qui s'appuie sur jQuery ne fonctionneront tout simplement pas.
jQuery dans Shadow DOM
Jetons un coup d'œil à une initialisation générale d'un plugin jQuery aléatoire :
$('.my-selector').fancyPlugin();
De cette façon, tous les éléments avec .my-selector
seront traités par fancyPlugin
. Cette forme d'initialisation suppose que .my-selector
est présent dans le DOM global. Cependant, une fois qu'un tel élément est placé dans Shadow DOM, tout comme avec les styles, les limites d'ombre empêchent jQuery de s'y faufiler. Par conséquent, jQuery ne peut pas trouver d'éléments dans Shadow DOM.
La solution consiste à fournir un deuxième paramètre facultatif au sélecteur qui définit l'élément racine à partir duquel jQuery effectue la recherche. Et c'est là que nous pouvons fournir notre shadowRoot
.
$('.my-selector', this.shadowRoot).fancyPlugin();
De cette façon, les sélecteurs jQuery et, par conséquent, les plugins fonctionneront parfaitement.
Gardez cependant à l'esprit que les composants Alien sont destinés à être utilisés à la fois : dans Alien sans shadow DOM et dans Host dans Shadow DOM. Nous avons donc besoin d'une solution plus unifiée qui ne supposerait pas la présence de Shadow DOM par défaut.
En analysant le composant MainSection
dans notre application React, nous constatons qu'il définit la propriété documentRoot
.
... this.documentRoot = this.props.root? this.props.root: document; ...
Donc, nous vérifions la propriété root
passée, et si elle existe, c'est ce que nous utilisons comme documentRoot
. Sinon, on revient à document
.
Voici l'initialisation du plugin tooltip qui utilise cette propriété :
$('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });
En prime, nous utilisons la même propriété root
pour définir un conteneur pour injecter l'info-bulle dans ce cas.
Maintenant, lorsque le composant Alien est prêt à accepter la propriété root
, nous mettons à jour le rendu du composant dans le wrapper Frankenstein correspondant :
// `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);
Et c'est tout! Le composant fonctionne aussi bien dans Shadow DOM que dans le DOM global.
Configuration Webpack pour scénario multi-wrappers
La partie passionnante se passe dans la configuration de Webpack lors de l'utilisation de plusieurs wrappers. Rien ne change pour les styles groupés comme ces modules CSS dans les composants Vue ou les composants stylés dans React. Cependant, les styles globaux devraient être légèrement modifiés maintenant.
Rappelez-vous, nous avons dit que style-loader
(responsable de l'injection des feuilles de style globales dans le DOM Shadow correct) est inflexible car il ne prend qu'un seul sélecteur à la fois pour son option d' insert
. Cela signifie que nous devrions diviser la règle .css
dans Webpack pour avoir une sous-règle par wrapper en utilisant la règle oneOf
ou similaire, si vous êtes sur un bundler autre que Webpack.
C'est toujours plus facile d'expliquer en utilisant un exemple, alors parlons de celui de la migration vers Vue cette fois (celui de la migration vers React est cependant presque identique) :
... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...
J'ai exclu css-loader
car sa configuration est la même dans tous les cas. Parlons plutôt de style-loader
. Dans cette configuration, nous insérons la <style>
dans *-header-*
ou *-listing-*
, selon le nom du fichier demandant cette feuille de style (règle de l' issuer
dans Webpack). Mais nous devons nous rappeler que la feuille de style globale requise pour le rendu d'un composant Alien peut être importée à deux endroits :
- Le composant Alien lui-même,
- Un emballage Frankenstein.
Et ici, nous devrions apprécier la convention de dénomination des wrappers, décrite ci-dessus, lorsque le nom d'un composant Alien et un wrapper correspondant correspondent. Si, par exemple, nous avons une feuille de style, importée dans un composant Vue appelé Header.vue
, elle obtient le wrapper *-header-*
correct. En même temps, si nous importons plutôt la feuille de style dans le wrapper, cette feuille de style suit exactement la même règle si le wrapper s'appelle Header-wrapper.js
sans aucun changement dans la configuration. Même chose pour le composant Listing.vue
et son wrapper correspondant Listing-wrapper.js
. En utilisant cette convention de nommage, nous réduisons la configuration dans notre bundler.
Une fois tous vos composants migrés, il est temps de passer à la dernière étape de la migration.
7. Passez à l'extraterrestre
À un moment donné, vous découvrez que les composants que vous avez identifiés à la toute première étape de la migration sont tous remplacés par des wrappers Frankenstein. Aucune application jQuery n'est vraiment laissée et ce que vous avez est, essentiellement, l'application Alien qui est collée ensemble à l'aide de Host.
Par exemple, la partie contenu de index.html
dans l'application jQuery — après la migration des deux microservices — ressemble maintenant à ceci :
<section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>
Pour le moment, il ne sert à rien de conserver notre application jQuery : à la place, nous devrions passer à l'application Vue et oublier tous nos wrappers, Shadow DOM et les configurations fantaisistes de Webpack. Pour ce faire, nous avons une solution élégante.
Parlons des requêtes HTTP. Je mentionnerai ici la configuration d'Apache, mais ce n'est qu'un détail d'implémentation : faire le changement dans Nginx ou quoi que ce soit d'autre devrait être aussi trivial que dans Apache.
Imaginez que votre site soit servi à partir du dossier /var/www/html
sur votre serveur. Dans ce cas, votre httpd.conf
ou httpd-vhost.conf
doit avoir une entrée qui pointe vers ce dossier comme :
DocumentRoot "/var/www/html"
Pour basculer votre application après la migration de Frankenstein de jQuery vers React, il vous suffit de mettre à jour l'entrée DocumentRoot
en quelque chose comme :
DocumentRoot "/var/www/html/react/build"
Construisez votre application Alien, redémarrez votre serveur et votre application est servie directement à partir du dossier Alien : l'application React servie à partir du dossier react/
. Cependant, il en va de même pour Vue, bien sûr, ou pour tout autre framework que vous avez également migré. C'est pourquoi il est si vital de garder Host et Alien complètement indépendants et fonctionnels à tout moment, car votre Alien devient votre hôte à cette étape.
Vous pouvez désormais supprimer en toute sécurité tout ce qui se trouve autour de votre dossier Alien, y compris tous les DOM Shadow, les wrappers Frankenstein et tout autre artefact lié à la migration. Le chemin a été difficile à certains moments, mais vous avez migré votre site. Toutes nos félicitations!
Conclusion
Nous avons certainement traversé un terrain quelque peu accidenté dans cet article. Cependant, après avoir commencé avec une application jQuery, nous avons réussi à la migrer vers Vue et React. Nous avons découvert des problèmes inattendus et pas si triviaux en cours de route : nous avons dû corriger le style, nous avons dû corriger la fonctionnalité JavaScript, introduire certaines configurations de bundle, et bien plus encore. Cependant, cela nous a donné un meilleur aperçu de ce à quoi s'attendre dans des projets réels. Au final, nous avons une application contemporaine sans aucun morceau restant de l'application jQuery même si nous avions tous les droits d'être sceptiques quant au résultat final pendant que la migration était en cours.
Frankenstein Migration n'est ni une solution miracle ni un processus effrayant. C'est juste l'algorithme défini, applicable à de nombreux projets, qui aide à transformer les projets en quelque chose de nouveau et de robuste de manière prévisible.