VueJS

VueJS est une librairie javascript qui permet de construire des UI web de façon réactive et déclarative.

Comme ReactJS et Angular, il encourage la création et l’utilisation de composants sur le modèle des éléments HTML.

Creation d’un premier projet VueJS avec Vite et TailwindCSS

Installer Vite, VueJS, TailwindCSS

Configurer TailwindCSS

Remplacer tailwind.config.js :

Remplacer src/style.css :

Tester

Remplacer src/App.vue :

App.css peut être supprimé

Exécution dans le serveur de développement :

  • index.html exécute src/main.js : l’exécution de createApp(App).mount('#app') rend dynamique le template <div id="app">...</div> de index.html

  • count est réactif : quand sa valeur change, les parties du DOM qui le référencent (comme {{ count }}) changent aussi en temps-réel

  • dans le contenu texte des éléments du template, on peut utiliser {{ <expr> }}

  • dans l’attribut d’un élément, on peut écrire :<attr>="<expr>"

Les expressions peuvent être quelconques, multiples (séparées par des ;) et référencer des variables réactives.

Binding

Contenu d’un élement

’>

Attribut d’un élément

’>

Double binding pour <input>, <textarea>

<input v-model="name"> est du sucre syntaxique pour :

Propriétés calculées (computed properties)

Une computed property est un objet réactif dont la valeur est synchronisée sur la valeur d’autres objets réactifs par le biais d’une fonction.

Exemple :

  • elles apportent beaucoup de déclarativité
  • les propriétés calculées sont mises en cache selon leurs dépendances ; leur évaluation est paresseuse

Boucles et conditionnelles

La propriété key est indispensable si des insertions / suppressions sont effectuées

Composants

  • un composant est une vue paramétrable avec des propriétés props fournies par son composant père
  • un composant peut émettre des événements vers son composant père

  • props down, events up : les propriétés descendent du père vers le fils, alors que des événements remontent du fils vers le père.
  • l’héritage des props ne va que du père vers les fils, il ne descend pas vers les petit-fils
  • un composant émet des événements que seul son père peut écouter (et pas son grand-père)

Le modèle de communication est donc simple et bien cloisonné, strictement entre un père et son fils.

Cycle de vie d’un composant

On peut créer des hooks pour chacun de ces moments du cycle de vie, par exemple :

Playgroound

https://play.vuejs.org

Exemple complet : chat temps-réel

Back-end

Front-end VueJS

La configuration de développement de Vite permet d’éviter l’erreur CORS qui interviendrait parce que le serveur de développement frontend et le serveur backend tournent sur le même serveur localhost, mais sur des ports différents :

Routage des URL front-end

Avec un site web statique dont les pages sont générées côté serveur, lors d’un changement d’url, le navigateur envoie une requête au serveur et remplace la vue par le contenu fourni par le serveur.

Avec une application SPA, le code JS peut surveiller et intercepter les changements de window.location.href et mettre à jour le DOM en fonction de règles fournies par un module appelé routeur.

Vue-router

Le routage des url par le client n’est pas assuré par VueJS lui-même, mais par un module tiers : vue-router.

main.js

/src/App.vue

router/index.js

components/ComponentA.vue

components/ComponentB.vue

Algorithmique

  • à chaque fois qu’un nouvel url est poussé (router.push, lien <router-link>), ou modifié (router.go), la table de routage est consultée et la première règle qui s’applique est prise en compte. Toutes les autres sont ignorées.
  • le tag <router-view /> de plus haut niveau est remplacé par le composant spécifié par la règle active. Selon la valeur de props (booléen, objet, fonction), les props du composant sont affectées (voir plus loin)
  • si la route contient des children, le même algorithme est appelé récursivement pour peupler les <router-view /> emboités

Le plugin Vue dans le navigateur permet de suivre les associations entre routes et instances de <router-view />

Passage des props aux composants d’une route

Mode booléen

Indiquer props: true passe au composant référencé par la route, tout l’objet route.params en tant que props. Par exemple :

Ici c’est l’objet { id, postid } qui est passé comme props au composant Post associé à la route

Mode objet

Quand props dans la définition de la route est un objet, c’est lui qui sera passé tel-quel au composant

Mode fonction

Quand props dans la définition de la route est une fonction, c’est la valeur de cette fonction qui sera passée au composant. Cette fonction prend comme argument l’objet route. Par exemple :

Exemple complet

/src/router/index.js

/src/use/useCities.js

/src/App.vue

/src/views/CityList.vue

/src/views/CityDetail.vue

Bonne pratique : normaliser les données

Normalizing State Shape

Activité à réaliser : gestion de tickets, part-1

Créer un projet ‘ticket’ et initialiser vuejs/tailwindcss dans un répertoire ‘frontend’.

Dans une branche ‘part-1’, réaliser un formulaire de saisie de ticket d’intervention :

  • catégorie : choix parmi ‘Mobilier’, ‘Informatique’, ‘Autre’
  • description : saisie d’un texte libre
  • urgence : choix parmi ‘faible’, ‘normale’, ‘forte’

Lors de l’appui sur ‘Valider’, afficher sur la console les données saisies.

Activité à réaliser : gestion de tickets, part-2

