«
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, , _c0); i0.ɵE(1, ); i0.ɵT(2); i0.ɵe(); i0.ɵE(3, , _c1); i0.ɵe(); i0.ɵe(); i0.ɵE(4, ); i0.ɵT(5, ); 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 AngularOn 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 CodeMê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 fonctionAinsi, 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?
