Séminaire
Objectifs
- Faire un projet avec Vue.js.
Rendu
- GitHub Classroom : https://classroom.github.com/a/bVgcQszS
- Moodle : lien vers le dépôt GitHub du projet
- Délai : 2 semaines
- Séminaire noté
Mise en place
- Cloner le nouveau dépôt GitHub Classroom dans le répertoire du cours.
- Ouvrir le dossier du dépôt dans Visual Studio Code.
Estimation
- Créer un ficher
report.mddans le dossier du dépôt. - Estimer le temps nécessaire pour réaliser ce travail dans le rapport.
- Découper le travail en tâches pour faciliter l'estimation.
- Une fois terminé, comparer le temps estimé avec le temps réellement passé.
| Tâche | Temps estimé | Temps passé | Commentaire |
|---|---|---|---|
| Estimation | 10m | 15m | ... |
| ... | ... | ... | ... |
| Total | 2h | 1h30 | ... |
Semaine 1
Vue.js
- Cloner le nouveau dépôt Git dans le répertoire du cours.
- Ouvrir le répertoire du dépôt Git dans Visual Studio Code.
- Installer l'extension Vue (Official).
- Créer le fichier
report.md. - Créer un projet Vue.js depuis le répertoire du dépôt :
npm create vue@latest- Project name:
quiz - Use TypeScript?
Yes - Select features to include in your project:
- Router (SPA development)
- Linter (error prevention)
- Prettier (code formatting)
- Select experimental features to include in your project:
- Rien sélectionner
- Skip all example code and start with a blank Vue project?
Yes
- Project name:
- Vérifier où se trouve le projet :
./├── .github/├── quiz/└── ...├── README.md└── report.md
- Copier tous les fichiers du projet Vue.js (dossier
quiz) dans le répertoire du dépôt Git (dossiersem07-app-{pseudo}) :./├── .github/├── .vscode/├── public/├── src/├── .editorconfig├── .gitattributes├── .gitignore├── .oxlintrc.json├── .prettierrc.json├── env.d.ts├── eslint.config.js├── index.html├── package.json├── README.md├── report.md├── tsconfig.app.json├── tsconfig.json├── tsconfig.node.json└── vite.config.ts - Pour le fichier
README.md, copier le contenu du projet Vue.js dans le fichierREADME.mddu dépôt Git, puis supprimer le fichierREADME.mddu projet Vue.js. - Supprimer le dossier
quiz(qui devrait être vide). - Installer les dépendances et formater le code :
npm installnpm run format
- Pour lancer le projet en mode développement :
npm run dev
- Ouvrir le navigateur à l'adresse indiquée pour voir le projet.
Bootstrap
- Installer Bootstrap et Bootstrap Icons :
npm install bootstrap @popperjs/core bootstrap-icons
- Changer la langue et le titre de l'application en modifiant
index.html:<!doctype html><html lang="fr"><head>...<title>Quiz</title></head>...</html> - Dans
eslint.config.ts, remplacerpluginVue.configs['flat/essential']parpluginVue.configs['flat/recommended']. - Créer les dossiers
src/components,src/assetsetsrc/views(dans le dossiersrcexistant). - Créer ou modifier les fichiers suivants :
- main.ts
- App.vue
- main.css
- index.ts
- AboutView.vue
- HomeView.vue
- QuizForm.vue
import "bootstrap-icons/font/bootstrap-icons.min.css";
import "bootstrap/dist/css/bootstrap.min.css";
import "./assets/main.css";
import "bootstrap";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(router);
app.mount("#app");
<script setup lang="ts">
import { RouterLink, RouterView } from "vue-router";
</script>
<template>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<RouterLink class="navbar-brand" to="/">
<i class="bi bi-question-square"></i>
Quiz
</RouterLink>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar"
aria-controls="navbar"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div id="navbar" class="collapse navbar-collapse">
<ul class="navbar-nav">
<li class="nav-item">
<RouterLink class="nav-link" to="/about">
<i class="bi bi-info-square"></i>
À propos
</RouterLink>
</li>
</ul>
</div>
</div>
</nav>
<RouterView />
</template>
/* https://getbootstrap.com/docs/5.3/components/buttons/#variables */
.btn-primary {
--bs-btn-color: #fff;
--bs-btn-bg: #0d6efd;
--bs-btn-border-color: #0d6efd;
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #0b5ed7;
--bs-btn-hover-border-color: #0a58ca;
--bs-btn-focus-shadow-rgb: 49, 132, 253;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #0a58ca;
--bs-btn-active-border-color: #0a53be;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff;
--bs-btn-disabled-bg: #0d6efd;
--bs-btn-disabled-border-color: #0d6efd;
}
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: HomeView,
},
{
path: "/about",
name: "about",
component: () => import("../views/AboutView.vue"),
},
],
});
export default router;
<template>
<div class="container mt-3">
<h1>À propos</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</p>
</div>
</template>
<script setup lang="ts">
import QuizForm from "@/components/QuizForm.vue";
</script>
<template>
<div class="container mt-3">
<h1>Quiz</h1>
<QuizForm />
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
const cheval = ref<string | null>(null);
const filled = computed<boolean>(() => cheval.value !== null);
function submit(event: Event): void {
event.preventDefault();
if (filled.value) {
alert(`Vous avez choisi la couleur ${cheval.value} !`);
}
}
</script>
<template>
<form>
De quelle couleur est le cheval blanc de Napoléon ?
<div class="form-check">
<input
id="chevalBlanc"
v-model="cheval"
class="form-check-input"
type="radio"
name="cheval"
value="blanc"
/>
<label class="form-check-label" for="chevalBlanc">Blanc</label>
</div>
<div class="form-check">
<input
id="chevalBrun"
v-model="cheval"
class="form-check-input"
type="radio"
name="cheval"
value="brun"
/>
<label class="form-check-label" for="chevalBrun">Brun</label>
</div>
<div class="form-check">
<input
id="chevalNoir"
v-model="cheval"
class="form-check-input"
type="radio"
name="cheval"
value="noir"
/>
<label class="form-check-label" for="chevalNoir">Noir</label>
</div>
<button
class="btn btn-primary"
:class="{ disabled: !filled }"
@click="submit"
>
Terminer
</button>
</form>
</template>
- Vérifier que l'application fonctionne correctement.
- Voici le code source et le site web final.
- Ne pas oublier de faire régulièrement des commits à chaque fois qu'on a un état stable du projet (code fonctionnel, comme ici).
- Déployer le projet sur GitHub Pages (voir la section Déploiement).
- Tester le projet (voir la section Test).
Quiz
- Modifier le quiz pour qu'il y ait deux-trois questions à choix multiples. Voici quelques idées :
- De quelle couleur est le cheval blanc de Napoléon ?
- Combien de pattes a un chat ?
- Quelle est la capitale de la Suisse ?
- Proposer quatre réponses possibles pour chaque question.
- Afficher le score à la fin du quiz.
- Mettre la logique du calcul dans la fonction
submit, juste après leevent.preventDefault(); - Une fois le score calculé, l'afficher dans l'alerte à la place de la réponse choisie.
- Afficher le score en pourcentage ou en nombre de bonnes réponses sur le nombre total de questions.
- Mettre la logique du calcul dans la fonction
- Afficher un message de félicitations si le score est parfait.
- Afficher un message différent selon le score.
- Ajouter un bouton pour réinitialiser le quiz.
- Ajouter un bouton dans
QuizForm.vue:<button class="btn btn-secondary" @click="reset">Réinitialiser</button> - Le bouton va appeler une fonction
resetqu'il faudra créer.
- Ajouter un bouton dans
- Modifier la couleur des
.btn-primarydansmain.css. - Changer les icônes dans la bar de navigation (en haut) en utilisant Bootstrap Icons.
Semaine 2
QuestionRadio
C'est fastidieux de devoir répéter les mêmes étapes pour chaque question. On va donc créer un composant pour les questions : QuestionRadio.vue.
Commencer par définir comment on voudrait que le composant fonctionne. On pourrait vouloir remplacer chaque question par un composant QuestionRadio :
<template>
<form>
<QuestionRadio
id="cheval"
v-model="cheval"
text="De quelle couleur est le cheval blanc de Napoléon ?"
:options="[
{ value: 'blanc', text: 'Blanc' },
{ value: 'brun', text: 'Brun' },
{ value: 'noir', text: 'Noir' },
]"
/>
...
</form>
</template>
- Le composant
QuestionRadiodoit recevoir les propriétés suivantes :v-model: la valeur de la réponse (bi-directionnel, car on veut pouvoir modifier la réponse depuis le composant parent lorsqu'on clique sur le bouton "Réinitialiser" et récupérer la réponse depuis le composant parent pour calculer le score).id: un identifiant unique pour le groupe de boutons radio.text: le texte de la question.options: un tableau d'objets pour les options de réponse. Chaque objet doit avoir une propriétévaluepour la valeur de la réponse et une propriététextpour le texte affiché de l'option.
- Ne pas oublier d'importer le nouveau composant dans
QuizForm.vue:<script setup lang="ts">import QuestionRadio from "@/components/QuestionRadio.vue";...</script>
Créer le fichier QuestionRadio.vue dans le dossier src/components :
<script setup lang="ts">
import { type PropType } from "vue";
const model = defineModel<string | null>();
const props = defineProps({
id: { type: String, required: true },
text: { type: String, required: true },
options: {
type: Array as PropType<Array<{ value: string; text: string }>>,
required: true,
},
});
</script>
<template>
{{ props.text }}
<div v-for="option in props.options" :key="option.value" class="form-check">
<input
:id="`${props.id}-${option.value}`"
v-model="model"
class="form-check-input"
type="radio"
:name="props.id"
:value="option.value"
/>
<label class="form-check-label" :for="`${props.id}-${option.value}`">
{{ option.text }}
</label>
</div>
</template>
- Dans la partie
<script>, on utilise les fonctionsdefineModeletdefinePropspour définir le modèle (v-model) et les propriétés (text,name,options) du composant. - Dans la partie
<template>:- On affiche le texte de la question :
{{ props.text }}. - On affiche les options de réponse en utilisant une boucle
v-forsurprops.options: le<div>sera répété pour chaque option. - La différence entre les attributs qui commencent par
:et ceux qui ne commencent pas par:est que les premiers sont des expressions JavaScript (interprétées) et les seconds sont des chaînes de caractères (non interprétées).
- On affiche le texte de la question :
Quelle est la différence entre un prop et un modèle (v-model) ?
QuestionText
De manière similaire, créer un composant QuestionText.vue pour les questions à réponse textuelle libre.
Voici un code qu'on voudrait extraire dans le composant QuestionText.vue :
<label for="exampleFormControlInput" class="form-label">
Combien de pattes a un chat ?
</label>
<input
id="exampleFormControlInput"
v-model="reponse"
class="form-control"
placeholder="Veuillez saisir un nombre"
/>
Et on souhaiterait le remplacer avec un nouveau composant QuestionText.vue comme ceci dans QuizForm.vue :
<QuestionText
id="chat"
v-model="reponse"
text="Combien de pattes a un chat ?"
placeholder="Veuillez saisir un nombre"
/>
Comment rendre la propriété placeholder optionnelle ?
Documentation : Vue.js + Bootstrap.
API
Open Trivia Database est une API qui fournit des questions de quiz. On va utiliser son API pour obtenir des questions aléatoires pour notre quiz :
On peut obtenir des questions en faisant une requête HTTP GET à l'URL suivante : https://opentdb.com/api.php?amount=3&type=multiple
amount: le nombre de questions à obtenir.type: le type de questions (multiple ou boolean).
Ajouter une nouvelle tab Trivia dans App.vue :
...
<ul class="navbar-nav">
<li class="nav-item">
<RouterLink class="nav-link" to="/trivia">
<i class="bi bi-question"></i>
Trivia
</RouterLink>
</li>
...
</ul>
...
Créer une nouvelle vue TriviaView.vue dans le dossier src/views :
<script setup lang="ts">
import QuizTrivia from "@/components/QuizTrivia.vue";
</script>
<template>
<div class="container mt-3">
<h1>Trivia</h1>
<QuizTrivia />
Source :
<a href="https://opentdb.com/" target="_blank">Open Trivia Database</a>
</div>
</template>
Mettre à jour le fichier router/index.ts en ajoutant une nouvelle route :
...
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
...
{
path: '/trivia',
name: 'trivia',
component: () => import('../views/TriviaView.vue'),
},
]
...
Finalement ajouter le composant QuizTrivia.vue dans le dossier src/components :
<script setup lang="ts">
import QuestionRadio from "@/components/QuestionRadio.vue";
import { reactive, ref } from "vue";
const questions = ref<
{
question: string;
correct_answer: string;
incorrect_answers: string[];
}[]
>([]);
const answers = reactive<{ [key: number]: string | null }>({});
fetch("https://opentdb.com/api.php?amount=3&type=multiple")
.then((response) => response.json())
.then((data) => (questions.value = data.results));
</script>
<template>
<form>
<QuestionRadio
v-for="(question, index) in questions"
:id="index.toString()"
:key="index"
v-model="answers[index]"
:text="question.question"
:options="[
{ value: question.correct_answer, text: question.correct_answer },
...question.incorrect_answers.map(answer => ({
value: answer,
text: answer,
})),
]"
/>
</form>
</template>
À sa création, ce composant va récupérer 3 questions avec l'API (https://opentdb.com/api.php?amount=3&type=multiple) et stocker les questions dans la ref questions.
Ensuite, on affiche chaque question avec le composant QuestionRadio (avec une boucle v-for) en passant les propriétés nécessaires.
QuestionCheckbox (bonus)
Les checkboxes sont comme les radios, mais on peut en sélectionner plusieurs. Créer un composant QuestionCheckbox.vue pour les questions à choix multiples. Voici un exemple d'utilisation :
<div class="form-check">
<input
id="checkboxJane"
v-model="checkedNames"
class="form-check-input"
type="checkbox"
value="Jane"
/>
<label class="form-check-label" for="checkboxJane">Jane</label>
</div>
<div class="form-check">
<input
id="checkboxJohn"
v-model="checkedNames"
class="form-check-input"
type="checkbox"
value="John"
/>
<label class="form-check-label" for="checkboxJohn">John</label>
</div>
Noter que comme la réponse est une liste, il faut initialiser la ref avec une liste vide :
const checkedNames = ref<string[]>([]);
Documentation : Vue.js + Bootstrap.
Améliorations
Ce projet reprend les deux premières semaines de celui de l'année dernière. Retrouver d'autres améliorations possibles dans les semaines suivantes.
Voici quelques idées supplémentaires pour aller plus loin :
QuestionCheckbox.vue: Sélectionner plusieurs réponses.QuestionSelect.vue: Sélectionner une réponse dans une liste déroulante.- Accepter plusieurs réponses possibles pour
QuestionText.vue(par exemple, "2" ou "deux"). - Adapter le Trivia pour pouvoir y jouer.
- Ordre aléatoire des choix dans
QuestionRadio.vue. - Ordre aléatoire des questions.
- Paramétrer les questions de l'API.
- Choisir le nombre de questions, la catégorie, la difficulté, le type, …
Comme les améliorations ne demandent pas toutes le même travail, elles seront pondérées différemment selon leur complexité (par exemple, l'ordre aléatoire des choix est simple donc il ne comptera que pour 0.5 pour le critère).
Indiquer les améliorations réalisées :
- Pourquoi les avoir choisies ?
- Comment les avoir réalisées (problèmes rencontrés, solutions trouvées, …) ?
Quelles améliorations supplémentaires pourraient être réalisées ?
Déploiement
Comme pour le générateur de site statique, on a besoin de compiler le projet avant de le déployer.
Sur la page GitHub du dépôt, aller dans Settings > Pages > Sous Build and deployment puis Source, sélectionner GitHub Actions.
Créer un fichier .github/workflows/deploy.yml avec le contenu suivant :
# https://vite.dev/guide/static-deploy.html#github-pages
name: Deploy static content to Pages
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
id-token: write
pages: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Setup Pages
uses: actions/configure-pages@v6
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: "./dist"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
Ajouter le lien du site dans le rapport.
Le lien renvoie vers une page blanche. Pour corriger cela, modifier le fichier vite.config.ts pour ajouter le chemin de base (nom du dépôt GitHub) :
import vue from "@vitejs/plugin-vue";
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vueDevTools from "vite-plugin-vue-devtools";
// https://vite.dev/config/
export default defineConfig({
base: "/sem07-app-{remplacer-pseudo}/",
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
Pourquoi cette erreur ? Comment la solution proposée la corrige-t-elle ?
Lorsqu'on change de page et qu'on la rafraîchit, on obtient une erreur 404. Pour corriger cela, remplacer createWebHistory par createWebHashHistory dans le fichier src/router/index.ts :
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
...
],
})
...
Pourquoi cette erreur ? Comment la solution proposée la corrige-t-elle ?
Test
Exécuter la commande npm install -D eslint-plugin-prettier.
Ajouter Prettier dans ESLint en modifiant le fichier eslint.config.ts (parfois caché sous package.json) :
...
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
export default defineConfigWithVueTs(
...
skipFormatting,
eslintPluginPrettierRecommended,
)
Créer un fichier test.js à la racine du dépôt avec le contenu suivant :
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import { execSync } from "child_process";
test("présence des fichiers et dossiers", () => {
// Liste des chemins attendus dans le projet.
const expectedPaths = [
".git",
".github/workflows/deploy.yml",
".gitignore",
".prettierrc.json",
"eslint.config.ts",
"index.html",
"package.json",
"README.md",
"report.md",
"src/App.vue",
"src/main.ts",
"src/router/index.ts",
"test.js",
"tsconfig.json",
"vite.config.ts",
];
// Vérifie que chaque chemin existe dans le projet.
expectedPaths.forEach((path) => {
assert.ok(
fs.existsSync(path),
`Le fichier ou dossier ${path} doit exister.`,
);
});
});
test("validation des fichiers", () => {
// Exécute la validation en utilisant https://eslint.org/
try {
const output = execSync("npx eslint").toString();
console.log(output);
} catch (error) {
assert.fail(error.output);
}
});
test("construction du site", () => {
try {
const output = execSync("npm run build").toString();
console.log(output);
} catch (error) {
assert.fail(error.output);
}
});
Ajouter le script test dans le fichier package.json pour pouvoir exécuter les tests avec la commande npm test :
{
...
"scripts": {
...
"test": "node --test"
}
}
Vérifier que les tests passent avec la commande npm test et corriger le code si cela n'est pas le cas.
Rapport
- Expliquer brièvement les principales difficultés rencontrées et comment les résoudre.
- Compléter les estimations.
- Ajouter une auto-évaluation du projet en utilisant les critères d'évaluation.
# Rapport
...
## Auto-évaluation
| Critère | Auto-évaluation (0, ½ ou 1) | Commentaire |
| ------------------------------------------------------------------------------------ | --------------------------- | ----------- |
| Le rendu est correct avec un journal de bord complet. | | |
| Le journal de bord est bien structuré et synthétique. | | |
| L'application a les mêmes fonctionnalités que l'exemple. | | |
| L'application est personnalisée. | | |
| L'application a plus de fonctionnalités que l'exemple. | | |
| L'application a encore plus de fonctionnalités que l'exemple. | | |
| Bootstrap est correctement utilisé pour rendre l'application responsive. | | |
| Le code suit les conventions de codage (formatage, nommage, organisation, …). | | |
| Le code est lisible et maintenable (nommage, commentaires, …). | | |
- Pousser tous les changements sur GitHub.
- Copier le lien du dépôt GitHub dans le devoir sur Moodle.
Aides
Nettoyage et vérification
- Vérifier tout le projet et nettoyer les codes qui ne sont plus utilisés.
- Vérifier que le code est correct localement, on peut construire le projet :
npm run build
- Vérifier que le site est correct en ligne et fonctionne sur différents appareils (ordinateur, téléphone, …).
- Vérifier que le rendu du rapport est correct.
Documentations
S'aider des documentations officielles pour réaliser le projet :
Code d'exemple :
Évaluation
L'évaluation du projet se portera sur les critères suivants :
- Rapport
- Le rendu est correct avec un journal de bord complet.
- Le journal de bord est bien structuré et synthétique.
- Fonctionnalités
- L'application a les mêmes fonctionnalités que l'exemple.
- L'application est personnalisée.
- L'application a plus de fonctionnalités que l'exemple.
- L'application a encore plus de fonctionnalités que l'exemple.
- Bootstrap est correctement utilisé pour rendre l'application responsive.
- Code
- Le code suit les conventions de codage (formatage, nommage, organisation, …).
- Le code est lisible et maintenable (nommage, commentaires, …).
| Note | 1 | 2 | 2.5 | 3 | 3.5 | 4 | 4.5 | 5 | 5.5 | 6 |
|---|---|---|---|---|---|---|---|---|---|---|
| Nombre de critères validés | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
- En gras : critères principaux.
- En italique : critères secondaires.