Une étude d'Ivy, le nouveau compilateur angulaire

« Je pense que les compilateurs sont très intéressants », explique Uri Shaked, l'auteur du document que nous publions aujourd'hui. L'année dernière, il a écrit un article qui parlait d'ingénierie inverse du compilateur angulaire et de simulation de certaines étapes du processus de compilation, ce qui aide à comprendre les caractéristiques de la structure interne de ce mécanisme. Il convient de noter qu'en général, ce que l'auteur de ce document parle de «compilateur» est appelé «moteur de rendu».

Quand Uri a appris qu'une nouvelle version du compilateur Angular, appelée Ivy, était sortie, il a immédiatement voulu regarder de plus près et découvrir ce qui avait changé en lui par rapport à l'ancienne version. Ici, comme auparavant, le compilateur reçoit les modèles et les composants créés par Angular, qui sont convertis en code HTML et JavaScript standard compréhensible pour Chrome et d'autres navigateurs.



Si vous comparez la nouvelle version du compilateur avec la précédente, il s'avère qu'Ivy utilise l'algorithme de tremblement d'arbre. Cela signifie que le compilateur supprime automatiquement les fragments de code inutilisés (cela s'applique également au code angulaire), ce qui réduit la taille des ensembles de projets. Une autre amélioration concerne le fait que désormais chaque fichier est compilé indépendamment, ce qui réduit le temps de recompilation. En résumé, grâce au nouveau compilateur, nous obtenons des assemblages plus petits, une recompilation accélérée des projets, un code prêt à l'emploi plus simple.

Comprendre le fonctionnement du compilateur est intéressant en soi (du moins l'auteur du matériel l'espère), mais il permet également de mieux comprendre les mécanismes internes d'Angular. Cela conduit à l'amélioration des compétences de "pensée angulaire", ce qui, à son tour, vous permet d'utiliser plus efficacement ce cadre pour le développement Web.

Au fait, savez-vous pourquoi le nouveau compilateur a été nommé Ivy? Le fait est que ce mot ressemble à une combinaison de lettres «IV», lues à haute voix, qui représente le chiffre 4, écrit en chiffres romains. «4» est la quatrième génération de compilateurs angulaires.

Application Ivy


Le lierre est toujours en cours de développement intensif, ce processus peut être observé ici . Bien que le compilateur lui-même ne soit pas encore adapté à une utilisation en combat, l'abstraction de RendererV3, qu'il utilisera, est déjà assez fonctionnelle et est livrée avec Angular 6.x.

Bien qu'Ivy ne soit pas encore tout à fait prêt, nous pouvons toujours jeter un coup d'œil aux résultats de son travail. Comment faire En créant un nouveau projet Angular:

ng new ivy-internals 

Après cela, vous devez activer Ivy en ajoutant les lignes suivantes au fichier tsconfig.json situé dans le nouveau dossier de projet:

 "angularCompilerOptions": { "enableIvy": true } 

Et enfin, nous démarrons le compilateur en exécutant la commande ngc dans le dossier de projet nouvellement créé:

 node_modules/.bin/ngc 

C’est tout. Vous pouvez maintenant examiner le code généré situé dans le dist/out-tsc . Par exemple, jetez un œil au fragment suivant du modèle AppComponent :

 <div style="text-align:center"> <h1>   Welcome to {{ title }}! </h1> <img width="300" alt="Angular Logo" src="…"> </div> 

Voici quelques liens pour vous aider à démarrer:


Le code généré pour ce modèle peut être trouvé en consultant le dist/out-tsc/src/app/app.component.js :

 i0.ɵE(0, "div", _c0); i0.ɵE(1, "h1"); i0.ɵT(2); i0.ɵe(); i0.ɵE(3, "img", _c1); i0.ɵe(); i0.ɵe(); i0.ɵE(4, "h2"); i0.ɵT(5, "Here are some links to help you start: "); i0.ɵe(); 

C'est dans ce type de code JavaScript qu'Ivy transforme le modèle de composant. Voici comment la même chose a été faite dans la version précédente du compilateur:


Code produit par une version précédente du compilateur Angular

On a le sentiment que le code généré par Ivy est beaucoup plus simple. Vous pouvez expérimenter avec le modèle de composant (il se trouve dans src/app/app.component.html ), le recompiler et voir comment les modifications qui y sont apportées affecteront le code généré.

Analyser le code généré


Essayons d'analyser le code généré et de voir exactement quelles actions il effectue. Par exemple, recherchons une réponse à une question sur la signification d'appels comme i0.ɵE et i0.ɵT

Si vous regardez au début du fichier généré, nous trouverons l'expression suivante:

 var i0 = require("@angular/core"); 

Donc i0 n'est que le module de base Angular, et toutes ces fonctions sont exportées par Angular. La lettre ɵ utilisée par l'équipe de développement Angular pour indiquer que certaines méthodes sont uniquement destinées à fournir des mécanismes de cadre interne , c'est-à-dire que les utilisateurs ne doivent pas les appeler directement, car l'invariance de l'API de ces méthodes n'est pas garantie lorsque de nouvelles versions d'Angular sont publiées (en fait, Je dirais que leurs API sont presque garanties de changer).

Ainsi, toutes ces méthodes sont des API privées exportées par Angular. Il est facile de comprendre leurs fonctionnalités en ouvrant le projet dans VS Code et en analysant les info-bulles:


Analyse de code dans VS Code

Même si un fichier JavaScript est analysé ici, VS Code utilise les informations de type de TypeScript pour identifier la signature d'appel et trouver la documentation d'une méthode particulière. Si, après avoir sélectionné le nom de la méthode, utilisez la combinaison Ctrl + clic (Cmd + clic sur Mac), nous constatons que le vrai nom de cette méthode est elementStart .

Cette technique a permis de découvrir que le nom de méthode ɵT est du text , le nom de méthode ɵe est ɵe . Armés de ces connaissances, nous pouvons «traduire» le code généré, en le transformant en quelque chose qui sera plus pratique à lire. Voici un petit fragment d'une telle "traduction":

 var core = require("angular/core"); //... core.elementStart(0, "div", _c0); core.elementStart(1, "h1"); core.text(2); core. (); core.elementStart(3, "img", _c1); core.elementEnd(); core.elementEnd(); core.elementStart(4, "h2"); core.text(5, "Here are some links to help you start: "); core.elementEnd(); 

Et, comme déjà mentionné, ce code correspond au texte suivant du modèle HTML:

 <div style="text-align:center"> <h1>   Welcome to {{ title }}! </h1> <img width="300" alt="Angular Logo" src="…"> </div> 

Voici quelques liens pour vous aider à démarrer:


Après avoir analysé tout cela, il est facile de remarquer ce qui suit:

  • Chaque balise HTML d'ouverture a un appel à core.elementStart() .
  • Les balises de fermeture correspondent aux appels à core.elementEnd() .
  • Les nœuds de texte correspondent aux appels à core.text() .

Le premier argument des méthodes elementStart et text est un nombre dont la valeur augmente à chaque appel. Il représente probablement un index dans un tableau dans lequel Angular stocke des liens vers des éléments créés.

Le troisième argument est également transmis à la méthode elementStart . Après avoir étudié les matériaux ci-dessus, nous pouvons conclure que l'argument est facultatif et contient une liste d'attributs pour le nœud DOM. Vous pouvez le vérifier en regardant la valeur de _c0 et en découvrant qu'il contient une liste d'attributs et leurs valeurs pour l'élément div :

 var _c0 = ["style", "text-align:center"]; 

NgComponentDef Note


Jusqu'à présent, nous avons analysé la partie du code généré qui est responsable du rendu du modèle pour le composant. Ce code est en fait situé dans un morceau de code plus grand qui est affecté à AppComponent.ngComponentDef - une propriété statique qui contient toutes les métadonnées sur le composant, telles que les sélecteurs CSS, sa stratégie de détection des modifications (si une est spécifiée) et le modèle. Si vous avez envie d'aventure - vous pouvez maintenant déterminer de façon indépendante comment cela fonctionne, bien que nous en parlerons ci-dessous.

Lierre fait maison


Maintenant que nous, en termes généraux, comprenons à quoi ressemble le code généré, nous pouvons essayer de créer, à partir de zéro, notre propre composant en utilisant la même API RendererV3 qu'Ivy utilise.

Le code que nous allons créer sera similaire au code produit par le compilateur, mais nous le rendrons plus facile à lire.

Commençons par écrire un composant simple, puis traduisons-le manuellement en code similaire à celui obtenu par Ivy:

 import { Component } from '@angular/core'; @Component({ selector: 'manual-component', template: '<h2><font color="#3AC1EF">Hello, Component</font></h2>', }) export class ManualComponent { } 

Le compilateur prend l'entrée du décorateur @component comme @component , crée des instructions, puis organise tout cela en tant que propriété statique de la classe de composants. Par conséquent, afin de simuler l'activité d'Ivy, nous @component décorateur @component et le remplaçons par la propriété statique ngComponent :

 import * as core from '@angular/core'; export class ManualComponent { static ngComponentDef = core.ɵdefineComponent({   type: ManualComponent,   selectors: [['manual-component']],   factory: () => new ManualComponent(),   template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {     //       }, }); } 

Nous définissons les métadonnées du composant compilé en appelant ɵdefineComponent . Les métadonnées incluent le type de composant (utilisé précédemment pour implémenter la dépendance), le sélecteur CSS (ou les sélecteurs) qui appellera ce composant (dans notre cas, manual-component est le nom du composant dans le modèle HTML), la fabrique qui renvoie la nouvelle instance composant, puis la fonction qui définit le modèle du composant. Ce modèle affiche une représentation visuelle du composant et le met à jour lorsque les propriétés du composant changent. Afin de créer ce modèle, nous utiliserons les méthodes que nous avons trouvées ci-dessus: ɵE , ɵe et ɵT .

     template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {     core.ɵE(0, 'h2');                 //    h2     core.ɵT(1, 'Hello, Component');   //       core.ɵe();                        //    h2   }, 

À ce stade, nous n'utilisons pas les paramètres rf ou ctf fournis par notre fonction de modèle. Nous y reviendrons. Mais d'abord, regardons comment afficher notre premier composant fait maison sur l'écran.

Première application


Afin d'afficher les composants à l'écran, Angular exporte une méthode appelée ɵrenderComponent . Tout ce que vous devez faire est de vérifier que le fichier index.html contient une balise HTML correspondant au sélecteur d'élément, <manual-component> , puis ajoutez ce qui suit à la fin du fichier:

 core.ɵrenderComponent(ManualComponent); 

C’est tout. Nous avons maintenant une application angulaire self-made minimale composée de seulement 16 lignes de code. Vous pouvez expérimenter avec l'application terminée sur StackBlitz .

Changer le mécanisme de détection


Donc, nous avons un exemple de travail. Pouvez-vous y ajouter de l'interactivité? Dites, que diriez-vous de quelque chose d'intéressant, comme utiliser le système de détection de changement d'Angular ici?

Modifiez le composant afin que l'utilisateur puisse personnaliser le texte de bienvenue. Autrement dit, au lieu que le composant affiche toujours le texte Hello, Component , nous allons laisser l'utilisateur modifier la partie du texte qui vient après Hello .

Nous commençons par ajouter la propriété name et une méthode pour mettre à jour la valeur de cette propriété dans la classe de composants:

 export class ManualComponent { name = 'Component'; updateName(newName: string) {   this.name = newName; } // ... } 

Certes, tout cela ne semble pas particulièrement impressionnant, mais le plus intéressant est à venir.

Ensuite, nous éditerons la fonction modèle afin qu'au lieu de texte immuable, elle affiche le contenu de la propriété name :

 template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) {   // :        core.ɵE(0, 'h2');   core.ɵT(1, 'Hello, ');   core.ɵT(2);   // <--   name   core.ɵe(); } if (rf & 2) {   // :       core.ɵt(2, ctx.name);  // ctx -     } }, 

Vous avez peut-être remarqué que nous avons encapsulé les instructions du modèle dans les instructions if qui vérifient les valeurs rf . Ce paramètre est utilisé par Angular pour indiquer si le composant est créé pour la première fois (le bit le moins significatif sera défini ), ou nous avons juste besoin de mettre à jour le contenu dynamique dans le processus de détection des changements (c'est ce à quoi la seconde if est destinée).

Ainsi, lorsque le composant est affiché pour la première fois, nous créons tous les éléments, puis, lorsque des modifications sont détectées, nous ne mettons à jour que ce qui pourrait changer. La méthode interne ɵt est responsable (notez la lettre minuscule t ), ce qui correspond à la fonction textBinding exportée par Angular:


Texte de fonction

Ainsi, le premier paramètre est l'indice de l'élément à mettre à jour, le second est la valeur. Dans ce cas, nous créons un élément de texte vide avec l'index 2 avec la commande core.ɵT(2); . Il agit comme un espace réservé pour le name . Nous le mettons à jour avec la commande core.ɵt(2, ctx.name); lors de la détection d'un changement dans la variable correspondante.

Pour le moment, la sortie de ce composant affichera toujours le texte Hello, Component , bien que nous puissions changer la valeur de la propriété name , ce qui entraînera une modification du texte à l'écran.

Pour que l'application devienne vraiment interactive, nous ajouterons ici un champ de saisie de données avec un écouteur d'événement qui appelle la méthode du composant updateName() :

 template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) {   core.ɵE(0, 'h2');   core.ɵT(1, 'Hello, ');   core.ɵT(2);   core.ɵe();   core.ɵT(3, 'Your name: ');   core.ɵE(4, 'input');   core.ɵL('input', $event => ctx.updateName($event.target.value));   core.ɵe(); } // ... }, 

La liaison d'événement est effectuée dans la ligne core.ɵL('input', $event => ctx.updateName($event.target.value)); . À savoir, la méthode ɵL responsable de la définition de l'écouteur d'événements pour le plus récent des éléments déclarés. Le premier argument est le nom de l'événement (dans ce cas, l' input est l'événement qui est déclenché lorsque le contenu de l'élément <input> change), le deuxième argument est un rappel. Ce rappel accepte les données d'événement comme argument. Ensuite, nous extrayons la valeur actuelle de l'élément cible de l'événement, c'est-à-dire de l'élément <input> , et la transmettons à la fonction dans le composant.

Le code ci-dessus équivaut à écrire le code HTML suivant dans un modèle:

 Your name: <input (input)="updateName($event.target.value)" /> 

Vous pouvez maintenant modifier le contenu de l'élément <input> et observer comment le texte du composant change. Cependant, le champ de saisie n'est pas rempli lorsque le composant est chargé. Pour que tout fonctionne de cette façon, vous devez ajouter une instruction supplémentaire au code de fonction de modèle, exécutée lorsqu'une modification est détectée:

 template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) { ... } if (rf & 2) {   core.ɵt(2, ctx.name);   core.ɵp(4, 'value', ctx.name); } } 

Ici, nous utilisons une autre méthode intégrée du système de rendu, ɵp , qui met à jour la propriété d'un élément avec un index donné. Dans ce cas, l'index 4 est transmis à la méthode, qui est l'index affecté à l'élément d' input , et nous indiquons ctx.name méthode de placer la valeur ctx.name dans la propriété value de cet élément.

Maintenant, notre exemple est enfin prêt. Nous avons implémenté, à partir de zéro, une liaison de données bidirectionnelle à l'aide de l'API du système de rendu Ivy. C'est tout simplement génial.
Ici, vous pouvez expérimenter avec le code fini.

Nous connaissons maintenant la plupart des éléments de base du nouveau compilateur Ivy. Nous savons comment créer des éléments et des nœuds de texte, comment lier des propriétés et configurer des écouteurs d'événements et comment utiliser le système de détection des modifications.

À propos des blocs * ngIf et * ngFor


Avant de terminer l'étude Ivy, regardons un autre sujet intéressant. À savoir, parlons de la façon dont le compilateur fonctionne avec les sous-modèles. Ce sont les modèles utilisés pour les *ngIf ou *ngFor . Ils sont traités d'une manière spéciale. Voyons comment utiliser *ngIf dans notre code de modèle maison.

Vous devez d'abord installer le paquet npm @angular/common - c'est là que *ngIf . Ensuite, vous devez importer la directive de ce package:

 import { NgIf } from '@angular/common'; 

Maintenant, afin de pouvoir utiliser NgIf dans le modèle, vous devez lui fournir des métadonnées, car le module @angular/common n'a pas été compilé avec Ivy (au moins lors de l'écriture du matériel, et à l'avenir, cela changera probablement de introduction de ngcc ).

Nous allons utiliser la méthode ɵdefineDirective , qui est liée à la méthode familière ɵdefineComponent . Il définit les métadonnées des directives:

 (NgIf as any).ngDirectiveDef = core.ɵdefineDirective({ type: NgIf, selectors: [['', 'ngIf', '']], factory: () => new NgIf(core.ɵinjectViewContainerRef(), core.ɵinjectTemplateRef()), inputs: {ngIf: 'ngIf', ngIfThen: 'ngIfThen', ngIfElse: 'ngIfElse'} }); 

J'ai trouvé cette définition dans le code source angulaire , avec la ngFor . Maintenant que nous avons préparé NgIf pour une utilisation dans Ivy, nous pouvons ajouter ce qui suit à la liste des directives pour le composant:

 static ngComponentDef = core.ɵdefineComponent({ directives: [NgIf], // ... }); 

Ensuite, nous définissons le sous-modèle uniquement pour la partition délimitée par *ngIf .

Supposons que vous deviez afficher une image. Définissons une nouvelle fonction pour ce modèle à l'intérieur de la fonction de modèle:

 function ifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) { if (rf & 1) {   core.ɵE(0, 'div');   core.ɵE(1, 'img', ['src', 'https://pbs.twimg.com/tweet_video_thumb/C80o289UQAAKIqp.jpg']);   core.ɵe(); } } 

Cette fonction de modèle n'est pas différente de celle que nous avons déjà écrite. Il utilise les mêmes constructions pour créer un élément img intérieur d'un élément div .

Et enfin, nous pouvons tout rassembler en ajoutant la directive ngIf au modèle de composant:

 template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) {   // ...   core.ɵC(5, ifTemplate, null, ['ngIf']); } if (rf & 2) {   // ...   core.ɵp(5, 'ngIf', (ctx.name === 'Igor')); } function ifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) {   // ... } }, 

Notez l'appel à la nouvelle méthode au début du code ( core.ɵC(5, ifTemplate, null, ['ngIf']); ). Il déclare un nouvel élément conteneur, c'est-à-dire un élément qui a un modèle. Le premier argument est l'indice de l'élément, nous avons déjà vu de tels index. Le deuxième argument est la fonction de sous-modèle que nous venons de définir. Il sera utilisé comme modèle pour l'élément conteneur. Le troisième paramètre est le nom de balise de l'élément, ce qui n'a pas de sens ici, et enfin, il y a une liste de directives et d'attributs associés à cet élément. C'est là ngIf .

Dans la ligne core.ɵp(5, 'ngIf', (ctx.name === 'Igor')); l'état de l'élément est mis à jour en liant l'attribut ngIf à la valeur de l'expression logique ctx.name === 'Igor' . Cela vérifie si la propriété name du composant est égale à Igor .

Le code ci-dessus est équivalent au code HTML suivant:

 <div *ngIf="name === 'Igor'"> <img align="center" src="..."> </div> 

Ici, on peut noter que le nouveau compilateur ne produit pas le code le plus compact, mais il n'est pas si mauvais par rapport à ce qu'il est maintenant.

Vous pouvez expérimenter avec un nouvel exemple ici . Pour voir la section NgIf en action, entrez le nom Igor dans le champ Your name .

Résumé


Nous avons à peu près parcouru les capacités du compilateur Ivy. J'espère que ce voyage a suscité votre intérêt pour une exploration plus approfondie d'Angular. Si oui, alors vous avez maintenant tout ce dont vous avez besoin pour expérimenter avec Ivy. Vous savez maintenant comment «traduire» des modèles en JavaScript, comment accéder aux mêmes mécanismes angulaires qu'Ivy utilise sans utiliser ce compilateur. Je suppose que tout cela vous donnera l'occasion d'explorer les nouveaux mécanismes angulaires aussi profondément que vous le souhaitez.

Ici , ici et ici - trois matériaux dans lesquels vous pouvez trouver des informations utiles sur Ivy. Et voici le code source de Render3.

Chers lecteurs! Que pensez-vous des nouvelles fonctionnalités d'Ivy?

Source: https://habr.com/ru/post/fr419995/


All Articles