version 1.1.0, dernière mise à jour le 9 février 2021.
Vue.js est un framework JavaScript permettant de facilement mettre en place des interfaces utilisateur. Il a été conçu dès l'origine pour pouvoir être adopté progressivement, notamment sans avoir à mettre en place d'infrastructure de dépendances. Il est cependant tout à fait possible de déployer des systèmes assez complexes. C'est cette progressivité et sa simplicité dans la mise en œuvre qui le rendent bien adapté à un cours d'initiation à un framework front. Cependant, Vue n'est pas compatible Internet Explorer 8
; il faut un navigateur compatible avec EcmaScript 5.
C'est la méthode la plus simple pour se lancer, car elle ne nécessite pas de passer par une quelconque installation à proprement parler. Il suffit d'importer le script:
<script src="https://unpkg.com/vue@next">
</script>
La documentation officielle précise d'utiliser cette méthode en phase de prototypage, et de lier vers une version stable en production. C'est cette méthode que nos allons utiliser pour le début de ce cours.
Pour des projets de plus grande envergure, il est plus simple de passer par npm
ou un autre gestionnaire de paquets : $ npm install vue@next
Le projet Vue.js met à disposition un programme en ligne de commande nommé Vue-CLI, qui permet de facilement mettre en place la structure et l'arborescence standard d'un projet Vue. Nous l'utiliserons plus tard dans le cours. Pour l'installer, il faut passer par un gestionnaire de paquets : npm install -g @vue/cli
pour une installation avec npm
par exemple.
Vue-devtools est un outil intégré aux outils de développement de Firefox et Chrome. L'extension apparaît sois la forme d'un onglet supplémentaire dans les outils de développement.
Certains sites de prototypage en ligne, comme CodeSandbox, permettent de créer rapidement de petits projets.
Vue s'organise autour d'un modèle de données, qui est ensuite traité et restitué sous la forme d'une… vue par le navigateur. À l'image de React, de la plupart des frameworks front, mais à la différence d'un autre framework comme Svelte par exemple, tout le traitement des données est ainsi réalisé par le navigateur.
Une fois que la bibliothèque est chargée, on a accès au constructeur Vue
, qui nous permet de créer l'application elle-même :
const appli = {} ;
Vue.createApp(appli) ;
Évidemment, telle quelle cette déclaration ne sert à rien, et il faut enrichir l'appli de données. C'est une des modifications introduites lors du passage de la versoin 2 à la version 3 de Vue :
const appli = { data() { return { message: "Ceci est un message" , nombre: 42 , tableau: [1, 2, 3] } } };
Si on se limite à cela, rien n'apparaîtra à l'écran, car il faut encore « accrocher » la vue à un élément du DOM. Par exemple, si on a créé un div
d'identifiant "monapp"
, on écrira
Vue.createApp(appli).mount("#monapp") ;
Nous n'avons jusqu'à présent fait que définir le modèle de données. Il faut maintenant l'afficher. On utilise une directive, c'est-à-dire un attribut à insérer dans l'élément HTML
visé. Par exemple, avec notre modèle précédent, si on veut afficher dans un paragraphe notre « message », on écrira…
<div id="monapp">
<p v-text="message">
</p>
</div>
On peut aussi utiliser la syntaxe dite « moustache » maintenant largement répandue :
<div id="monapp">
<p>{{message}}</p>
</div>
Cependant, si on souhaite ajouter du code HTML
, par exemple si on a initialisé message
avec la chaîne de caractères "<span>Message</span>"
, les balises ne seront pas interpétées et on aura à l'écran <span>Message</span>
. Pour éviter cela, on peut recourir à la directive v-html
. Attention cependant, c'est un risque potentiel de sécurité, donc à manier avec parcimonie…
<div id="monapp">
<p v-html="message">
</p>
</div>
A contrario, si l'on ne souhaite pas que soient calculées des expressions (par exemple quand on veut sciemment écrire des doubles accolades), et que donc le texte ne soit pas interprété, il faut utiliser v-pre
.
Il n'est pas possible de charger une donnée directement dans un attribut HTML
, par exemple en écrivant title="{{message}}"
. Il faut lier la valeur de l'attribut au modèle, à l'aide de la directive v-bind
:
<p v-bind:style="'font-weight :'+graisseTexte">Blabla</p>
<img v-bind:src="urlImage" v-bind:alt="altImage">
On peut parcourir un tableau, ou un objet quelconque, à l'aide d'une boucle v-for
. Il y a deux syntaxes possibles. Supposons un tableau tab
défini dans le modèle…
<ul><li v-for="item in tab">{{item}}</li></ul>
permet de parcourir le tableau, et de créer une liste à puces avez autant de puces que d'éléments dans le tableau
<ul><li v-for="(item, index) in tab">{{item}} numéro {{index}}</li></ul>
permet aussi de parcourir le tableau, et de créer une liste à puces avez autant de puces que d'éléments dans le tableau, mais aussi de garder une trace de l'index de l'élément courant du tableau
L'ordre d'affichage par défaut est basé sur l'ordre d'énumération de Object.keys()
et par conséquent, selon la documentation officielle, il n'y a pas garantie de cohérence de cet ordre d'affichage en fonction du moteur de rendu JavaScript utilisé. Si l'on souhaite modifier l'ordre d'affichage du tableau, il y a deux possibilités :
soit en utilisant un mutateur, c'est-à-dire une méthode permettant de modifier un tableau, comme sort()
, reverse()
…
soit en utilisant un filtre à partir d'une propriété calculée (voir plus loin)
La directive v-show
, qui accepte un booléen, permet de conditionner l'affichage d'un contenu : le contenu est affiché si le booléen vaut true
, caché sinon.
La directive v-if
permet de conditionner, un peu comme v-show
, l'affichage d'un élément à la réalisation d'un test. Il existe cependant une différence entre les deux : v-show
cache l'élément via CSS
, tandis que v-if
ne l'insère pas dans le DOM. Cela rend le masquage et l'affichage plus rapides par v-show
sous réserve qu'aucun calcul ou manipulation de données ne soit faits dans l'élément. v-if
est plus lent pour basculer d'un affichage à un masquage, mais en contrepartie aucun calcul superflu n'est réalisé.
Il existe également les directives v-else
et v-else-if
dont les noms sont explicites et qui viennent compléter v-if
.
L'affectation d'un gestionnaire d'événements est facilitée par la directive v-on
:
<button v-on:click="gestionnaire">Cliquez-moi!</button>
Dans ces conditions, comme l'on passe par Vue pour gérer l'événement (ce qui n'est pas obligatoire, Vue cohabitant sans problème avec le JavaScript « Vanilla »), il faut que le gestionnaire soit défini lors de la déclaration de l'application, en tant que méthode :
const appli = { data() { return { (…) } }, methods(){ gestionnaire(){ (…) } } };
Pour accéder à une donnée du modèle, il faut recourir à this
:
const appli = { data() { return { message: "Bonjour" } }, methods(){ gestionnaire(){ this.message="Salut" } } };
Un raccourci pour la directive est le caractère @
; on écrira ainsi <button @click="gestionnaire">Cliquez-moi!</button>
On doit parfois recourir à des méthodes en JavaScript
pour, par exemple, empêcher le comportement par défaut lors d'un événement (avec preventDefault()
) ou bloquer la propagation d'un événement dans le DOM (avec stopPropagation()
). Vue permet très facilement, via des modificateurs précisés lors de l'appel du gestionnaire. Ces modificateurs sont :
.stop
pour stopper la propagation de l'événement (analogue de stopPropagation()
)
.prevent
pour empêcher le traitement par défaut de l'événement (analogue de preventDefault()
)
.capture
pour préciser que l'on souhaite traiter l'événement dans sa phase de capture
.self
pour indiquer que la cible de l'événement est l'élément lui-même
On écrira ainsi, par exemple, <a href="#" @click.prevent="gestionnaire">
.
Un autre raccourci offert par Vue est une simplification de la gestion des touches clavier. Avec le JavaScript standard, cette gestion est complexe. Vue demande juste de spécifier le code de la touche quand on spécifie l'événement, par exemple <button @keydown.13="submit">OK</button>
. Cerise sur la gâteau, on dispose aussi de raccourcis pour quelques touches, sans qu'il soit ainsi nécessaire de retenir leur code : enter
, tab
, delete
, esc
, space
, up
, down
, left
et right
. On pourra écrire par exemple <div @keydown.left="bougeGauche" @keydown.right="bougeDroite" @keydown.up="bougeHaut" @keydown.down="bougeBas">
.
La directive v-model
crée une « double liaison » entre le modèle et un champ de formulaire : un changement dans le modèle est répercuté dans le formulaire, mais l'inverse est aussi vrai. Cela donne la possibilité de modifier dynamiquement le modèle via un champ de saisie. Le fonctionnement en est très simple ; si une donnée du modèle est message
, initialisée à "Début"
, alors si on écrit <label for="chpmsg">Message à saisir</label><input v-model="message" id="chpmsg">
, au chargement de la page le champ sera pré-rempli avec la chaîne de caractères "Début", et dès que l'on aura commencé à saisir un nouveau texte dans le champ, le modèle sera mis à jour.
Dans le cas de boutons radio portant le même attribut name
et donc s'excluant mutuellement (quand on en coche un, le second est décoché), c'est la valeur du bouton radio sélectionné qui sera stockée dans le modèle, par exemple…
<label for="btn_haut">Haut</label>
<input type="radio" id="btn_haut" name="sens" value="haut" v-model="direction">
<label for="btn_bas">Bas</label>
<input type="radio" id="btn_bas" name="sens" value="bas" v-model="direction">
const appli = { data() { direction: "haut" } };
Une case à cocher est associée à un booléen, valant true
si la case est cochée, false
sinon :
<label for="case">Case à cocher</label>
<input type="checkbox" id="case" v-model="ok">
const appli = { data() { return { ok: false } } };
Dans le cas d'un menu de sélection avec choix unique, la situation est très simple :
<label for="menu">Faites un choix…</label>
<select id="menu" v-model="choix">
<option>Choix 1</option>
<option>Choix 2</option>
<option>Choix 3</option>
<option>Choix 4</option>
</select>
const appli = { data() { return { choix: "Choix 1" } } };
Dans le cas d'une sélection multiple, le champ de formulaire doit être couplé avec une donnée tabulaire…
<label for="menu">Faites un choix…</label>
<select id="menu" v-model="choix" multiple>
<option>Choix 1</option>
<option>Choix 2</option>
<option>Choix 3</option>
<option>Choix 4</option>
</select>
const appli = { data() { return { choix: ["Choix 1", "Choix 2"] } } };
On peut faire des calculs à partir des données du modèle au moment de restituer la vue, mais cela a tendance à alourdir la syntaxe. Par exemple…
const appli = { data() { return { nom: "Deblouze", prenom: "Agathe" } } };
Si l'on souhaite afficher le nom complet dans la vue, on est obligé d'écrire <p>Nom complet : {{prenom}} {{nom}}</p>
. Évidemment, ici le cas est très simple mais on peut imaginer des cas plus complexes, comme le calcul du montant d'un bon de commande par exemple. On gagnerait en visibilité si, sans toucher au modèle de données, on avait à disposition une propriété calculée à partir des données, et qu'on appellerait {{nomComplet}}
: <p>Nom complet : {{nomComplet}</p>
. Cela est possible. On ajoute à la définition de l'application une propriété, computed
, qui est un objet contenant des méthodes retournant les valeurs choisies :
const appli = { data() { return { nom: "Deblouze", prenom: "Agathe" } }, computed:{ nomComplet(){ return this.prenom+' '+this.nom; } } };
Le cas précédent est assez limité en fonctionnalité, puisque l'on ne peut pas associer via un v-model
une propriété calculée à un champ de saisie, par exemple, pour pouvoir la modifier en répercutant ces modifications sur les données du modèle. Pour pallier cela, Vue donne la possibilité de définir des getters et des setters (respectivement accesseurs et mutateurs). Cela donne le code suivant :
<div id="monapp">
<label for="chpprenom">Prénom</label>
<input id="chpprenom" v-model="prenom">
<br>
<label for="chpnom">Nom</label>
<input id="chpnom" v-model="nom">
<br>
<label for="chpcomplet">Prénom</label>
<input id="chpcomplet" v-model="nomComplet">
</div>
const appli = { data() { return { nom: "Deblouze", prenom: "Agathe" } }, computed:{ nomComplet : { get () { return this.prenom+' '+this.nom; }, set (prenom_nom) { let nom = prenom_nom.split(" "); this.prenom = nom[0]; this.nom = nom[1]; } } } };
Les composants permettent d'organiser plus rationnellement une application en la « découpant » en modules pouvant échanger des informations et pouvant être paramétrés. Les composants échangent des données via des « props
».
Jusqu'à présent, nous créions une nouvelle application et l'associions dans la foulée à l'aide d'une ligne du genre Vue.createApp(appli).mount("#monapp");
. Cependant, cette étape peut en fait être scindée. La méthode createApp
renvoie en effet une référence à un objet application, que l'on peut réutiliser pour définir des composants :
const app = Vue.createApp(appli) ;
app.component(…) ;
app.mount("#monapp") ;
La méthode component
prend deux arguments : une chaîne de caractères donnant le nom du composant et sa déclaration elle-même, par exemple…
app.component("parag", { template: "<p>Paragraphe ajouté</p>" });
Il suffit alors dans la vue, dans l'élément d'identifiant monapp
d'ajouter le code <parag>
pour qu'un nouveau paragraphe soit créé.
Évidemment, un tel composant ne sert pas à grand'chose puisqu'il renvoie toujours le même contenu. On peut lui passer des données afin qu'il soit paramétrable. L'appel à ces données se fait à l'aide de l'attribut v-bind
:
<div id="monapp">
<parag v-bind:texte="à afficher">
</parag>
</div>
app.component("parag", { props: ["texte"], template: "<p>Paragraphe ajouté, avec un message {{texte}}</p>" });
L'exemple précédent crée un paragraphe contenant « Paragraphe ajouté, avec un message à afficher ».
Nous avons jusqu'à présent vu des exemples limités à une seule page, et ne faisant pas appel à beaucoup de composants. Cependant, dans le cas de systèmes plus complexes (pages ou gabarits multiples, nombreux composants…), il est nécessaire de plus structurer le projet. Pour cela, il existe un outil assez pratique, Vue-CLI (pour Command Line Interface). Dans ce cas cependant, tout ne se fait plus côté client, et il faut mettre en place un serveur Node. Heureusement, Vue-CLI fait tout le travail…
L'installation se fait grâce à votre gestionnaire de paquets préféré, ici npm
: npm install --global @vue/cli
dans une ligne de commande avec les droits administrateur.
Pour créer un nouveau projet Vue, il suffit de taper ensuite (il n'est plus nécessaire d'avoir les droits administrateur) : vue create monprojet
. Cela aura pour effet dans un premier temps de créer le répertoire monprojet
. À l'heure où ce cours est écrit, la version en cours est la 4.5.11. Au lancement du script, on est accueilli par quelques questions (et un peu de coloration syntaxique qui n'est pas reproduite ici) :
Vue CLI v4.5.11 ? Please pick a preset: (Use arrow keys) ❯ Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) Manually select features
Comme nous étudions Vue3, c'est la deuxième option qu'il faut choisir. Vous constaterez qu'un certain nombre d'outils sont installés, comme ESLint ou Babel, et surtout TypeScript. Surtout, l'installation met automatiquement en place une surveillance dynamique des fichiers du projet ; lancer npm run serve
permet de facilement les mettre à jour sans avoir à relancer le serveur, dans un environnement de développement. Par défaut sur le port 8080 du localhost, vous trouverez une page d'accueil avec des liens de documentation :
Si le port est déjà pris, son numéro est automatiquement incrémenté. L'arborescence automatiquement créée par défaut est la suivante :
. ├── babel.config.js ├── node_modules │ └── (…) ├── package.json ├── package-lock.json ├── public │ ├── favicon.ico │ └── index.html ├── README.md └── src ├── App.vue ├── assets ├── components │ └── HelloWorld.vue └── main.js
Le répertoire src
va contenir toute l'application: les vues (qui sont en fait les pages-types), la définition des composants génériques appelés par les vues, et les fichiers de traitement.
Le fichier index.html est un simple conteneur vide.
Le fichier main.js est très simple :
import { createApp } from 'vue' ;
import App from './App.vue' ;
createApp(App).mount('#app') ;
Ce fichier importe la vue « App » et monte l'appli dans le conteneur indiqué dans index.html.
Au CSS près, le fichier App.vue est un peu plus complexe…
<template> <img alt="Vue logo" src="./assets/logo.png"> <HelloWorld msg="Welcome to Your Vue.js App"/> </template> <script> import HelloWorld from './components/HelloWorld.vue' export default { name: 'App', components: { HelloWorld } } </script>
La première partie définit un composant, assez simple, qui appelle en fait le composant HelloWorld
avec un seul paramètre. Il faut donc importer ce composant, ce qui est le travail de la ligne import HelloWorld from './components/HelloWorld.vue'
. Mais le fichier main.src appelle App
dans le fichier App.vue. Il faut donc nommer le composant défini plus haut sous ce nom (name: 'App'
), et si l'on veut que le composant Helloword soit reconnu lors de son interprétation, il faut aussi l'exporter. Enfin, export default {…}
permet d'indiquer que le composant (≤ élément » template) défini plus haut est le composant par défaut à exporter en tant qu'« App ».
Le fichier HelloWorld.vue est le plus long en apparence, mais une bonne partie est du HTML statique. Au final, il se résume à……
<template> <div class="hello"> <h1>{{ msg }}</h1> <!-- le HTML statique de la page --> </template> <script> export default { name: 'HelloWorld', props: { msg: String } } </script>
On retrouve là encore la définition d'un composant, mais cette fois-ci avec un paramètre, et l'export de ce composant sous le nom HelloWorld. TypeScript est ici utilisé pour définir le type de données que doit avoir msg
. Pour rappel, les principaux types de données de JavaScript
sont String, Date, Object, Number, Boolean, Function, Array, RegExp. et Error. À noter en passant dans le code CSS
de la page l'attribut scoped
, qui a été retiré de la spécification CSS mais est ici interprété par Vue pour indiquer que le style défini ne doit être utilisé que sur le composant du fichier, et ne peut ainsi « contaminer » les autres composants.
Jusqu'à présent, nous n'avons écrit que des « Single Page Applications, des SPA mais dans le cas d'un site complexe, plusieurs points d'entrée peuvent être nécessaire. Vue permet de facilement mettre en place un routage, même s'il est bien sûr possible d'utiliser un framework comme Express pour cela.
L'installation se fait soit en liant vers une version en ligne à l'adresse https://unpkg.com/vue-router/dist/vue-router.js
, soit sur le serveur local avec npm install vue-router
, soit encore si on utilise Vue-CLI
en tapant vue add router
. Dans ce cas, le fichier par défaut App.vue est remplacé en ajoutant deux liens vers Home et About en haut de page, et l'arborescence nécessaire est automatiquement mise en place.
Dans le fichier main.js
, l'import du module router
est ajouté :import router from './router'
, et il est spécifié que ce routeur doit être utilisé lors de la création de l'application : createApp(App).use(router).mount('#app')
.
Le module ajoute deux directives : <router-view />
qui indique où le composant gérant la vue de la page en cours de consultation doit être affiché, et <router-link />
qui permet de mettre en place des liens de navigation. Le code d'App.vue
est ainsi mis à jour :
<template> <div id="nav"> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> </div> <router-view/> </template>
L'URL racine renvoie alors vers le composant Home.vue
dans le répertoire views
créé lors de l'installation, et l'URL /about
renvoie vers le composant About.vue
.
La définition des routes se fait elle-même dans le fichier src/router/index.js
, qui contient par défaut les routes suivantes :
const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') } ]
Si on souhaite ajouter une nouvelle route, il suffit de créer par exemple le composant correspondant dans un fichier views/page1.vue
, et d'indiquer la nouvelle route dans src/router/index.js
:
const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') }, { path: '/page1', name: 'Page1', component: () => import('../views/page1.vue') } ]
On peut aussi souhaiter charger des URL à paramètre, par exemple pouvoir réagir à (…)/adresse/valeur1
ou (…)/adresse/valeur2
et pouvoir utiliser valeur1
ou valeur2
, voire n'importe quelle autre valeur. Cela se fait en trois temps :
il faut créer d'abord la vue correspondante (par exemple ici src/views/adresse.vue
). Les paramètres de l'URL sont exposés dans la collection $route.params
. Par exemple, ici, si on veut afficher dans le composant le paramètre valeur
, on écrira $route.params.valeur
il ne faut ensuite par oublier, dans src/router/index.js
, d'importer le composant présent dans la vue. Si on l'a appelé adresse
, on écrira donc import adresse from ('../views/adresse.vue);
enfin, il faut définir une nouvelle route, en mentionnant le paramètre en le préfixant par « :
», par exemple {path: '/adresse/:valeur', component: adresse}
Si l'on se contente de faire la liste des adresses autorisées, l'application est incomplète car dans ce cas si l'on essaie d'accéder à une axdresse non répertoriée, aucun affichage spécifique ne se déclenche. Dans ce cas, il faut avoir défini un composant pour cela, et l'appeler. Si on a défini un composant pageAbsente
, on écrira à la fin des routes
{ path: "/:catchAll(.*)", component: pageAbsente, },
Cette création est mise à disposition par Gilles Chagnon sous un contrat Creative Commons.