Open menu

Préambule

  • Ce TP permet de créer son premier projet vuejs avec vue-cli et de comprendre l'organisation générale d'une application SPA créée via vue-cli.
  • Le contexte est orienté jeu de rôle, avec des villes, des rues et des boutiques où des aventuriers peuvent acheter des items pour ensuite les assigner à des parties de leur personne.

 

  • Ce TP permet également de découvrir quelques éléments basiques de la syntaxe vuejs en écrivant quelques composants simples.
  • A noter que certaines parties du sujet consiste à copier/coller du code dans certains fichiers, sans fournir d'explications détaillées. Dans ce cas, celles-ci seront données lors des cours dédiés à la syntaxe vuejs ou aux plugins.
  • En conclusion, ce TP est plutôt un tutoriel de découverte qu'un véritable exercice.

 

0°/ Installer l'environnement de développement

 

1°/ Créer le projet avec vue-cli + vue-router + vuex + vuetify

1.1°/ Créer un canevas de base 

  • Dans un terminal, taper : vue create drmad
  • on obtient un menu comme suivant :

Vue CLI v4.5.14
┌──────────────────────────────────────────┐
│ │
│ New version available 4.5.14 → 5.0.8 │
│ Run npm i -g @vue/cli to update! │
│ │
└──────────────────────────────────────────┘

? Please pick a preset: (Use arrow keys)
Default ([Vue 2] babel, eslint)
Default (Vue 3) ([Vue 3] babel, eslint)
Manually select features

 

  • Utiliser la flèche bas pour choisir "Manually select features". On arrive sur un second menu :

Vue CLI v4.5.14
┌──────────────────────────────────────────┐
│ │
│ New version available 4.5.14 → 5.0.8 │
│ Run npm i -g @vue/cli to update! │
│ │
└──────────────────────────────────────────┘

? Please pick a preset: Manually select features
? Check the features needed for your project:
◉ Choose Vue version
◉ Babel
◯ TypeScript
◯ Progressive Web App (PWA) Support
◉ Router
❯◉ Vuex
◯ CSS Pre-processors
◉ Linter / Formatter
◯ Unit Testing
◯ E2E Testing

 

  • Pour sélectionner un item, il suffit d'utiliser les flèches haut/bas puis d'appuyer sur espace mettre à jour la sélection d'un item.
  • Pour ce TP, on va installer les plugins vue-router et vuex, comme indiqué dans l'exemple ci-dessus.
  • Une fois la sélection faite, appuyer sur "Entrée". Un nouveau menu apparaît avec plusieurs questions qui se suivent.

ATTENTION ! Dans cet exemple, le profil par défaut crée un projet basé sur vuejs v2, mais selon la version de vue-cli, le choix par défaut peut également être la v3.

  • Voici les réglages à utiliser pour les TPs (la plupart sont des choix par défaut) :
    • vue v2.x  (ATTENTION ! on travaille exclusivement en vuejs V2 => ne surtout pas choisir v3.x)
    • history mode for router
    • ESLint with error prevention only
    • Lint on save
    • Config in dedicated config files.

 

  • Ensuite, vue-cli va créer un répertoire drmad, et télécharger plein de modules node, puis créer les fichiers "par défaut" du projet.
  • Il y a notamment :
    • package.json : fichier décrivant les différent modules nodejs utilisés, leur dépendances et certaines options de configuration.
    • node_modules : où est stocké le code des modules.
    • public : contient le patron de fichier html utilisé pour l'application SPA
    • src : les sources de l'application.
  • Sauf exception, on a besoin d'intervenir uniquement sur les fichiers de src.

  • src contient :
    • App.vue : décrit la page principale de l'application. Le fichier est structuré comme tous les fichiers .vue, avec une partie <template> décrivant le html, une partie <script> contenant du javascript et notamment tout ce qui est associé à vuejs (data, methods, ...), et enfin une partie <style> avec le CSS pour formater l'affichage. 
    • main.js : crée l'instance de vue. On modifie ce fichier quand on veut ajouter à la vue des modules/plugins après la création (par ex, vue-router, vuetity, ...). 
    • components : le répertoire contenant les composants de l'application, généralement sous forme de fichier .vue. Par défaut, vue-cli crée un composant HelloWorld.vue qui permet seulement de personnaliser le titre principal du composant.
    • router : le répertoire contenant le fichier de définition des "routes" de l'application. ATTENTION ! Ce ne sont pas les routes que l'on pourrait utiliser pour faire des requêtes à une API. Il s'agit de routes "internes" à l'application, c.a.d. ce qui permet de passer d'une partie de l'application à une autre.
    • views : le répertoire qui contient des composants dont l'affichage est conditionné au fait de suivre une route. Ce répertoire n'est pas obligatoire (on pourrait tout mettre dans components) mais il permet de différencier les composants manipulés par le router.
    • store : le répertoire contenant le définition du dépôt central de données. En effet, l'application est construite en assemblant des composants. Quand ceux-ci doivent manipuler des données en commun, la meilleure solution consiste à utiliser un tel dépôt. En effet, en vuejs basique (donc sans vuex), il est facile de passer des données entre un composant père et un fils, mais c'est plutôt fastidieux de le faire entre des frères, cousins, ...

 

