:: Enseignements :: ESIPE :: E3INFO :: 2022-2023 :: Programmation Web avec JavaScript ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
Librarie Réactive
|
Le but de ce TP est d'écrire une version simplifiée d'une bibliothèque réactive (comme react ou hyperapp)
de manipulation du DOM.
Voilà le code de la page HTML,
virtualdom.html, que l'on veut afficher
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8"/>
<script src="virtualdom.js" defer></script>
</head>
<body>
<div id = "my_node">
</div>
</body>
</html>
ainsi que le fichier
virtualdom.js
'use strict';
// Ecrire le reste du code ici !
render({
state: 0,
view: state =>
v("div", {}, [
v("h1", { class: "red" }, state),
v("button", { onclick: state => state - 1 }, "subtract"),
v("button", { onclick: state => state + 1 }, "add")
]),
node: document.getElementById("my_node")
});
Exercice 1 - Virtual DOMination
Une application réactive est une application qui change son affichage en fonction du changement
de valeur d'un objet particulier qui correspond à l'état de l'application.
Par exemple, une application simple qui affiche une valeur avec un bouton + et -
pour incrémenter ou décrémenter la valeur aura pour état un simple entier
tandis que des applications plus compliquées utiliseront des listes ou des objets comme état.
Une application réactive est composée de 3 valeurs différentes, l'état dont nous venons de parler,
la vue qui permet à partir de l'état de générer un arbre DOM qui sera affiché (une fonction donc)
et enfin un objet DOM de la page HTML qui va être remplacé par l'arbre DOM généré par la vue.
Donc de façon rudimentaire, l'application réactive qui incrémente et décrémente un entier peut se
décrire par l'object JavaScript suivant :
{
state: 0,
view: state => ... ,
node: document.getElementById("my_node")
}
Reste à savoir comment créer l'arbre DOM correspondant à l'état (les ... dans l'exemple, ci-dessus),
on se propose pour cela d'utiliser ce que l'on appelle un
virtual DOM qui est un arbre
comme l'arbre DOM, mais géré en JavaScript (contrairement à un arbre DOM qui est géré en C++ et
visible en JavaScript).
Voici un exemple de
virtual DOM
v("div", {}, [
v("h1", { class: "red" }, state),
v("button", { onclick: state => state - 1 }, "subtract"),
v("button", { onclick: state => state + 1 }, "add")
])
La fonction
v prend 3 paramètres, un nom d'élément (ici, div, h1 ou button), un ensemble
de propriétés qui sont les propriétés de l'élément (par exemple la class de l'élément h1 ou la propriété
onclick) et enfin une valeur qui peut être soit un élément, soit un tableau d'éléments
soit une valeur qui va être transformée en string pour être affichée.
Pour notre implantation, la fonction
v va renvoyer une instance de la classe
Element
ayant les attributs
name (le nom de l'élément),
properties un objet contenant les
propriétés de l'élément et
textOrChildren qui contient soit un élément, un tableau d'éléments
ou une valeur.
De plus, la classe
Element va posséder une méthode
toDOM qui permet à partir
d'un élément du
virtual-dom de transformer celui-ci en élément réel du DOM,
créé en utilisant les méthodes habituelles
document.createElement(),
document.createTextNode(),
element.appendChild() et
element.append().
On peut noter que la fonction prise en paramètre par
onclick n'est pas la fonction classique
prise par la propriété
onclick sur l'arbre DOM, en effet, cette fonction est ajoutée
sur le
virtual dom pas sur le DOM généré.
La fonction classique
onclick enregistrer lors de la génération de l'arbre DOM
par la méthode
toDOM va appeler la fonction du
virtual dom avec en paramétre
l'état de l'application, puis stocker le résultat de l'appel dans le champs
state
et enfin appeler la fonction
render pour changer l'affichage graphique.
Cette astuce permet d'être sur que lorsque l'on clique sur un bouton, l'état de l'application
est mis à jour et que l'affichage graphique est rafraîchi.
Si on met les différentes parties ensemble, on obtient le code suivant :
render({
state: 0,
view: state =>
v("div", {}, [
v("h1", { class: "red" }, state),
v("button", { onclick: state => state - 1 }, "subtract"),
v("button", { onclick: state => state + 1 }, "add")
]),
node: document.getElementById("my_node")
});
Lors de l'affichage de la page, la fonction
render prend l'objet correspondant à l'application,
récupère la valeur de l'état, appelle la fonction
view pour créer l'arbre d'éléments
(le
virtual dom, appelle la méthode
toDOM sur l'arbre d'élément pour créer l'arbre DOM
correspondant puis remplace l'objet
node par le nœud racine de l'arbre DOM créé
(le remplacement se fait en demandant le
parentNode de
node puis en appelant la méthode
replaceChild() sur le parent (attention, les arguments de replaceChild() sont inversés !)).
Le code JavaScript doit utiliser la version JavaScript 2015 (classes, fonctions flèches, etc).
Les deux dernières questions optionnelles sont indépendantes.
-
Ecrire la classe Element et son constructeur. Ecrire la fonction v
qui renvoie une nouvelle instance de la classe Element à chaque appel.
-
Ecrire dans la classe Element la méthode toDOM et vérifier
avec un exemple sans gestion des évènements que l'arbre DOM généré est correct.
Note: Array.isArray permet de savoir si un objet est un tableau et
value instanceof Element permet de savoir si la variable value est un Element.
-
Ecrire la fonction render et vérifier que l'affichage de la page HTML
affiche bien l'arbre DOM généré.
-
Ajouter la gestion des évènements de telle sorte à ce que l'exemple où l'on incrémente et décremente
une valeur ci-dessus fonctionne.
Note: value instanceof Function permet de savoir si la variable value est une fonction.
-
Optionnellement, le problème du design actuel est qu'il n'y a pas de notion de composant
avec leur état propre, on a une seule valeur d'état (state) pour toute l'application,
donc les différentes parties de l'application (composants) ne se composent pas bien.
Par exemple, si on veut deux compteurs, il va falloir avoir deux champs counter1 et
counter2 en tant que state et dupliquer le code de visualisation des deux compteurs.
render({
state: { counter1: 0, counter2: 0 },
view: state =>
v("div"), {} [
v("div", {}, [
v("h1", { class: "red" }, state),
v("button", { onclick: state => { state..., counter1: state.counter1 - 1 } }, "subtract"),
v("button", { onclick: state => { state..., counter1: state.counter1 + 1 } }, "add")
]),
v("div", {}, [
v("h1", { class: "red" }, state),
v("button", { onclick: state => { state..., counter2: state.counter2 - 1 } }, "subtract"),
v("button", { onclick: state => { state..., counter2: state.counter2 + 1 } }, "add")
])
])
node: document.getElementById("my_node")
});
Donc ce n'est pas très beau.
On se propose de faire une nouvelle implantation avec une notion de composant et d'état d'un composant.
Pour cela, on va créer une classe Reagent qui prend le nœud du DOM à modifier à la création
et qui possède deux méthodes useState() et view().
useState(default_value) permet de créer une nouvelle case mémoire avec une valeur par défaut
et renvoie un tableau avec une fonction getter et une fonction setter sur la case mémoire.
view(node, refresh) créée une méthode update de mise à jour qui,
appel refresh(), transforme l'élément renvoyé en objet DOM (avec toDOM()) puis installe ce nouvel arbre
à la place du node passé en paramètre (ou de l'ancien arbre si ce n'est pas le premier appel à update()).
Lorsque le setter renvoyé par useState est appelé, en plus de changer la valeur de la case mémoire,
on appel la méthode update pour mettre à jour l'interface graphique sur l'objet Reagent courant.
let reagent = new Reagent();
let counterComponent = () => {
let [getCounter, setCounter] = reagent.useState(0);
return () =>
v("div", {}, [
v("h1", { class: "red" }, getCounter()),
v("button", { onclick: () => setCounter(getCounter() - 1) }, "subtract"),
v("button", { onclick: () => setCounter(getCounter() + 1) }, "add")
]);
};
let counter1 = counterComponent();
let counter2 = counterComponent();
reagent.view(
document.getElementById("my_node"),
() => v("div", {}, [
counter1(),
counter2()
]));
Implanter la classe Reagent ainsi que ses méthodes useState et view
tel que le code ci-dessus fonctionne.
Note: la classe Element est plus exactement la même, donc il est plus simple de commenter
l'ancien code et d'en créer un nouveau.
-
Optionnellement, au lieu de remplacer tout le DOM à chaque changement, on peut utiliser une technique
que l'on appelle le DOM diffing pour ne remplacer que les nœuds du DOM qui ont changé.
Le diff d'un arbre consiste à parcourir un element et le nœud du DOM correspondant en même temps
et modifier le nœud du DOM si le le name ou properties sont différents
puis parcourir les fils et faire le diff sur les fils.
De plus, si le nombre de fils de l'élément et du nœud du DOM ne sont pas identiques,
il va falloir ajouter ou retirer des nœuds du DOM.
Donc au lieu de la méthode toDOM, créer une nouvelle méthode diffDOM dans Element
qui prend un nœud DOM et fait l'algorithme de DOM diffing décrit ci-dessus.
© Université de Marne-la-Vallée