- Les démonstrations de ce TD se basent sur les sources disponibles [ ici ].
- Attention, seul le répertoire src est dans l'archive. Il faut donc créer un projet vide et remplacer son répertoire src.
1°/ Principes de vuejs
- L'objectif de Vuejs est de prendre en charge l'affichage et le rafraîchissement automatique d'éléments d'une page HTML quand la valeur de variables JS change.
- Pour cela, vuejs met en place des mécanismes d'observation de ces variables et déclenche la mise à jour du DOM quand leur valeur change.
- Les valeurs de ces variables peuvent aussi bien venir d'un script JS qui est chargé localement dans le navigateur, que de requêtes à un serveur qui va répondre au navigateur avec par exemple des données au format JSON.
- Le principe de création d'une application Web avec vuejs repose donc clairement sur le principe de séparation stricte entre la visualisation des données et le contrôle des actions utilisateur, qui est entièrement géré par le navigateur, et la fourniture des données qui est gérée par des serveurs.
- L'utilisation de vuejs nécessite de penser autrement le développement web, par rapport aux pratiques classiques liées au PHP/Python/... où le serveur interprète un langage pour produire du HTML.
- Avec vuejs, le serveur Web se contente d'envoyer au navigateur un unique fichier html, des scripts JS, du css. Cet ensemble de fichiers peut être entièrement écrit "à la main", ou bien partiellement généré grâce à un environnement de développement tel que vue-cli (pour vuejs V2) ou vite (pour vuejs V3)
- Contrairement à l'approche php, les scripts JS exécutés par le navigateur contiennent toute la logique de fonctionnement de l'application Web.
- Quand des données externes sont nécessaires, les scripts JS peuvent interroger un serveur de type API, pour aller les récupérer avec une requête asynchrone. Le navigateur n'a donc pas besoin de demander au serveur Web une autre page. C'est pourquoi une application web faite avec vuejs est appelée SPA = Single Page Application, car il n'y a qu'un seul fichier html pour toute l'application.
- Cette approche est la même que pour une application client/serveur basée sur des sockets.
- Son énorme avantage est de pouvoir développer la partie front-end (navigateur) et le back-end (serveurs) en parallèle.
- La seule chose qui doit être réglée avant est de spécifier le format des requêtes et des données échangées entre le navigateur et les serveurs.
- Pour créer et tester une application vuejs, il suffit d'avoir à sa disposition un éditeur de texte et un navigateur.
- En effet, il n'y a pas spécialement besoin d'héberger les fichiers créés sur un serveur Web, à part quand on passe en phase de déploiement/production.
- Pour autant, en milieu professionnel, la création d'applications web de type SPA avec Vuejs ne se fait pas en codant tout "à la main".
- En effet, vuejs repose sur la définition de composants. Un composant contient généralement une partie décrivant son aspect visuel (HTML) et une autre partie décrivant son aspect fonctionnel (en JS).
- Or, écrire des composants et les assembler sans passer par un environnement de développement tel que vue-cli/vite est relativement verbeux et fastidieux.
- C'est pourquoi on utilise tout le temps ce type d'environnement, à part pour de très petits exemples pédagogiques (cf. article hello world pour un exemple en vuejs 2)
- Premièrement, vite comporte des outils permettant de créer une application web à partir de fichiers dont la syntaxe n'est pas du pur javascript. Ce sont des fichiers dits SFC = Single File Components qui permettent de décrire des composants, à savoir leur visuel écrit en HTML + instructions vuejs, leurs données et fonctions écrit en JS, et leur style écrit en CSS. Leur description est donnée en section 2.
- Il y a également des logiciels associés à vite permettant de faire la "compilation" et l'assemblage de ces fichiers pour produire une application.
- L'organisation du code de l'application est modulaire, à savoir qu'un fichier constitue un module qui va "exporter" des variables/fonctions que d'autres fichiers (donc modules) vont pouvoir importer pour les manipuler. En terme de résultat, c'est un peu similaire au include du php.
- Deuxièmement, vite est un gestionnaire de projet :
- on peut créer facilement un arborescence basique d'application,
- on peut ajouter des modules/plugins au projet,
- on peut tester rapidement l'application,
- on peut créer (et tester) une version de production, prête à être déployée sur un serveur Web.
- etc.
2°/ Description d'un fichier SFC pour vuejs (extension .vue)
2.1°/ Structuration générale
- Un fichier .vue décrit un composant en 3 parties :
<template>
<!-- visuel du composant, en HTML + directive vuejs -->
</template>
<script setup>
// modèle des données et du contrôle du composant, écrit en JS
</script>
<style>
// définition des styles CSS
</style>
- Seule la partie <template> est obligatoire. Comme indiqué, cette partie est rédigée en HTML, plus les directives introduites par vuejs.
- ATTENTION : <template> ne doit contenir qu'une seule balise "racine". Bien entendu, cette balise peut contenir elle-même d'autres balises. C'est pourquoi on utilise très souvent la balise <div> comme balise racine.
- La partie <script> devient nécessaire lorsque la partie <template> doit afficher des données non statiques et sans utiliser les anciens principes de manipulation du DOM.
- Dans ce cas, la partie <script> va décrire les données locales du composant, celles reçues du composant parent, ainsi que les traitements manipulant ces données.
2.2°/ partie <script>
- La façon d'écrire cette partie dépend de la syntaxe utilisée, à savoir celle de vuejs V2 ou bien V3.
- Comme la V2 arrive en fin de vie, ce TD ne décrit que la nouvelle syntaxe.
IMPORTANT :
- Pour écrire un même composant, il n'est pas possible de mélanger les 2 syntaxes. Il faut en choisir une.
- En revanche, il est parfaitement possible de créer une application utilisant des composants écrits avec les deux syntaxes, comme dans l'exemple de démonstration.
- Cependant, les composants écrits en V2 peuvent être incompatibles avec certains plugins écrits pour la V3 (pinia, vue-router, ...). C'est pourquoi il est parfois nécessaire de migrer des composants en syntaxe V3.
- Le patron de code ci-dessous donne la plupart des sous parties se plaçant dans la partie script :
<script setup>
// importation de modules/fonctions/objets/...
// définition des variables locales, avec ref/reactive/...
// définition des "props"
// définition des variables "computed"
// définition des fonctions normales
// définition des watchers
// définition des "hook" (onMounted, ...)
</script>
Remarques :
- l'ordre de ces sous-parties n'est pas fixé et elles peuvent même être entremêlées, bien que cela rende le code peu lisible.
- aucune de ces sous-parties n'est obligatoire (certains composants peuvent n'avoir aucune partie script). Leur utilisation dépend des besoins fonctionnels du composant.
- il existe d'autres sous-partie possibles, mais beaucoup plus rares d'utilisation.
- utiliser setup comme attribut de balise permet de simplifier drastiquement l'écriture. Cependant, ce n'est pas très compliqué d'écrire sans cet attribut, juste plus fastidieux.
2.2.1°/ imports
- Pour écrire un composant vuejs, il est nécessaire d'utiliser certaines fonctions fournies par vuejs ou un plugins vuejs (pinia, vue-router, vuetify, ...).
- De plus, il est fréquent qu'un composant utilise un autre composant.
- Enfin, du code JS et/ou des données peuvent être définies dans un fichier externe.
- Dans tous ces cas, il faut au préalable importer les éléments nécessaire, grâce à l'instruction import de JS.
- La syntaxe d'utilisation de cette instruction est multiple et dépend de la façon dont un plugin/composant/... exporte des données/fonctions/... Mais il y a globalement deux cas principaux, qui sont résumés dans l'exemple ci-dessous
import {ref, computed} from "vue"
import MonComp from '@/components/MyCompo.vue'
- Dans le premier cas, on importe les fonctions ref() et computed(), définies dans le module vue,
- Dans le deuxième cas, on importe sous le nom MonComp, le composant définit dans le fichier MyComp.vue.
- On remarque que quand on veut référencer un fichier du projet, c'est une bonne idée d'utiliser @ pour indiquer un chemin qui commence à la racine des sources, donc au répertoire src.
2.2.2°/ variables locales
- La plupart des composants ont besoin de manipuler des variables pour stocker des données, calculer, ...
- Si ces variables sont ensuite utilisées pour afficher quelque chose dans le template, l'usage correct de vuejs consiste à utiliser des variables qui sont constamment "observées" par vuejs.
- Ainsi, quand la valeur d'une telle variable change, vuejs s'en aperçoit et va modifier le DOM là où la variable est utilisée pour l'affichage, et ainsi rafraîchir automatiquement la page.
- C'est le même principe de réactivité, qui se retrouve dans le framework React.
- Pour déclarer/définir une telle variable, il est obligatoire de faire appel à des fonctions spéciales de vuejs, soit ref(), soit reactive().
- La première est la seule solution possible pour déclarer une variable de type primaire : int, booléen, string, ... En effet, reactive() ne permet que de déclarer des types objet (objets, tableaux, Map, ...), alors que ref() permet de déclarer tous les types possibles.
- L'inconvénient de ref() est que dans le reste de la partie script (NB: mais pas dans template, cf. exemple ci-dessous), on ne modifie pas directement la variable ainsi créée, mais son attribut value. Ce n'est pas le cas des champs/valeurs d'un objet reactive, que l'on peut manipuler directement.
- Au final, on peut n'utiliser que ref(), ou mixer les deux, selon ses goûts.
- A noter que le bon usage de ces deux fonctions est de créer non pas une variable, mais une constante, donc le contenu sera lui, variable (cf. exemple ci-dessous)
- A noter également qu'un objet ainsi créé est totalement observé, même les sous-objets qu'il peut contenir (= réactivité profonde)
ATTENTION : quelle que soit la fonction utilisée, une variable contenant un tableau doit être manipulée via les fonctions de la classe Array : push(), splice(), ... Sinon, vuejs ne verra pas le changement du contenu du tableau et il n'y aura pas de réactivité.
- Exemple :
<template>
{{ val }}
{{ perso }}
...
</template>
<script setup>
...
const val = ref(10)
const perso = reactive({name:"toto", age: 20})
const tabref = ref([])
const tabreac = reactive([])
...
val = 5 // INCORRECT
val.value = 5 // OK
perso.name = "tutu" //OK
perso.age = 21 // OK
tabref.value[0] = 3 // PAS REACTIF
tabref.value.push(2) // OK
tabreac.push(2) // OK
...
<script>
2.2.3°/ props
- Une props est un type spéciale de variable locale à un composant, dont la valeur est fournie par le composant parent (s'il existe)
- Cela correspond à la situation d'un composant A, qui a dans son template une balise <B> ... </B>, où B est le nom d'un autre composant. Dans ce cas, A peut passer à B des valeurs comme props, en utilisant le nom de la props comme un attribut.
- Par exemple, dans la démonstration, le composant MyComponentV3 a une props nommé title. Pour que le composant App donne une valeur à cette props, il suffit d'écrire dans son template <MyComponentV3 title="ma_valeur">
- Vuejs va également observer ces "props" et en cas de changement, selon où elles sont utilisées, réactualiser le DOM, donc l'affichage de l'application.
- Pour créer des props, on utilise la fonction prédéfinie (= pas besoin de l'importer) defineProps(). Celle-ci renvoie un objet qui contient les props et que l'on peut utiliser dans le reste de la partie script. Dans le template, on peut utiliser directement le nom de la props.
- Le paramètre de cette fonction est un objet définissant les noms et types des props.
ATTENTION : on ne doit JAMAIS modifier directement la valeur d'une props, par exemple dans une fonction, un watcher, ...
Exemple :
<template>
{{ title }}
</template>
<script setup>
const props = defineProps({
title: String
})
console.log(props.title)
...
</script>
2.2.4°/ variable computed
- C'est également un type spécial de variable locale à un composant.
- Chacune de ces variables est associée à une fonction (dite computed) qui va calculer leur valeur, à partir des variables locales, des props, ou d'autres variables computed. Chaque fois que ces dernières changent, vuejs appelle la fonction associée pour recalculer automatiquement la nouvelle valeur de la variable computed.
- Pour créer l'association, on doit utiliser la fonction computed() fournie par vuejs. Cette dernère prend en paramètre la fonction que l'on veut associer à la variable computed.
- Malheureusement, il n'est pas possible de donner des paramètres à la fonction associée. Il est cependant possible de contourner cette limitation, car la valeur d'une variable computed peut être de n'importe quel type, y compris fonction.
- Exemple :
<script setup>
...
const idx = ref(0)
const tab = ref([])
...
const idxSafe = computed( () => { if ((idx.value<0) || (idx.value >= tab.value.length)) return 0; return idx.value } )
const getval = computed( () => index => tab.value[index] )
...
console.log(getval.value(10))
</script>
Explication : la fonction associée à getval() renvoie une fonction avec un paramètre. Écrire getval.value permet de récupérer cette fonction, et donc getval.value(...) d'appeler cette fonction.
2.2.5°/ méthodes "normales"
- Comme en POO, un composant a également besoin de méthodes pour faire de traitement qui vont modifier les valeurs des variables locales.
- Pour définir une telle méthode, il suffit d'écrire une fonction JS classique :
<script setup>
...
function maFct(val, msg) {
...
}
...
<script>
- Cette fonction peut ensuite être appelée par d'autres fonctions de la partie script, ou bien dans le template.
2.2.6°/ watchers
- watch() est une fonction permettant de mettre en place un mécanisme d'observation d'une variable particulière.
- En vuejs, il est normalement très rare d'utiliser des watcher, car la plupart des besoins en réactivité peuvent être mis en place grâce aux variables locales et computed.
- Le cas le plus courant d'utilisation est lorsque l'on veut déclencher automatiquement un traitement relativement complexe lorsqu'une, et une seule variable change de valeur.
- On peut tout imaginer comme traitement : requête à une API, affichage d'alertes, appeler d'autres fonctions, ...
- En revanche, si le traitement consiste simplement à changer la valeur de une ou plusieurs variables locales, computed() est normalement suffisant. (NB : ce type de situation abusive de watch() fait partie de l'exemple de démonstration).
- Pour mettre en place un "watcher", il faut appeler watch() avec comme paramètre une variable observée et une fonction associée qui sera appelée automatiquement en cas de changement de la variable. La fonction peut prendre un premier paramètre qui est la nouvelle valeur de la variable, et éventuellement un deuxième qui est l'ancienne valeur.
Exemple :
<script setup>
...
const val = ref(0)
...
watch( val, (newVal) => { if (newVal<0) alert("valeur positive") } )
...
</script>
2.2.7°/ hooks
- Quand vuejs crée un composant, l'attache au DOM, le met à jour, il est possible de lancer d'appeler automatiquement des fonctions appelées "hook".
- Pour définir un hook, il faut appeler les fonctions onXXXX() fournis par vuejs et leur fournir en paramètre la fonction hook.
- Bien entendu, il faut choisir la fonction onXXX() en fonction du moment où l'on veut que le hook soit appelé.
Exemple :
<script setup>
...
onMounted( () => console.log("mounted") )
onUpdated( () => console.log("updated") )
...
</script>
3°/ Exemple illustratif (avec spoiler sur les directives vuejs)
ATTENTION ! Cet exemple permet juste d'illustrer les différents points abordés précédemment, dans un but pédagogique. En pratique, on n'écrirait pas un tel composant de cette façon, mais plus simplement (par ex, pas besoin de watcher, ni de mounted() ).
Télécharger les sources : [ vuejs-td1-src.tgz ]
NB : cette archive contient uniquement le répertoire src. Il faut donc au préalable créer un projet basique avec vue-cli puis remplacer le répertoire src par celui contenu dans l'archive.
Dans model.js, on a :
var gamers = [ {name: 'jean', pseudo: 'jj'}, {name: 'pierre', pseudo: 'pepe'} ]
export {gamers}
Dans MyComponentV3.vue, on a :
<template>
<div>
<h1>{{ title }}</h1>
<p>Il y a actuellement {{ nbGamers }} joueurs </p>
<select v-model="idSelected">
<option v-for="(gamer,index) in gamersList" :key="index" :value="index">{{gamer.name}}</option>
</select>
<p v-if="currentGamer">pseudo joueur sélectionné : {{ currentGamer.pseudo }} </p>
<hr>
<input v-model="playerName"><input v-model="playerPseudo">
<button @click="addPlayer">Ajouter joueur</button>
</div>
</template>
<script setup>
import {gamers} from './model'
import {ref, computed, watch, onMounted} from "vue";
const props = defineProps({
title: String
})
// pour accéder aux props, on utilise l'objet renvoyer par defineProps
console.log(props.title)
const gamersList = ref(gamers)
const currentGamer = ref(null) // normalement, devrait être computed (cf. commentaire ci-dessous)
const idSelected = ref(-1)
// pour pouvoir ajouter des joueurs
const playerName = ref("")
const playerPseudo = ref("")
const nbGamers = computed(() => gamersList.value.length) // si gamersList change de taille, nbGamers sera automatiquement recaculé
/* NB : normalement, on a pas besoin d'utiliser setCurrentGamer+watcher : il suffit d'utiliser une fonction computed
qui "calcule" currentGamer en fonction de idSelected. Cela s'écrit :
const currentGamer = computed(() => { if( (idSelected.value >= 0) && (idSelected.value < nbGamers.value) ) return gamersList.value[idSelected.value]; return null })
*/
function setCurrentGamer(idx) {
if( (idx >= 0) && (idx < nbGamers.value) ) { // on accède à la variable locale calculée nbGamers
currentGamer.value = gamersList.value[idx]
}
}
watch( idSelected, (newVal) => {
console.log("nouveau joueur sélectionné : "+newVal)
setCurrentGamer(newVal);
})
function addPlayer() {
// NB: si on essaie d'ajouter à gamers au lieu de gamersList => pas de réactivité donc comportement "bizarre"
gamersList.value.push({
name: playerName.value,
pseudo: playerPseudo.value,
})
}
// "hook" appelé lorsque le composant estmonté dans le DOM
onMounted(() => {
console.log("MyComponentV3 est monté dans le DOM")
})
</script>
- les items de la liste déroulante sont créés dynamiquement en fonction des objets se trouvant dans le tableau local gamersList, dont le contenu est celui de la variable importée gamers.
- grâce à la directive v-for, on peut parcourir les élément de ce tableau, en les comptant, pour créer des balises "en boucle".
- dans le cas présent, le texte de chaque item est tiré de l'attribut name et la valeur correspondant à l'item sélectionné est l'indice dans le tableau.
- grâce à la directive v-model, on indique à vuejs que la sélection de l'utilisateur doit mettre à jour la variable idSelected, qui prendra donc comme valeur un indice dans le tableau gamersList.
- grâce à un watcher, on observe tout changement de la valeur de idSelected. Si c'est le cas, on appelle une méthode qui met à jour un objet nommé currentGamer, représentant le joueur sélectionné.
- comme indiqué en commentaire, ce watcher est normalement inutile : on peut très bien calculer la valeur de currentGamer en fonction de idSelected, via le mécanisme de variable computed.
- on remarque également l'utilisation d'une variable computed nbGamers pour contenir le nombre d'éléments dans gamersList. Si gamersList change, alors nbGamers sera réévaluée.
- la fonction addPlayer() est appelée via le clic sur le bouton ajouter. On remarque qu'elle utilise bien la fonction push() pour modifier gamersList. NB: si on modifiait gamers, il n'y aurait pas de réactivité et la page, donc la liste déroulante ne serait pas mise à jour automatiquement.
- onMounted() est utilisée pour afficher un message lors de l'intégration du composant dans le DOM.
- enfin, on remarque l'utilisation d'une directive v-if, qui permet d'inclure de manière conditionnelle le paragraphe <p>