1.2°/ Ajouter vuetify

  • Par défaut, vuejs n'est pas "livré" avec des composants graphiques customisés. On a donc uniquement accès à ceux définis grâce aux balises HTML classiques : <button>, <input>, ...
  • Pour créer une IG digne de ce nom, il faut ajouter au projet une bibliothèque de composants.
  • Il existe plusieurs bibliothèques, plus ou moins sophistiquées. Une des plus aboutie est Vuetify, qui sera abordée dans un cours dédié.
  • Cette bibliothèque propose de très nombreux composants facilement customisables, avec parfois des fonctionnalités très avancées (par ex, les tables avec pagination/filtre/sélection/...)
  • Elle a cependant 2 inconvénients mineurs :
    • le code à écrire est plutôt verbeux
    • elle n'est pas encore (mais bientôt) disponible pour la version 3 de vuejs, ce qui peut représenter un problème de pérennité à long terme.

 

  • Pour ajouter vuetify au projet, il suffit d'aller dans le répertoire drmad puis de lancer : vue add vuetify.
  • Un menu demande quel "profil" de vuetify doit être utilisé. Garder celui par défaut  : ❯ Vuetify 2 - Vue CLI (recommended) 
  • vue-cli télécharge le plugin et modifie les fichiers de base, notamment App.vue et main.js

 

  • Il est possible qu'avec certaines configurations, l'ajout se termine avec une erreur :

 added 13 packages in 3s
⠋ Running completion hooks...
/home/sdomas/cours/Vuejs/TP/drmad/src/views/HomeView.vue
9:11 error Component name "Home" should always be multi-word vue/multi-word-component-names

✖ 1 problem (1 error, 0 warnings)

 

  • Dans ce cas, il suffit d'éditer le fichier src/views/HomeView.vue et de changer la ligne name: 'Home' en name: 'HomeView'
  • Ensuite, passez directement à la section suivante ( = ne surtout pas relancer vue add vuetify)

 

1.3°/ Tester le projet

  • Pour vérifier que tout est en place, il suffit de lancer la compilation et le serveur de test : npm run serve
  • Par défaut, le serveur web de test est configuré pour que l'URL de l'application soit : http://localhost:8080
  • Utiliser un navigateur pour vérifier que l'application est accessible.
  • Si tout est Ok, arrêter le serveur de test (ctrl+c)

 

1.4°/ Passer le projet sous Idea

  • Pour cela, consulter le chapitre 3 de l'article : support : Installation de vue-cli & config. idea
  • Si vous avez suivi correctement les indications, un clic sur la flèche verte permet de compiler le projet et lancer un serveur de test (NB : ça lance la commande npm run serve).
  • Vous pouvez laisser le serveur tourner : chaque fois que vous modifierez les sources, idea compilera de nouveau le projet et s'il n'y a pas d'erreur, la page du navigateur sera réactualisée.

 

2°/ Compléter la structure du projet