Dans une branche ‘part-2’, ajouter une logique de validation au formulaire :

  • l’email doit être correctement formé
  • une catégorie doit avoir été choisie
  • la description doit être non vide
  • un niveau de priorité doit avoir été choisi

Le bouton ‘Valider’ doit être ‘disabled’ tant que le formulaire est invalide.

Activité à réaliser : gestion de tickets, part-3

Dans une branche ‘part-3’, ajouter une sauvegarde des données saisies dans une base de données :

  • dans une répertoire ‘backend’, créer un projet npm Express/Prisma
  • le schéma de la base de données est :
generator client {
   provider = "prisma-client-js"
}

model Ticket {
   id                 Int               @default(autoincrement()) @id
   created_at         DateTime          @default(now()) @db.Timestamptz(6)
   handled_at         DateTime?
   done_at            DateTime?
   email              String
   category           Category
   description        String
   priority           PriorityLevel
   status             Status            @default(submitted)
   answer             String?
}

enum Category {
   furniture
   computer
   other
}

enum PriorityLevel {
   low
   normal
   high
}

enum Status {
   submitted
   ongoing
   done
}

datasource db {
   provider = "postgresql"
   url      = "postgresql://user:password@localhost:5432/tickets"
}
  • créer la route GET /api/ticket qui renvoie la liste de tous les tickets
  • créer la route POST /api/ticket qui créé un nouveau ticket

Activité à réaliser : gestion de tickets, part-4

Dans une branche ‘part-4’, ajouter un routeur pour gérer plusieurs pages :

  • installer et configurer vue-router
  • mettre la saisie d’un ticket sur la route /
  • ajouter une page /recap/:id qui affiche le récapitulatif de la demande :id, et aller vers cette page à chaque création de ticket
  • ajouter une page /tickets qui affiche la liste de tous les tickets et leur statut

Activité à réaliser : gestion de tickets, part-5

Dans une branche ‘part-5’, structurer l’application en composants :

  • TicketCard, propriété ‘ticketId’, qui affiche dans un petit cadre rectangulaire les informations essentielles d’un ticket d’identifiant ‘ticketId’, sauf le texte de description
  • TicketDetail, propriété ‘ticketId’ qui affiche toutes les informations du ticket

Modifier la route /tickets pour qu’elle affiche la liste des tickets sous forme d’une colonne de TicketCard à gauche, et que le clic sur un des TicketCard ‘ticketId’ affiche le détail TicketDetail dans l’espace restant à droite, associé à la route /tickets/:ticketId

Lorsqu’un ticket de la liste de gauche est cliqué, sa couleur de fond doit changer pour indiquer qu’il est sélectionné. Réciproquement, lorsque l’url /ticket/:ticketId est entrée, le ticket associé de la liste doit apparaître sélectionné.

Activité à réaliser : gestion de tickets, part-6

Dans une branche ‘part-6’, normaliser les données et les mettre dans un état central.

Créer un module use/useTickets.js qui contient toute la logique métier de gestion des tickets, et qui stocke les informations recueillies dans un cache partagé :

  • créer un cache ‘id2ticket’ qui associe à chaque id le ticket associé
  • exporter une fonction ‘asyncTicket(ticketId)’ qui renvoie une promesse de ticket. Si ticketId est dans le cache il est renvoyé ; sinon il est demandé au backend et la promesse est ensuite résolue
  • ajouter dans ce module les accès HTTP au backend, en stockant immédiatement les données reçues dans le cache d’état

Activité à réaliser : gestion de tickets, part-7

Dans une branche ‘part-7’, persister l’état dans sessionStorage

En utilisant https://vueuse.org/core/useSessionStorage/, persister le cache id2ticket dans la clé ‘id2ticket’ de sessionStorage. Vérifier que le contenu du cache résiste au rechargement de la page

Activité à réaliser : gestion de tickets, part-8

Dans une branche ‘part-8’, mettre en place un filtrage sur la liste des tickets

  • filtrage par catégories
  • filtrage par priorité

Activité à réaliser : gestion de tickets, part-9

Dans une branche ‘part-9’, mettre en place une authentification et autorisation minimales.

  • mettre en place une route frontend publique /signin qui demande la saisie d’un email et d’un mot de passe et qui appelle le backend avec POST /api/auth { email, password }. Pour simplifier le backend contiendra une liste d’utilisateurs en dur :
[
   { email: 'admin@mail.fr', admin: true, password: '2xaf67yret32' },
   { email: 'jc@mail.fr', admin: false, password: 'hjaf67yret32' },
   ...
]

Le backend renverra un ordre de stockage d’un cookie JWT nommé access_token dont le payload contient les autorisations, 'read-list'='all', 'read-ticket'='all', 'write-ticket'='all' pour un administrateur, 'read-list'='(email)', 'read-ticket'='(email)', 'write-ticket'='none' pour les autres

  • sécuriser les routes backend pour qu’elles prennent en compte les autorisation contenues dans le payload du cookie JWT transmis avec chaque requête, en filtrant les listes renvoyées, ou en renvoyant le status 403 selon les cas
  • sécuriser les routes frontend pour afficher des messages d’interdiction, en fonction de la page demandée et du cookie d’authentification présent