Le langage Vue.js :
Ce cours est un mélange entre mes connaissances en Vue.js et la formation de Grafitkart sur Vue.js.
Ce cours est un mélange entre mes connaissances en Vue.js et la formation de Grafitkart sur Vue.js.
Vue.js est un framework JavaScript progressif utilisé pour construire des interfaces utilisateur. Il se distingue par sa facilité d'utilisation, sa flexibilité et sa performance. Vue.js est conçu comme une amélioration de l'HTML permettant de créer des interfaces réactives plus simplement à l'aide d'attributs spéciaux.
Dans ce cours, nous allons commencer par découvrir les bases du framework (la syntaxe, la réactivité, ...). Puis, après quelques exercices, on étudiera plus en profondeur le fonctionnement interne de Vue.js pour comprendre ses subtilités et découvrir des cas d'utilisations plus spécifiques.
Enfin, on finira en étudiant l'écosystème autour de Vue.js pour découvrir les librairies intéressantes.
Quand on crée une interface complexe, il est souvent difficile de maintenir l'interface (le DOM) synchronisé avec l'état système.
Vue.js utilise un système de template qui va permettre de décrire la structure HTML attendue. Cette structure se mettra à jour automatiquement lorsque l'état change.
Il existe aujourd'hui plusieurs framework et on peut se demander pourquoi utiliser Vue.js plutôt qu'un autre framework.
Simple à prendre en main, son approche pensée comme de l'HTML améliorée permet une prise en main rapide.
Gestion des transitions, Vue.js intègre un système de transition qui permet de gérer facilement l'appartition et la disparition d'élément.
Un écosystème conséquent permet de trouver de nombreuses librairies pour accélérer le développement.
De bonnes performances obtenues grâce à l'utilisation d'un compiler.
Dans cette partie, nous allons voir comment initialiser un projet en utilisant le framework Vue.js. Pour commencer à travailler, plusieurs solutions s'offrent à vous :
En ligne à l'aide du playground Vue.js.
En ligne via StackBlitz.
En créant un projet en local à l'aide du kit de démarrage offert par vue (npm create vue@latest).
Après avoir ouvert le dossier contenant votre projet dans VSCode, on doit installer les dépendances avec la commande :
npm install
Dans VSCode, on peut installer l'extension "Vue - Officiel" pour avoir la coloration syntaxique de Vue.js.
Pour démarrer le projet, on fait :
npm run dev
On doit pas relancer la commande précédente à chaque fois qu'on modifie un fichier dans notre projet.
Dans un fichier Vue, dont l'extension est ".vue", on a une balise <template> contenant le contenu HTML de notre composant.
Dans ce fichier, on a également une balise <script> avec l'attribut setup contenant le JS de notre composant.
Enfin, dans ce même fichier, on a une balise <style> contenant le style de notre composant. On peut lui rajouter l'attribut scoped pour que ce style soit uniquement pour ce composant.
Par exemple, le fichier `App.vue` :
<template>
<h1>Bonjour {{ firstName }}</h1>
<p>Comment allez-vous ?</p>
</template>
<script setup>
const firstName = "Driss";
</script>
<style>
h1 {
color: red;
}
</style>
Dans cette partie, nous allons découvrir la syntaxe permettant de décrire la structure HTML générée par Vue.js. Pour plus de détails sur les éléments de syntaxes détaillés ci-dessous, vous pouvez utiliser la documentation.
Ensuite, nous allons voir comment créer des variables réactives à l'aide de la fonction ref().
N.B : la fonction v-hide n'est pas accessible nativement, elle nécessite l'installation d'un package supplémentaire (@ventralnet/v-hide).
Pour mieux comprendre la syntaxe, on va faire l'exemple du compteur :
<template>
<p :id="`p-${count}`" :class="{active: count > 5}">Compteur : {{ count }}</p>
<div v-show="count >= 5">Bravo, vous avez cliqué plus de 5 fois !</div>
<button @click="increment">Incrémenter</button>
<button :style="{color: 'red'}" @click="decrement">Décrémenter</button>
<hr>
<button @click="sortMovies">Réorganiser</button>
<form action="" @submit.prevent="addMovie">
<label for="newMovie">Nouveau film</label>
<input type="text" id="newMovie" v-model="movieName">
<button>Ajouter</button>
{{ movieName }}
</form>
<button @click.prevent="randomAge">Change âge</button>
<ul>
<li v-for="movie in movies" :key="movie">
{{ movie }} <button @click="deleteMovie(movie)">Supprimer</button>
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
const movies = ref([
'Matrix',
'Lilo & Stitch',
'Titanic'
]);
const movieName = '';
const person = ref({
firstname: 'John',
lastname: 'Doe',
age; 20
});
const increment = (event) => {
count.value++;
};
const decrement = () => {
count.value--;
};
const deleteMovie = (movie) => {
movies.value = movies.value.filter(m => m !== movie);
};
const sortMovies = () => {
movies.value.sort((a, b) => a > b ? 1 : -1);
};
const addMovie = () => {
movies.value.push(movieName.value);
movieName.value = '';
};
const randomAge = () => {
person.value.age = Math.round(Math.random() * 100);
};
</script>
<style>
.active {
color: red;
}
</style>
v-if supprime l'élément du DOM par rapport à v-show (inverse du v-hide).
Afin d'assoir les notions vues jusqu'à maintenant, je vous propose un petit TP (Travaux Pratiques) qui permettra de les pratiquer.
Pour ce premier TP, votre objectif est de créer un petit système de tâches à faire.
On affiche un message s'il n'y a pas de tâches à faire.
Un champ texte accompagné d'un bouton "Ajouter" sera présent au-dessus de la liste et permettra d'ajouter une nouvelle tâche.
Pour chaque tâche, une case à cocher permettra de maruqer la tâche comme faite.
Une tâche terminée sera barrée (à l'aide de CSS).
Les tâches à faire seront toujours affichées en premier.
Une case, en bas de liste, permettra de masquer les tâches terminées.
Les tâches respecteront le format suivant :
[
{ "title": "Acheter la propriété 'Rue de la Paix'", "completed": false, "date": 20240730 },
{ "title": "Construire un hôtel sur 'Avenue Foch'", "completed": false, "date": 20240730 },
{ "title": "Éviter la case prison", "completed": false, "date": 20240730 }
]
Voici la correction qui se trouve dans le fichier `App.vue` :
<template>
<form action="" @submit.prevent="addTodo">
<fieldset role="group">
<input type="text" placeholder="Tâche à effectuer" v-model="newTodo">
<button :disabled="newTodo.length === 0">Ajouter</button>
</fieldset>
</form>
<p v-if="todo.length === 0">Vous n'avez pas de tâches à faire !</p>
<div v-else>
<ul>
<li v-for="todo in sortedTodos()" :key="todo.key" :class="{completed: todo.completed}">
<label>
<input type="checkbox" v-model="todo.completed">
{{ todo.title }}
</label>
</li>
</ul>
<label>
<input type="checkbox" v-model="hideCompleted">
Masquer les tâches complétées
</label>
</div>
</template>
<script setup>
import { ref } from 'vue';
const newTodo = ref('');
const hideCompleted = ref(false);
const todos = ref([]);
const addTodo = () => {
todos.value.push({
title: newTodo.value,
completed: false,
date: Date.now()
});
newTodo.value = '';
};
const sortedTodos = () => {
const sortedTodos = todos.value.toSorted((a, b) => a.completed > b.completed ? 1 : -1);
if (hideCompleted.value) {
return sortedTodos.filter(t => !t.completed);
}
return sortedTodos;
};
</script>
<style>
.completed {
opacity: .5;
text-decoration: line-throught;
}
</style>
Précédemment, on a vu le concept de ref() qui sont des variables réactives qui peuvent être observée par Vue.js pour synchroniser la structure du DOM. On aura parfois besoin de dériver une valeur à partir d'une ref. On pourra dans ce cas-là utiliser la méthode computed() qui permet de générer une nouvelle valeur réactive qui évoluera quand la variable interne utilisée change.
import { ref, computed } from 'vue';
const count = ref(0);
const double = computed(() => count.value * 2);
Dans ce cas-là, la variable double peut être utilisée dans la partie <template> et se mettra à jour dès que la valeur de count changera.
Dans l'exemple de notre Todolist, on peut utiliser la méthode computed() pour la constante sortedTodos comme ceci :
const sortedTodos = computed(() => {
const sortedTodos = todos.value.toSorted((a, b) => a.completed > b.completed ? 1 : -1);
if (hideCompleted.value) {
return sortedTodos.filter(t => !t.completed);
}
return sortedTodos;
});
Dans cet exemple, on peut rajouter la propriété calculée remainingTodos qui retourne le nombre de tâches encore à faire et l'afficher dans le template :
const remainingTodos = computed(() => {
return todos.value.filter(t => !t.completed).length;
});
<p v-if="remainingTodos > 0">
{{ remainingTodos }} tâche{{ remainingTodos > 's' ? '' }} à faire
</p>
Jusqu'à maintenant, nous avons écrit l'ensemble de la logique de notre application dans un seul et unique fichier `App.vue`. Mais, ce qui va être particulièrement intéressant avec Vue.js c'est sa capacité à créer des composants qui vont avoir leur propre logique et que l'on va pouvoir réutiliser.
Pour plus d'informations, vous pouvez consulter la documentation sur les composants.
On peut séparer notre application sur la Todolist en plusieurs fichiers, dont le fichier `Checkbox.vue` qui pourrait, comme son nom l'indique, afficher une case à cocher :
<template>
<label>
<input type="checkbox" v-model="model">
{{ label }}
</label>
</template>
<script setup>
const model = defineModel();
defineProps({
label: String
});
const emits = defineEmits(['check', 'uncheck']);
const onChange = (event) => {
if (event.currentTarget.checked) {
emits('check', event.currentTarget);
} else {
emits('uncheck', event.currentTarget);
}
}
</script>
Et ainsi, dans notre vue, on va ajouter ce nouveau composant en modifiant notre code au bon endroit :
<template>
<Checkbox :label="todo.title" v-model="todo.completed"/>
</template>
<script setup>
import Checkbox from './Checkbox.vue';
</script>
Pour parler d'héritage, c'est-à-dire pour passer des propriéts d'un composant parent à un ou des composants enfants, on va créer un nouveau composant dans le fichier `Button.vue` :
<template>
<button class="secondary"><slot></slot></button>
</template>
La balise <slot> permet de récupérer le contenu entre la balise ouvrante et la balise fermante de notre composant comme ceci :
<template>
<Button>
<strong>Demo</strong> de bouton
</Button>
</template>
<script setup>
import Button from './Button.vue';
</script>
On peut avoir plusieurs balises <slot> dans un composant. Pour illustrer ceci, on va créer un composant `Layout.vue` qui crée la structure de notre page de manière générale :
<template>
<div class="layout">
<header v-if="$slots.header">
<slot name="header"></slot>
</header>
<aside>
<slot name="aside"></slot>
</aside>
<main>
<slot name="main"></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<style>
.layout {
display: grid;
grid-template-columns: 200px 1fr;
}
.layout > * {
border: 1px solid red;
}
.layout header,
.layout footer {
grid-column: 1 / -1;
}
</style>
Dans notre fichier par défaut, `App.vue`, on va importer notre composant comme ceci :
<template>
<Layout>
<template #header>
En-tête
</template>
<template #aside>
Sidebar
</template>
<template #main>
Main
</template>
<template #footer>
Footer
</template>
</Layout>
</template>
Dans cette partie, nous allons découvrir le cycle de vie des composants et comment on peut utiliser les fonctions associées pour venir rajouter de la logique à certains moments clefs.
Toujours dans notre exemple de Todolist, on va utiliser des méthodes du cycle de vie comme onMounted() et onUnmounted() :
const todos = ref();
onMounted(() => {
fetch('https://jsonplaceholder.typicode.com/todos')
.then(r => r.json())
.then(v => todos.value = v.map(todo => ({ ...todo, date: todo.id })));
});
Les watchers permettent d'observer manuellement une valeur réactive pour déclencher un comportement spécial lors du changement.
const count = ref(0);
watch(count, (newValue, oldValue) => {
// Sera appelé lorsque la valeur de count change
});
Il est aussi possible d'utiliser la fonction watchEffect() qui sera capable d'observer automatiquement ses dépendances pour se redéclencher automatiquement lorsqu'une valeur change.
const count = ref(0);
watchEffect(() => {
console.log(count.value); // Détecte que l'on dépend de count
});
La fonction watch() contient un troisième paramètre qui est facultatif qui permet de dire si c'est immédiat ou pas (par défaut, c'est à false) :
<template>
<input type="text" v-model="name">
</template>
<script setup>
import { ref, watch } from 'vue';
const name = ref('');
watch(name, (newValue, oldValue) => {
document.title = newValue;
}, { immediate: true });
</script>
Comme cette partie est assez courte, on va parler de compositions avec l'exemple d'une fonction useTimer() qui va se trouver dans le fichier `src/composable/useTimer.js` :
import { onMounted, onUnmounted, ref } from 'vue';
export function useTimer (initial = 0) {
const time = ref(initial);
let timer;
onMounted(() => {
timer = setInterval(() => {
time.value++;
}, 1_000);
});
onMounted(() => {
clearInterval(timer);
});
return {
time,
reset() {
clearInterval(timer);
timer = setInterval(() => {
time.value++;
}, 1_000);
time.value = 0;
}
};
};
<template>
<input type="text" v-model="page.title">
Temps écoulé : {{ time }}
<button @click="reset">Reset</button>
</template>
<script setup>
import { ref, watch, watchEffect } from 'vue';
import { useTimer } from './composable/useTimer.js';
const { time, reset } = useTimer();
const page = ref({
title: ''
});
watchEffect(() => {
document.title = page.value.title;
});
</script>
Maintenant que l'on a vu pas mal de nouvelles notions, je vous propose de les mettre en pratique à travers la création d'un système de Quiz. Voici un exemple de JSON pour vous exercer :
{
"title": "Questionnaire sur les Films et Séries",
"minimum_score": 4,
"success_message": "Félicitations! Vous êtes un véritable cinéphile!",
"failure_message": "Dommage! Vous devriez regarder plus de films et séries.",
"questions": [
{
"question": "Dans quel film trouve-t-on le personnage de Jack Dawson?",
"choices": ["Titanic", "Le Seigneur des Anneaux", "Inception", "Avatar"],
"correct_answer": "Titanic"
},
{
"question": "Quelle série met en scène le personnage de Walter White?",
"choices": [
"Breaking Bad",
"Stranger Things",
"Game of Thrones",
"The Walking Dead"
],
"correct_answer": "Breaking Bad"
},
{
"question": "Dans quel film Harry Potter rencontre-t-il pour la première fois Lord Voldemort?",
"choices": [
"Harry Potter à l'école des sorciers",
"Harry Potter et la Chambre des secrets",
"Harry Potter et le Prisonnier d'Azkaban",
"Harry Potter et la Coupe de feu"
],
"correct_answer": "Harry Potter à l'école des sorciers"
},
{
"question": "Quel est le nom de l'intelligence artificielle dans '2001: L'Odyssée de l'espace'?",
"choices": ["HAL 9000", "Siri", "Cortana", "Jarvis"],
"correct_answer": "HAL 9000"
},
{
"question": "Qui joue le rôle principal dans la série 'Sherlock' de la BBC?",
"choices": [
"Benedict Cumberbatch",
"David Tennant",
"Matt Smith",
"Tom Hiddleston"
],
"correct_answer": "Benedict Cumberbatch"
}
]
}
Quand on va commencer à créer une application avec Vue.js, la première chose que vous devez faire c'est d'essayer de commencer à décomposer et réfléchir aux composants que vous aurez besoin.
Pour l'exercice du Quiz, en plus de l'application dans `App.vue`, j'aurais mis les composants Quiz, Progress, Question et Radio, mais ça peut changer au fil du développement.
Le code JSON ci-dessus va être dans le fichier `quiz.json` dans le dossier public de notre projet.
Pour pas que ma page ait un style dégeulasse, je vais utiliser Pico mais vous pouvez utiliser n'importe quel framework CSS et je rajoute dans mon fichier `index.html` :
<link rel="stylesheet" href="https://cdn.jsdeliver.net/npm/@picocss/pico@2/css/pico.pumpkin.min.css">
Dans mon application, la première étape est de charger les données du fichier JSON avec la méthode onMounted() avec un fetch :
<template>
<div class="container">
<div v-if="state === 'error'">
Impossible de charger le quiz
</div>
<div :aria-busy="state === 'loading'">
{{ quizz }}
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
const quiz = ref(null);
const state = ref('loading');
onMounted(() => {
fetch('/quiz.json')
.then(r => {
if (r.ok) {
return r.json();
}
throw new Error('Impossible de récupérer le json');
})
.then(data => {
quiz.value = data;
state.value = 'idle';
})
.catch(e => {
state.value = 'error';
});
});
</script>
<style>
.container {
margin-top: 2rem;
}
</style>
C'est parti pour créer notre premier composant : `components/Quiz.vue` :
<template>
<div>
<h1>{{ quiz.title }}</h1>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
quiz: Object
});
const step = ref(0);
</script>
Dans notre App, on oublie pas de l'importer et de modifier le code en conséquence :
<Quiz :quiz="quiz" v-if="quiz"/>
import Quiz from './components/Quiz.vue';
Ensuite, comme on a besoin d'une barre de progression, on va créer le composant Progress :
<template>
<div>
Étape {{ value + 1 }}/{{ max + 1 }}
<progress :value="value" :max="max">
</div>
</template>
<script setup>
defineProps({
value: Number,
max: Number
});
</script>
Également, on oublie pas de l'importer dans notre composant Quiz :
<Progress :value="step" :max="quiz.questions.length - 1"/>
import Progress from './Progress.vue';
Ensuite, on va créer le composant `Question.vue` et on n'oublie pas de l'importer dans notre `Quiz.vue` :
<template>
<div class="question">
<h3>{{ question.question }}</h3>
<ul>
<li v-for="(choice, index) in question.choices" :key="choice">
<label :for="`answer${index}`">
<input type="radio" name="answer" :id="`answer${index}`" :value="choice" v-model="answer" :disabled="hasAnswer">
{{ choice }}
</label>
</li>
</ul>
<button :disabled="!hasAnswer" @click="emits('answer', answer)">Question suivante</button>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
const props = defineProps({
question: Object
});
const emits = defineEmits(['answer']);
const answer = ref(null);
const hasAnswer = computed(() => answer.value !== null);
</script>
<style>
.question {
padding-top: 2rem;
}
.question button {
margin-left: auto;
display: block;
}
</style>
<Question :key="question.question" :question="question" v-if="state === 'question'" @answer="addAnswer"/>
import Question from './Question.vue';
const question = computed(() => props.quiz.questions[step.value]);
const state = ref('question');
const answers = ref(props.quiz.questions.map(() => null));
const addAnswer = (answer) => {
answers.value[choice.value] = answer;
if (step.value === props.quiz.questions.length - 1) {
state.value = 'recap';
} else {
step.value++;
}
};
On va créer un nouveau composant Recap.vue qui va afficher le score final quand la constante state de Quiz.vue a la valeur recap :
<template>
<h1>Recap</h1>
<p>{{ hasWon ? quiz.success_message : quiz.failure_message }}</p>
<p>Score : {{ score }}/{{ quiz.questions.length }}</p>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
quiz: Object,
answers: Array
});
const score = computed(() => {
return props.quiz.questions.reduce ((acc, question, k) => {
if (question.correct_answer === props.answers[k]) {
return acc + 1;
}
return acc;
}, 0);
});
const hasWon = computed(() => score.value >= props.quiz.minimum_score);
</script>
<Recap v-if="state === 'recap'" :answers="answers" :quiz="quiz"/>
Pour ne pas afficher les choix tout le temps dans le même ordre, on peut créer une liste aléatoire de choix qui permet à chaque fois qu'on rafraîchit la page d'avoir les choix mélangés.
Si on veut utiliser la méthode pour mélanger un tableau dans plusieurs composants, on peut l'exporter dans un nouveau fichier : functions/array.js qui, comme son nom l'indique, contiendra toutes les méthodes qu'on veut utiliser sur des tableaux :
export function shuffleArray(arr) {
return arr.map(item => ({value: item, sort: Math.random()}))
.sort((a, b) => a.sort - b.sort)
.map(item => item.value);
};
import { shuffleArray } from '@/functions/array.js;
const renderChoices = computed(() => shuffleArray(props.question.choices));
Comme il y a déjà beaucoup de code dans notre composant Question, on va créer un nouveau composant `Answer.vue` que l'on va importer dedans :
<template>
<label :for="id" :class="classes">
<input type="radio" :disabled="disabled" :id="id" name="answer" v-model="model" :value="value">
{{ value }}
</label>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
id: String,
disabled: Boolean,
value: String,
correctAnswer: String
});
const model = defineModel();
const classes = computed(() => ({
disabled: props.disabled,
right: props.disabled && props.value === props.correctAnswer,
wrong: props.disabled && props.value !== props.correctAnswer && model.value === props.value
}));
</script>
<style>
.disabled {
opacity: .5;
}
.right {
opacity: 1;
color: green;
}
.wrong {
opacity: 1;
color: red;
}
</style>
<Answer :id="`answer${index}`" :disabled="hasAnswer" :value="choice" v-model="answer" :correctAnswer="question.correct_answer"/>
import Answer from './Answer.vue';
Si on veut rajouter un timer pour dire que si l'on ne répond pas à une question en temps voulu on passe directement à la question suivante. Donc, on va supprimer le bouton "Question suivante" dans le composant Question et on y rajoute le timer :
import { onMounted, onUnmounted } from 'vue';
let timer;
onMounted(() => {
timer = setTimeout(() => {
emits('answer', answer.value);
}, 3_000);
});
onUnmounted(() => {
clearTimeout(timer);
});
On va rajouter un événement @change="onChange" dans le input :
const emits = defineEmits(['change']);
const onChange = (event) => {
emits('change', event);
};
Enfin, dans le composant Question, on va ajouter l'événement @change="onAnswer" :
const onAnswer = () => {
clearTimeout(timer);
timer = setTimeout(() => {
emits('answer', answer.value);
}, 1_5OO);
};
onMounted(() => {
timer = setTimeout(() => {
answer.value = '';
onAnswer();
}, 3_000);
});
Si vous avez fait un tour sur la documentation, vous avezz dû remarquer la présence d'un bouton permettant de changer le format de code. Mais à quoi correspond ces formats ?
Depuis le début de ce cours, on a utilisé l'API Composition mais il existe une autre approche qui utilise un objet d'option.
<script>
export default {
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
};
},
computed: {
// a computed getter
publishedBooks() {
// `this` points to the component instance
return this.author.books.length;
}
}
};
</script>
Cette manière d'écrire les composants est héritée de la version 2 de Vue.js mais n'est pas conseillée si vous commencez un nouveau projet. Elle avait comme principal inconvénient de rendre difficile la réutilisation de logique.
Pour pallier au problème de réutilisation, l'API composition a été introduite lors de la version 3 et utilisait une option setup.
<script>
export default {
setup() {
const author = ref({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
});
const publishedBooks = computed(() => author.value.books.length);
return {
author,
publishedBooks
};
};
};
</script>
Pour simplifier la syntaxe, l'attribut setup a été introduit pour simplifier encore plus la syntaxe.
<script setup>
const author = ref({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
});
const publishedBooks = computed(() => author.value.books.length);
</script>
C'est cette syntaxe que l'on utilisera aujourd'hui par défaut lorsque l'on travaille avec Vue.
Dans cette partie, nous allons explorer plus en profondeur le fonctionnement de la réactivité pour comprendre comment Vue.js gère la synchronisation entre le DOM et l'état. Nous allons pour cela explorer le code généré lorsque l'on construit le projet.
Lorsqu'on avait vu la partie template, on a remarqy" qu'il n'était pas nécessaire d'écrire le .value lorsqu'on fait référence à une ref. En analysant le code compilé, on remarque que Vue.js utilise systèmatiquement unref() lors de l'accès aux variables de premier niveau. Cette méthode permet d'accéder à la propriété value est une ref.
Ainsi <h1gt;{{ msg }}> devient :
createElementVNode("h1", null, toDisplayString(unref(msg)), 1);
Le second point est l'utilisation du virtual DOM. Un composant est une fonction qui va renvoyer un objet représentant la structure attendue. Cette structure est appelée du virtual DOM. Lorsque l'état change, la fonction est ré-exécutée pour renvoyer une nouvelle structure virtual DOM. En comparant cette structure à la précédente, Vue.js sera capable de déterminer les changeements à faire au niveau du DOM réel.
Maintenant, si on regarde le code d'un composant compilé, voici ce que l'on obtient :
const __sfc__ = {
__name: 'App',
setup(__props) {
const msg = ref('hello');
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("h1", null, _toDisplayString(msg.value), 1 /* TEXT */),
_withDirectives(_createElementVNode("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ((msg).value = $event))
}, null, 512, /* NEED_PATCH */), [
[_vModelText, msg.value]
])
], 64 /* STABLE_FRAGMENT */));
};
}
};
La fonction qui génère le Virtual DOM va agir comme un watchEffect() et Vue.js détectera automatiquement de quelle variable dynamique la fonction dépend. Lorsque la valeur d'une de ces variables change, Vue.js saura qu'il doit générer une nouvelle version du Virtual DOM.
Dans le fichier de config de notre projet, `vite.config.js`, on va rajouter une section build :
build: {
minify: false,
},
Ensuite, on lance la commande suivante : npm run build. On peut ainsi ouvrir, dans le dossier `dist`, notre fichier JavaScript qui contient le code compilé.
Dans cette partie, nous allons voir comment gérer les animations dans le cadre de nos composants et nous allons découvrir le composant Transition et TransitionGroup.
<template>
<button @click="toggleSpoiler">Afficher / Masquer le spoiler</button>
<Transition name="fadeslide">
<div v-if="showSpoiler" class="spoiler">
À la fin de la série, Marc Curningan meurt !
</div>
</Transition>
</template>
<script setup>
import { ref } from 'vue';
const showSpoiler = ref(false);
const toggleSpoiler = () => showSpoiler.value = !showSpoiler.value;
</script>
<style>
.spoiler {
padding: 1rem;
border: 1px solid #ffffff58;
transition: .5s;
}
.fadeslide-enter-from {
opacity: 0;
transform: translateX(10px);
}
.fadeslide-leave-to {
opacity: 0;
transform: translateX(-10px);
}
</style>
Si vous avez besoin plusieurs fois de cette transition fadeslide, on peut créer un composant `FadeSlideTransition.vue` :
<template>
<Transition name="fadeslide" mode="out-in" appear>
<slot></slot>
</Transition>
</template>
<style>
.fadeslide-enter-active,
.fadeslide-leave-active {
transition: .5;
}
.fadeslide-enter-from {
opacity: 0;
transform: translateX(10px);
}
.fadeslide-leave-to {
opacity: 0;
transform: translateX(-10px);
}
</style>
Pour l'exemple du TransitionGroup, on va se créer une liste de films comme ceci :
<template>
<form role="group" @submit.prevent="addMovie">
<input type="text" v-model="movie">
<button :disabled="movie.length === 0">Ajouter</button>
</form>
<TransitionGroup name="list" tag="ul">
<li v-for="movie in movies" :key="movie">
{{ movie }}
<button class="secondary" @click="removeMovie(movie)">x</button>
</li>
</TransitionGroup>
<button @click="randomize">Réorganiser</button>
</template>
<script setup>
import { ref } from 'vue';
import { shuffleArray } from '@/functions/array.js;
const movies = ref([
'Les Évadés',
'Le Parrain',
'The Dark Knight : Le Chevalier Noir',
'Pulp Fiction',
'Forrest Gump',
'Inception'
]);
const movie = ref('');
const addMovie = () => {
movies.value = [movie.value, ...movies.value];
movie.value = '';
};
const removeMovie = (movie) => {
movies.value = movies.value.filter(m => m !== movie);
};
const randomize = () => {
movies.value = shuffleArray(movies.value);
};
</script>
<style>
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-leave-active {
position: absolute;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
Provide et Inject sont des fonctionnalités offertes par Vue.js qui permettent de partager des données entre un composant parent et ses descendants, sans avoir besoin de passer ces données explicitement à travers chaque composant intermédiaire. Cela peut grandement simplifier la gestion des états et la communication entre composants dans une application Vue complexe.
Le mécanisme fonctionne de la manière suivante : le composant parent utilise la propriété provide pour fournir des données, tandis que les composants descendants utilisent la propriété inject pour recevoir ces données.
Par exemple, on peut parler du système de "dark mode", c'est-à-dire que si la page est blanche l'écriture est en noire et si la page est noire l'écriture est en blanc. On va créer le composant `DarkMode.vue` :
<template>
<slot></slot>
</template>
<script setup>
import { provide, ref, readonly } from 'vue';
const darkMode = ref(true);
provide('darkMode', {
darkMode: readonly(darkMode),
toggleDarkMode: () => {
darkMode.value = !darkMode.value;
}
});
</script>
Dans notre composant Button.vue, on peut injecter le "darkMode" :
<script setup>
const darkMode = inject('darkMode');
</script>
Dans notre composant App.vue, on peut encapsuler notre composant enfant de la balise <DarkMode> :
<template>
<DarkMode>
<Sidebar />
</DarkMode>
</template>
<script setup>
import DarkMode from './components/DarkMode.vue';
provide('darkMode', false);
</script>
Grâce à cela, dans notre composant Button.vue, on peut lui rajouter une classe CSS dark :
const classes = computed(() => ({
btn: true,
dark: props.dark || unref(darkMode),
}));
Dans notre composant Sidebar, on peut modifier notre composant Button pour pouvoir activer ou désactiver le darkMode :
<template>
<Button @click="toggleDarkMode">Bonjour</Button>
</template>
<script setup>
import { inject } from 'vue';
import Button from './components/Button.vue';
const { toggleDarkMode } = inject('darkMode');
</script>
Dans cette partie, je vous propose de découvrir quelques composants et éléments spéciaux dans Vue.js comme les composants l'élément template, l'élément component, le composant KeepAlive, le composant Teleport et le composant Sync (ou plus précisément Suspense) qui, lui, est en développement.
Dans une balise <template>, on peut en imbriquer une ou plusieurs autres balises identiques :
<template>
<h1>Démonstration des composants</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus repudiandae vel fuga, corporis nemo accusantium odio repellat delectus assumenda est non quam dignissimos quidem, incidunt porro quos! Nemo, distinctio recusandae.</p>
<label>
<input type="checkbox" v-model="showCount">
Afficher le compteur
</label>
<ul>
<template v-for="item of items">
<li v-if="item % 2 === 0">
{{ item }}
</li>
</template>
</ul>
</template>
<script setup>
import { ref } from 'vue';
const items = [1, 2, 3, 4];
const showCount = ref(false);
</script>
L'élément <component> permet, comme son nom l'indique, de générer un composant dynamiquement.
<template>
<h1>Démonstration des composants</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus repudiandae vel fuga, corporis nemo accusantium odio repellat delectus assumenda est non quam dignissimos quidem, incidunt porro quos! Nemo, distinctio recusandae.</p>
<label>
<input type="checkbox" v-model="showCount">
Afficher le compteur
</label>
<component :is="componentToShow" />
</template>
<script setup>
import { computed, ref } from 'vue';
import Compteur from './Compteur.vue';
import Placeholder from './Placeholder.vue';
const items = [1, 2, 3, 4];
const componentToShow = computed(() => {
if (showCount) {
return Compteur;
} else {
return Placeholder;
}
});
const showCount = ref(false);
</script>
Le composant KeepAlive va vous permettre de garder des composants actifs lorsqu'ils sont démontés avec un v-if ou lorsqu'on utilise l'élément component.
Par exemple, dans notre code juste en haut, le compteur est remis à 0 chaque fois que le composant Compteur est démonté car l'état n'est pas sauvegardé. Pour le garder, on va entourer l'élément <component> par le composant <KeepAlive> :
<KeepAlive>
<component :is="componentToShow" />
</KeepAlive>
Attention, on notera que ce KeepAlive va avoir un effet sur le comportement des composants enfants. En effet, les composants ne vont plus être considérés comme démontés ou montés, c'est-à-dire que, si vous utilisez le onMounted() et le onUnmounted() pour venir greffer des choses liées au DOM, ça peut poser un problème. À la place, vous aurez deux hooks que vous pourrez utilisez : onActivated() et onDesactivated().
Le composant <Teleport>, comme son nom l'indique, va permettre de téléporter un élément particulier. Par exemple, une boîte de dialogue qui sera plus dans l'élément initial mais téléporter ailleurs : dans le body.
<p>
<button @click="increment">Incrémenter</button>
<Teleport to="body">
<dialog open>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Officiis eligendi blanditiis officia voluptates incidunt molestias necessitatibus ut quo, error ea fugit, adipisci harum, aperiam tempore iusto. Earum inventore voluptatum aliquid corporis molestiae laborum cumque suscipit consequuntur dolores omnis sunt iste, itaque, vitae minus quis aperiam labore soluta, necessitatibus nemo. Debitis.
</dialog>
</Teleport>
</p>
Dans cette partie, je vous propose de découvrir comment gérer les styles au niveau de vos composants Vue.js. Par défaut, lorsque le code est compilé, le CSS dans les balises <style> va être placé dans un fichier CSS séparé. Dans ces conditions, il est important de faire en sorte que les classes que l'on utilise soient uniques, mais Vue.js offre des mécaniques qui vont nous permettre de gérer automatiquement l'isolation des styles.
La première mécanique est la mise en place d'un attribut scoped au niveau de notre balise style. Cet attribut va permettre de rajouter une condition lorsque les sélecteurs CSS vont être générés.
<style scoped>
.container {}
</style>
Une fois compilé le sélecteur deviendra :
.container[v-data-a6737]
Vue.js va rajouter un attribut sur l'ensemble des éléments injectés dans le DOM. Cet attribut permettra au sélecteur CSS de n'impacter que les éléments qui correspondent à ce composant directement.
Vous pouvez contrôler la portée d'un sélecteur à l'aide de la pseudo-classe :deep() :
<style scoped>
.container p {} /* deviendra ".container p[data-v-a6737]" */
.container :deep(p) {} /* deviendra ".container[data-v-a6737] p" */
</style>
Pour plus d'informations, vous pouvez vous rendre sur la page de la documentation.
Afin de limiter la portée des sélecteurs CSS, il est aussi possible d'utiliser le système de module CSS.
<template>
</template>
<p :class="$style.red">This should be red</p>
<style module>
.red {
color: red;
}
</style>
Si votre style dépend d'une variable d'un composant, vous avez la possibilité d'utiliser v-bind au niveau de la partie style.
<script setup>
import { ref } from 'vue';
const theme = ref({
color: 'red',
});
</script>
<template>
<p>Bonjour</p>
</template>
<style scoped>
p {
color: v-bind('theme.color');
}
</style>
Vue.js va générer une variable CSS qui sera changée lorsque la valeur à l'intérieur de la ref change.
Il existe également le :global(sélecteur) et le slotted(sélecteur). On peut aussi insérer du SASS, c'est-à-dire du scss :
<style scoped lang="scss">
@use 'sass:color';
.text {
color: color.scale(#FF0000, $alpha: -40%);
}
</style>
Vue Router est la bibliothèque officielle de routage pour Vue.js, essentielle pour créer des applications monopages (ou SPAs pour Single Page Applications). Elle permet de définir les routes de notre application, de gérer la navigation et de rendre des composants en fonction de l'URL. Grâce à Vue Router, on peut facilement mettre en place des fonctionnalités de navigation avancées, comme les routes imbriquées, la navigation dynamique et les transitions entre les vues.
Pour utiliser Vue Router, on va installer sa dépendance avec la commande :
npm install vue-router
Ensuite, dans notre composant Header, on va remplacer nos balises a par un composant nommé RouterLink :
<RouterLink to="/">Mon site</RouterLink>
Dans notre composant App, on va rajouter un composant <RouterView/>. Pour gérer les différentes routes, on va créer un nouveau fichier nommé routed.js :
import BlogPage from './pages/BlogPage.vue';
import ContactPage from './pages/ContactPage.vue';
import HomePage from './pages/HomePage.vue';
import SinglePage from './pages/SinglePage.vue';
export const routes = [
{ path: '/', component: HomePage },
{ path: '/blog', component: BlogPage },
{ path: '/contact', component: ContactPage },
{ path : '/blog/:id', component: SinglePage },
];
Dans notre fichier main.js, on va créer le routeur :
import { createApp } from 'vue';
import App from './App.vue';
import { createRouter, createWebHistory } from 'vue-router';
import { routes } from './routes.js';
const router = createRouter({
history: createWebHistory(),
routes
});
const app = createApp(App);
app.use(router);
app.mount('#app');
Ainsi, on ne peut plus récupérer l'id via les props, mais on va utiliser le hook useRoute() dans le composant :
/*
const props = defineProps({
id: String
});
*/
const route = useRoute();
console.log(route.params.id);
Mais il y a une solution pour quand même passer l'id dans les props :
{ path: '/blog/:id', component: SinglePage, props: true },
On peut également une mettre une expression régulière sur les paramètres de l'URL afin que l'utilisateur ne rentre pas n'importe quoi dans l'URL :
{ path: '/blog/:id(\\d+)', component: SinglePage, props: true },
On peut aussi avoir une route qui capture toutes les URLS qui ne sont pas dans les choix précédents :
{ path: '/:pathMatch(.*)*', component: NotFoundPage },
Enfin, on peut ajouter un nom à une route :
{ path: '/blog/:id(\\d+)', component: SinglePage, props: true, name: 'posts.show' },
<RouterLink :to="{name: 'posts.index'}">Blog</RouterLink>
On peut créer un layout pour plusieurs composants enfants ayant le même début de route :
{ path: '/blog', component: BlogLayout, children: [
{ path: '', component: BlogPage, name: 'posts.index' },
{ path: ':id(\\d+)', component: SinglePage, props: true, name: 'posts.show' },
] },
Dans cette partie, je vous propose de découvrir comment tester vos composants Vue.js. Pour cela, on va se reposer sur plusieurs librairies :
Vitest pour lancer les tests.
Vue test utils qui offre des méthodes utiles pour tester des composants.
On va installer les différentes dépendances utiles pour les tests :
npm i -D vitest jsdom @vue/test-utils
On va tester le composant Alert.vue suivant :
<template>
<div :class="classes">
<slot></slot>
<button aria-label="Close" @click="emits('close')">×</button>
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
type: String
});
const emits = defineEmits(['close']);
const classes = computed(() => `alert alert-${props.type}`);
</script>
<style>
.alert {
position: relative;
padding: 0.75rem 1.25 rem;
margin-bottom: 1rem;
border: 1px solid transparent;
}
</style>
Pour faire le test de ce fameux composant, on va créer le fichier `test/Alert.test.js` :
import Alert from '@/components/Alert.vue';
import { mount } from '@vue/test-utils;
import { describe, it, expect } from 'vitest';
describe('<Alert>', () => {
it('should render the correct HTML', () => {
const wrapper = mount(Alert, {
props: {
type: 'danger',
},
slots: {
default: 'Bonjour'
}
});
expect(wrapper.html()).toMatchInLineSnapshot();
});
});
Dans notre fichier de configuration, `vite.config.js`, on va rajouter la propriété test si elle n'existe pas avec le contenu :
test: {
environment: 'jsdom'
}
Et le defineConfig ne va plus être importé depuis vite, mais depuis vitest/config.
Au niveau du terminal, on va lancer la commande suivante :
npx vitest
On va ensuite tester le click sur le bouton "fermer" :
it('should emit close when closing', async () => {
const wrapper = mount(Alert, {
props: {
type: 'danger',
},
slots: {
default: 'Bonjour'
}
});
await wrapper.get('[aria-label="Close"]').trigger('click');
expect(wrapper.emitted()).toHaveProperty('close');
expect(wrapper.emitted().close).toHaveLength(1);
});
Maintenant, on va tester un composant un plus complexe, le Header.vue, car il contient les RouterLink dans un fichier `test/Header.test.js :
import Header from '@/components/Header.vue';
import { routes } from '@/routes.js';
import { mount, flushPromises } from '@vue/test-utils;
import { describe, it, expect, beforeEach } from 'vitest';
import { createMemoryHistory, createRouter } from 'vue-router';
describe('<Header>', () => {
let router;
beforeEach(async () => {
router = createRouter({
history: createMemoryHistory(),
routes
});
router.push('/');
await router.isReady();
});
it('should render correct HTML', async () => {
const wrapper = mount(Header, {
global: {
plugins: [router],
},
});
expect(wrapper.html()).toMatchInLineSnapshot();
router.push('/blog');
await router.isReady();
await flushPromises();
expect(wrapper.html()).toMatchInLineSnapshot();
});
});
Enfin, pour tester le BlogPage qui récupère des données d'une API, BlogPage.test.vue :
import { useFetch } from '@/composable/useFetch.js';
import BlogPage from '@/pages/BlogPage.vue';
import { routes } from '@/routes.js';
import { mount, flushPromises } from '@vue/test-utils;
import { describe, beforeEach, it, expect, afterEach } from 'vitest';
import { createMemoryHistory, createRouter } from 'vue-router';
describe('<BlogPage>, () => {
let router;
beforeEach(async () => {
router = createRouter({
history: createMemoryHistory(),
routes
});
router.push('/');
await router.isReady();
});
afterEach(() => {
useFetch.mock = {};
});
it('should render the right amount of articles', () => {
useFetch.mock = {
'https://jsonplaceholder.typicode.com/posts?_limit=26_page=1': () => Promise.resolve([] ),
};
const wrapper = mount(BlogPage, {
global: {
plugins: [router]
}
});
await flushPromises();
expect(wrapper.findAll('article')).toHaveLength(4);
});
});
Aujourd'hui, nous allons explorer ensemble Pinia, un gestionnaire d'État global pour Vue.js. Même si vous pouvez créer des valeurs réactives globales en utilisant les fonctionnalités de base de Vue, Pinia offre plusieurs avantages qui peuvent simplifier votre travail et améliorer la structure de votre application.
Pinia offre plusieurs avantages par rapport à une simple variable réactive :
La possibilité d'inspecter l'état et de le modifier à travers l'extension Vue.js.
Le support de rechargement à chaud.
Des outils de tests.
L'intégration dans Nuxt.js pour le rendu côté serveur.
Au-delà de ces avantages, ce qu'offre surtout Pinia, c'est une manière d'organiser les stores bien spécifiques avec la possibilité de rajouter des extensions (comme par exemple le stockage local afin de persister l'état lors du rechargment). C'est particulièrement intéressant lorsque vous travaillez en équipe car ça permet d'avoir une organisation prédéfinie et connue.
Si vous n'êtes pas intéressé par le rendu côté serveur et que vous avez un cas plutôt simple, il n'est pas forcément nécessaire d'utiliser Pinia dans votre application point, même si la librairie est relativement légère, elle apporte un poids supplémentaire qui n'est pas forcément pertinent.
Pour installer Pinia, on utilise la commande suivante :
bun add pinia
Dans notre application, on va utiliser le createPinia() :
import { createPinia } from 'pinia';
app.use(createPinia());
On va créer un store pour l'authentification dans le fichier `store/auth.js` :
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
export const useAuth = defineStore('auth', () => {
const user = ref(null);
const isAuthenticated = computed(() => user !== null);
const authenticate = () => {
user.value = {
username: 'Jane Doe'
};
};
const route = useRoute();
return {
user,
url: computed(() => route.fullPath),
isAuthenticated,
authenticate,
};
});
On va créer un store Pinia pour le thème dans le fichier `store/theme.js` :
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useTheme = defineStore('theme', () => {
const darkTheme = ref(false);
const switchTheme = () => {
darkTheme.value = !darkTheme.value;
};
return {
isDark: darkTheme,
switchTheme,
};
});
Dans cette partie, nous allons voir comment utiliser TypeScript dans le cadre d'un projet Vue.js. La vérification du code et la validation des composants Vue.js nécessitera l'utilisation de vue-tsc. La méthode la plus simple est d'initialiser le projet avec le support de TypeScript dès la création du projet.
npm create vite@latest my-vue-app -- --template vue-ts
Il sera possible ensuite de préciser le langage à utiliser dans nos composants à l'aide de l'attribut lang.
<script lang="ts" setup>
</script>
L'utilisation de TypeScript va changer la manière de déclarer les propriétés et les événements des composants.
<script lang="ts" setup>
const props = defineProps<{
prefix: string
}>();
const emit = defineEmits<{
change: [id: number] // named tuple syntax
update: [value: string]
}>();
const model = defineModel<string>();
</script>
Pour plus d'information sur les spécificités de TypeScript dans le cadre de Vue.js, n'hésitez pas à vous rendre sur la documentation.
Nuxt est un framework open source qui permet de créer des applications plus facilement en utilisant des composants Vue.js. Son principal objectif est de fournir une structure de projet prête à l'emploi avec le support du rendu côté serveur afin d'améliorer l'expérience utilisateur et le référencement de site développés avec Vue.js.
Pour commencer à utiliser Nuxt, il faut initialiser un projet avec Nuxt avec la commande suivante :
bun x nuxi@latest init nomProjet
Nuxt offre deux avantages principaux :
Une structure imposée qui permet de se concentrer sur le code. Il intègre vue-router mais vous n'aurez pas à définir les routes vous même. La position est le nom des fichiers vue permet à Nuxt de générer les routes pour vous.
Le rendu côté serveur permet à Nuxt d'utiliser vos composants vue pour générer le code HTML des pages qui composent votre site. Cela permet aux visiteurs de ne pas avoir à attendre le JavaScript pour voir la page s'afficher, mais permet aussi aux moteurs de recherche de pouvoir référencer votre site convenablement. Si avez besoin de plus d'information sur les différents modes de rendu, je vous renvoie vers cette vidéo qui explique la différence entre le rendu côté client et le rendu côté serveur.
Avec Nuxt, il est donc possible de générer des pages HTML qui vont être renvoyées à l'utilisateur. Ce rendu HTML peut être fait de plusieurs manières et il est impératif de les comprendre pour bénéficier au maximum des avantages de Nuxt.
Le rendu Serveur est la méthode par défaut utilisé pour rendre les pages via Nuxt. Lorsqu'un utilisateur accède à une page, le serveur exécute le code du composant et génère l'HTML pour cette page. Le rendu est ensuite envoyé au client, qui affiche simplement l'affichage HTML. Cette approche est intéressante pour des contenus dynamiques qui changent souvent mais apporte plusieurs inconvénients :
Surcharge du serveur : Si votre application web est très populaire ou si vous avez des pages complexes à rendre, cela pourrait entraîner une surcharge du serveur.
Temps d'attente plus longs : Les pages prennent un peu plus de temps à charger puisqu'elles doivent être rendues côté serveur.
Le pré-rendu consiste à rendre les pages à la compilation, en génération les versions HTML à l'avance qui peuvent ensuite être distribuées simplement. Cela permet de remédier aux deux problèmes vus plus haut mais ne fonctionne que pour des pages dont le contenu ne change jamais.
Ce type de rendu est un mélange entre les deux types de rendu précédents. Le principe est alors de distribuer aux utilisateurs une version pré-générée de la page mais de rendre en tâche de fond une nouvelle version de la page si la durée de la mise en mémoire est écoulée. Cette approche permet d'avoir la rapidité de rendu statique tout en ayant des données qui s'actualisent à un intervalle de temps prédéfini.
Il est possible de passer d'un mode de rendu à l'autre au cas par cas à l'aide de la configuration nuxt.config.ts.
export default defineNuxtConfig({
routeRules: {
// La page d'accueil est préchargée lors de la compilation.
'/': { prerender: true },
// La page produits est générée à la demande, se revalide en arrière-plan et reste dans le cache jusqu'à ce que les données API aient changé.
'/products': { swr: true },
// Les pages de produit sont générées à la demande, se revalident en arrière-plan et restent dans le cache pendant 1 heure (3600 secondes).
'/products/**': { swr: 3600 },
// La page blog est générée à la demande, se revalide en arrière-plan et reste dans le cache sur les serveurs CDN pendant 1 heure (3600 secondes).
'/blog': { isr: 3600 },
// Les pages de billet du blog sont générées à la demande une seule fois jusqu'à la prochaine mise à jour, restent dans le cache sur les serveurs CDN.
'/blog/**': { isr: true },
// La page d'interface administrative est affichée uniquement côté client.
'/admin/**': { ssr: false },
// Ajoutez des en-têtes CORS aux routes API.
'/api/**': { cors: true },
// Redirige les anciennes URLs vers de nouvelles URLs.
'/old-page': { redirect: '/new-page' }
}
});
Pour plus d'information sur les méthodes de rendu, vous pouvez vous rendre sur la page Rendering Modes de la documentation.
Dans cette partie, je vous propose de découvrir une librairie incontournable lorsqu'il s'agit de récupérer des données tiers avec Vue.js : TanStack Query.
Dans la plupart des cas, une application front-end va avoir besoin de récupérer des données depuis un serveur et de les afficher à l'utilisateur mais plusieurs pages peuvent avoir des mêmes données et gérer cela via un état global peut s'avérer complexe. TanStack Query approte un système de cache global qui permet de garder en mémoire les résultats d'une requête pour l'afficher instantanément à l'utilisateur lors de visites successives :
Lors du premier chargement d'une ressource, les données sont mises en mémoire et associées à une clef.
Si la même requête est chargée, les données en cache sont utilisées pendant que TanStack Query demande les nouvelles données au serveur.
Lorsque les nouvelles données arrivent, le cache est remplacé et la page est mise à jour.
Il est important de noter que TanStack Query n'a pas d'opinion sur comment vous récupérez les données. C'est à vous d'implémenter cette partie-là. La librairie ne se charge que de la mise en cache et de proposer une fonction composable utile.
Pour commencer à utiliser TanStack Query, on l'ajoute aux dépendances du projet.
$ npm i @tanstack/vue-query
# or
$ bun add @tanstack/vue-query
Puis, on l'ajoute comme plugin à notre projet Vue.js :
import { VueQueryPlugin } from '@tanstack/vue-query';
app.use(VueQueryPlugin);
Une fois chargé, on pourra utiliser la fonction composable useQuery() pour gérer le chargement de nos contenus.
<script setup>
import { useQuery } from '@tanstack/vue-query';
const {data, isLoading, isFetching, isError, error} = useQuery({
queryKey: ['posts'],
queryFn: () => getPosts({page: 1})
});
</script>
La clef permet de retrouver la requête plus tard pour pouvoir invalider le cache ou le mettre à jour. Par défaut, une requête est considérée comme "périmée" dès sa récupération mais il est possible de modifier ce comportement à l'aide du paramètre staleTime.
const {data, isLoading, isFetching, isError, error} = useQuery({
queryKey: ['posts'],
queryFn: () => getPosts({page: 1}),
staleTime: 30_000, // Temps avant que le cache soit considéré comme périmé (0 par défaut)
gcTime: 60_000, // Durée de vie du cache (5 min par défaut)
});
Pourquoi ces propriétés ?
Un cache périmé est utilisé pendant que les nouvelles données sont récupérées, l'utilisateur voit les anciennes données pendant le chargement des nouvelles données.
Un cache supprimé fait que l'utilisateur ne voit plus le contenu lors du second affichage d'une requête.
Lorsque l'on souhaitera mettre à jour des données il sera possible d'utiliser la fonction composable useMutation().
<script setup>
import { useMutation } from '@tanstack/vue-query';
const { isLoading, isError, error, isSuccess, mutate } = useMutation({
mutationFn: (data) => updatePost(props.id, data),
});
const handleSubmit = (e) => {
mutate(new FormData(e.target));
};
</script>
<template>
<form @submit.prevent="handleSubmit">
<!-- ... -->
<button type="submit">Mettre à jour l'article</button>
</form>
</template>
Contrairement aux requêtes, une mutation ne s'exécute pas au montage du composant mais seulement lorsque la méthode mutate ou mutateAsync est appelée. Il n'y a pas non plus de mise en cache.
Le problème de la mise en cache est que les données peuvent changer lorsque l'utilisateur effectue certaines opérateurs (traitement de formulaire par exemple). Dans ce cas-là, il est possible d'invalider manuellement le cache depuis le client.
import { useQueryClient } from '@tanstack/vue-query';
const queryClient = useQueryClient();
const handleSubmit = async () => {
// Mis à jour des données sur le serveur.
await updatePost(new FormData(e.target));
// On invalide les données concernant les articles.
await queryClient.invalidateQueries(
{
queryKey: ['posts'],
}
);
};
Il est aussi possible de venir modifier les données dans le cache pour un retour instantané aurpès de l'utilisateur. L'inconvénient est qu'il faut s'assurer de le faire pour toutes les requêtes qui peuvent contenir les données.
import { useQueryClient } from '@tanstack/vue-query';
const queryClient = useQueryClient();
const handleSubmit = async () => {
// Mis à jour des données sur le serveur.
await updatePost(new FormData(e.target));
// On invalide les données concernant les articles.
await queryClient.setQueryData(['posts'], (oldData) => {
// Génère les nouvelles données et les retourne.
return newData;
});
};
Enfin, une dernière fonctionnalité intéressante de TanStack Query est la possibilité de mettre en place facilement une pagination infinie grâce à la fonction useInfiniteQuery().
<script>
const {
fetchNextPage,
hasNextPage,
isLoading,
isFetching,
hasNextPage,
// Data contient l'ensemble des pages.
data,
} = useInfiniteQuery({
queryKey: ['posts'],
initialPageParam: 1,
queryFn; ({ pageParam }) => getPosts({page: pageParam}),
// Doit renvoyer bull s'il n'y a pas de page suivante.
getNextPageParam: (lastPage, pages) => pages.length + 1;
});
// On peut récupérer les articles sous forme de liste à plat.
const posts = computed(() => data.value?.pages.flat());
</script>
<template>
<main>
<;Spinner v-if="isLoading"/>
<div class="posts" v-else>
<PostCard v-for="post in posts" :key="post.id" :post="post"/>
</div>
<button @click="fetchNextPage" :disabled="isFetching" v-if="hasNextPage">Page suivante</button>
</main>
</template>