2.1°/ Les services d'accès aux données.

  • Comme dit en cours, une application SPA bien conçue permet de développer le front-end indépendamment du back-end.
  • Cela implique notamment de définir précisément le format des données que le front va envoyer/recevoir du back. A l'heure actuelle, on utilise généralement des objets JSON (ou des tableaux d'objets)
  • Dans ce cas, on peut tester le front sans avoir besoin du back.

IMPORTANT !

Quand on interroge une API, il est possible que la requête soit malformée, ou bien que des paramètres soient invalides, etc. Dans ce cas, l'API devrait toujours signaler au front qu'il y a une erreur. Dans ce cas, le problème est qu'il faut que le front dispose d'un moyen de savoir s'il y a eu erreur ou bien si les données reçues sont valides.

Le système le plus simple et universel pour mettre cela en place consiste à toujours renvoyer un objet JSON au format : { error: code_erreur, status: code_status, data: données }  avec :

  • error :valeur représentant le code de l'erreur (ou 0 si pas d'erreur)
  • status : valeur représentant le statut de la requête, qui sert de deuxième information sur le traitement de la requête. Dans le cas d'une API REST via http, status est souvent initialisé avec le statut http de la requête, par exemple 200, 201, 400, 404, etc. L'application émettrice de la requête peut se servir ou non de cette information pour réagir au résultat de la requête.
  • data : les données demandées s'il n'y a pas d'erreur, ou bien un message/objet d'erreur, si code_erreur est différent de 0.

 

C'est ce principe qui est utilisé pour ce TP.

 

  • L'idée est d'écrire des méthodes qui vont renvoyer des données au même format que celle de l'API, MAIS soit en allant chercher ces données dans un fichier, soit en les générant.
  • Quand l'API sera fonctionnelle, on pourra remplacer ces méthodes par celles qui vont faire les requêtes sur l'API de façon transparente.
  • Pour mettre en place ce mécanisme, tout en prévoyant de pouvoir accéder à l'API, on définit des services d'accès aux données.
  • Pour cela, on crée un sous-répertoire nommé par exemple services et on crée dedans des fichiers JS.
  • Chaque fichier contient des fonctions permettant d'accéder à des données d'une certaine catégorie auprès d'une certaine source de données.

 

  • Dans ce TP, il y aura des données pour représenter les personnages et les villes. On va donc créer 2 fichiers service :
    • dans le répertoire src, créer le sous-répertoire services.
    • copier dans ce répertoire le fichier à télécharger : [ towns.service.js ]
    • créer dans ce répertoire un fichier vide : persos.service.js
  • Ces deux fichiers seront complétés au fur et à mesure du TP, mais le premier contient déjà un exemple de comment écrire un triplet de fonctions pour chaque requête en vue d'obtenir des données. En l'occurrence, on a :
    • getAllTownsFromLocalSource() qui va chercher des données dans la source locale (cf. section suivante)
    • getAllTownsFromAPI() qu'il faudra écrire et surtout utiliser quand l'API sera fonctionnelle
    • getAllTowns() qui appelle l'une ou l'autre fonction et qui, en l'absence d'erreur, va convertir les données reçues au format interne, puis renvoyer le résultat.
  • L'explication détaillée de comment écrire ces méthodes est donnée en section 4.

 

2.2°/ Créer la source de données "locale"

  • Comme indiqué ci-dessus, ce TP n'utilise pas une (ou des) API comme source de données. Celle-ci va être locale, sous la forme d'un fichier JS importé dans les services.
  • Le problème principal d'un fichier vient du fait que l'application finale va envoyer des requêtes pour obtenir des données, mais également pour mettre à jour des données.
  • Or, on ne peut pas écrire dans un fichier. En revanche, rien n'empêche que ce fichier crée initialement des objets en mémoire et contienne des fonctions de manipulation de ces objets.
  • Ces fonctions sont exportées et donc accessibles aux services.
  • Au final, il y aura donc 2 ensembles d'objets : un représentant la source de données, un représentant les données manipulées par l'application à un instant t
  • Mais quand l'API sera pleinement opérationnelle, il ne restera plus que le second ensemble.

 

  • L'inconvénient de cette technique est qu'il faut créer des objets dont le format correspond à ce que l'API renvoie et que cela peut être fastidieux, surtout si on écrit directement en JSON.
  • Pour faciliter cette tâche, on peut créer des classes représentant ces objets pour ensuite les instancier.
  • Le second inconvénient est qu'il faut en gros écrire des fonctions qui feront un peu la même chose que celles de l'API, mais dans aller chercher/mettre à jour dans une BdD. C'est donc du travail en plus.
  • Mais comme le développement de l'API se fait généralement en parallèle du front, en pratique, on est pas obligé d'écrire absolument toutes les fonctions nécessaires pour simuler l'API.

 

  •  Afin d'implémenter la source de données de façon structurée, on va créer 3 fichiers :
    • model.js : contient des classes pour représenter les données reçues de l'API
    • data.js : contient des objets et tableaux d'objets instancié grâce aux classes
    • controller.js : contient les méthodes qui "simulent" l'accès à l'API et qui renvoient des données créées dans data.js

 

  • Télécharger l'archive [ datasource.tgz ] dans src, puis la décompacter. Cela crée un sous-répertoire datasource, avec dedans les fichiers suivants : model.jsdata.jscontroller.js.

 

Remarques :

  • controller.js ne contient qu'une seule méthode pour obtenir le tableaux des villes. Les autres seront ajoutées au cours du TP.
  • cette méthode ne renvoie pas directement le tableau mais un objet structuré selon le principe exposé au début de la section 2.1

 

 2.3°/ La structuration des données pour le front.

  • Une application SPA peut assez fréquemment faire appel à une API qui renvoie plus de données que nécessaire pour le front, ou bien différentes API qui utilisent différentes structurations des données renvoyées.
  • Ces différences de format peuvent être source de bug assez facilement, surtout si l'API est modifiée par la suite.
  • C'est pourquoi on fait du découplage avec un front qui utilise sa propre structuration des données.
  • Cela implique plusieurs choses :
    • que l'on définisse des classes pour représenter les données manipulées en interne par le front,
    • que ces classes contiennent des méthodes permettant de "convertir" des données JSON reçues en objets, et inversement.
    • que l'on extrait de ces objets les données nécessaires pour interroger les API. Cela peut se faire à différents endroits du code, par exemple dans le store ou encore dans les fonction de service.
  • L'autre avantage d'écrire des classes est que l'on peut définir dans ces classes des méthodes de manipulation de ces objets, notamment pour mettre à jour leurs attributs.
  • Cela est utile notamment quand la modification d'un objet ne nécessite pas de faire immédiatement une requête à l'API pour sauvegarder le changement.

Remarques :

  • Quand une application ne s'adresse qu'à une seule API dont on a la maîtrise du développement, il n'est pas strictement nécessaire d'utiliser un format interne de représentation des données. Dans ce cas, le front stocke et manipule directement les données reçues via l'API. Mais on perd tous les avantages des classes et du découplage des données entre front et back.
  • Dans ce TP, on va "forcer" le fait que le front utilise un format différent de celui de l'API. Par exemple, les attributs des objets envoyés par l'API sont en anglais et en français pour le front, et certains attributs vont être organisés différemment.

 

  • Pour ce TP, on va créer des classes pour représenter en interne les personnages, les villes, les rues et les boutiques.
  • Pour cela, copier dans services le fichier téléchargeable : [ data.service.js ]

Remarques :

  • Pour l'instant, ces classes ne contiennent  qu'un constructeur et les fonctions qui permettent de faire la conversion entre structure locale et API.
  • Cependant, on verra qu'il est pratique d'ajouter des méthodes à ces classes, lorsque l'on veut changer l'état d'objets au niveau du front, sans pour cela avoir besoin de passer par une requête sur la source de données.

 

3°/ Une première application

 

3.1°/ Objectif

  • L'objectif de cette version est de pouvoir afficher :
    • une page contenant la liste des villes au format JSON
    • une page contenant la liste des personnages au format JSON
  • L'affichage de l'une ou l'autre est pilotée grâce à vue-router, en fonction de "routes" (c.a.d. l'URL) que l'on utilise. En l'occurrence, si on tape comme URL :
    • localhost:8080/towns, on affiche la liste des villes,
    • localhost:8080/persos, on affiche la liste des personnages.
  • Pour réaliser cet objectif, il faut donc écrire un fichier pour configurer vue-router à cet effet.

 

  • La liste des villes et des personnages est normalement stockée en Bdd et accessible via l'API.
  • Mais dans ce TP, elles sont en mémoire, créées par le script data.js qui se trouve dans le répertoire datasource.
  • L'application doit donc récupérer les deux listes grâce à des méthodes de service.
  • La question fondamentale est : où va-t'on stocker ce que l'on récupère ?
  • Réponse : soit dans les données locales d'un composant, soit dans le dépôt central (= le store) créé grâce à vuex.
  • Choisir entre les 2 solutions est simple :
    • si des données ne sont manipulées que par un seul composant de l'application, on peut stocker dans les données locales du composant
    • sinon, on stocke dans le store.
  • Dans ce TP, on va utiliser le store pour presque tout stocker, notamment les deux listes, afin de comprendre un peu comme il fonctionne. Mais en réalité, le store pourrait uniquement contenir le personnage et la boutique couramment sélectionnés.

 3.2°/ Mise en place initiale

  • effacer les fichiers dans components, et dans views.
  • télécharger l'archive [ vuejs-tp1-src.tgz ] dans le répertoire src, puis décompactez-la.
  • cette archive va créer/écraser les fichiers suivants :
    • App.vue
    • views/PersosView.vue et views/TownsView.vue
    • router/index.js
    • store/index.js
  • Si vous avez fait correctement ces étapes, Idea devrait recompiler les nouvelles sources sans erreur.
  • En revanche, la page affichée ne comporte plus que le bandeau du haut.
  • Vous pouvez tester les URLs indiquées plus haut. Celle qui affiche les villes affiche un gros objet JSON, alors que celle des personnages met juste un titre : normal car le code qui récupère les personnages n'est pas écrit.

Quelques explications sur ces fichiers :

store/index.js :

  • un store est créé grâce à l'intégration du plugin vuex dans le projet.
  • C'est un objet avec différents attributs permettant de créer un "dépot" centralisé de données, accessible à tous les composants.
  • Les attributs utilisé par le store dans ce TP sont :
    • state : définit les variables que l'on veut centraliser dans le store. N'importe quel composant pourra y avoir accès.
    • mutations : définit des fonctions synchrones permettant de mettre à jour le state. En effet, on ne modifie jamais directement le state.
    • actions : définit des fonctions asynchrones permettant de mettre à jour le state, via les mutations. En effet, quand le store doit récupérer des données via une API, cela suppose l'appel à des fonctions asynchrones dont le résultat arrivera ... quand il arrivera. Dans ce cas, il faut obligatoirement définir une action, qui fait les appels asynchrones, puis, une fois les données récupérées, utilise les mutations.
  • NB : les fonctionnalités basique et avancées de vuex seront abordées dans un cours dédié

 

router/index.js :

  • le routeur est créé grâce à l'intégration du plugin vue-router dans le projet.
  • ce routeur est configuré grâce au contenu du fichier index.js, qui définit des "routes".
  • une route est, au minimum, définie par un chemin (comme ceux pour accéder à un fichier), par exemple /towns, et un composant qui va être inséré dans le DOM lorsque l'on "suit" la route.
  • le composant sera inséré dans l'application à l'endroit où se trouve la balise <router-view>.
  • de ce fait, on a l'impression que l'application est multi-page, comme en PHP, alors qu'en fait, il n'y a qu'une seule page (SPA) dont on change certaines parties en fonction de la route suivie.
  • Par exemple, si on analyse le fichier index.js, on voit que demander au navigateur l'URL localhost:8080/towns va déclencher la création d'une instance du composant TownsView et son insertion dans le DOM à la place de la balise <router-view> utilisée dans App.
  • S'il y avait déjà un composant inséré à cet emplacement, il est détruit et remplacé par le nouveau.
  • NB : les fonctionnalités basique et avancées de vue-router seront abordées dans un cours dédié

 

App.vue :

  • à la fin du template, il y a une balise <router-view>. Comme indiqué ci-dessus, cela indique à vue-router l'emplacement où il doit insérer un composant en fonction de la route suivie.
  • mapActions() (importé au début de <script>) est une fonction fournie par vuex. Cette fonction permet de créer automatiquement des méthodes vuejs (donc ajoutées à l'attribut methods).
  • mapActions() prend en paramètre un tableau contenant le nom des actions du store que l'on veut pouvoir appeler dans App.vue. Pour chaque nom, mapActions() va créer une méthode "relai" du même nom pour le composant, qui peut ensuite l'utiliser dans sa partie script pour appeler l'action du store.
  • En l'occurrence, mapActions() crée une méthode nommée getAllTowns(). Quand App appelle cette méhode, cela appelle la fonction getAllTowns() du store.
  • A la fin, on définit une fonction mounted(). Cette fonction sera appelée automatiquement par vuejs chaque fois que le composant est inséré dans le DOM. Dans le cas présent, cela n'arrive qu'une fois, lorsque l'on charge l'application dans le navigateur.

TownsView.vue :

  • pour afficher la liste des villes stockée dans le store, on utilise cette fois la fonction mapState() qui va créer une variable calculée (donc ajoutée à l'attribut computed) pour chaque nom donné dans le tableau en paramètre.  Les noms doivent être ceux des variables du store, donc se trouvant dans la partie state. Les variables calculées ainsi crées portent le même nom que celles du state, et ont la même valeur.
  • en l'occurrence, mapState() crée une variable calculée nommée villes, qui a la même valeur que la valeur de villes définie dans le state du store.

 

3.3°/ Affichage des personnages

NB : Cette partie est à réaliser en autonomie et vous devez intervenir sur plusieurs fichiers.

controller.js :

  • importer le tableau characs (exporté par data.js),
  • créer une fonction getAllCharacs() qui fait la même chose que getAllTowns() mais avec characs,
  • ajouter cette fonction à l'export. 

 

persos.service.js : il faut écrire un code très similaire à celui de towns.service.js, avec comme différences :

  • importer la classe Perso,
  • définir les fonctions getAllCharacsFromLocalSource() et getAllCharacs().
  • la première fait appel à LocalSource.getAllCharacs().
  • la deuxième transforme les données reçues en utilisant Perso.fromAPI(),
  • exporter getAllCharacs().

 

store/index.js :

  • importer le service des personnages (par ex, sous le nom CharacService),
  • définir la fonction getAllCharacs() de façon similaire à getAllTowns(), excepté que l'on utilise CharacService, et que pour mettre à jour l'état du store, on utilise la mutation udpatePersos.

 

views/PersosView.vue :

  • utiliser le même principe que dans TownsView.vue, mais mapState() permet d'accéder au tableau persos du store et de l'afficher dans la partie template.

 

App.vue :

  • ajouter 'getAllCharacs' au tableau passé en paramètre à mapActions(),
  • appeller getAllCharacs() dans mounted().

 

  • Si vous avez écrit correctement ces différents codes, l'URL localhost:8080/persos devrait permettre d'afficher la liste des personnages sous la forme d'un objet JSON.

 

4°/ Conclusion

  • On remarque que la structure générale de l'application permet d'ajouter facilement des fonctionnalités de manière incrémentale. Qui plus est, on part souvent de code existant qui peut être copié/modifié pour les développer.
  • Sans une structuration de ce type et les plugins utilisés, le développement du front serait moins découplé de celui du back, l'apparition conditionnelle des composants serait compliquée à mettre en place, et la gestion des données partagées entre composants serait plus que fastidieuse.
  • Il n'y a au final que l'aspect purement graphique apporté par vuetify qui soit un peu superflu pour réaliser correctement une application SPA. D'ailleurs les TPs suivants ignorent totalement cet aspect (sauf celui sur vuetify)