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 celui d'un savant fou, Dr Mad, qui peut acheter des virus dans une boutique spécialisée, les triturer dans son laboratoire, et revendre ses inventions sur le dark web.
- 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
- Si ce n'est déjà fait, installer node, npm, serve. Pour cela, consulter l'article : support : Installation de vue-cli & config. idea
1°/ Créer le projet avec vue-cli + vue-router + vuex
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 ? Please pick a preset: (Use arrow keys) |
- Utiliser la flèche bas pour choisir "Manually select features". On arrive sur un second menu :
Vue CLI v4.5.14 ? Please pick a preset: Manually select features |
- 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°/ 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.3°/ 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 :
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 la boutique de vente de virus, ainsi que des comptes bancaires. 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 : [ shop.service.js ]
- créer dans ce répertoire un fichier vide : bankaccount.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. Par exemple, on a dans shop.service.js :
- getAllVirusesFromLocalSource() qui va chercher des données dans la source locale (cf. section suivante)
- getAllVirusesFromAPI() qu'il faudra écrire et surtout utiliser quand l'API sera fonctionnelle
- getAllViruses() 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.
- Il y a exactement le même triplet pour les fonctions permettant de se loguer à la boutique.
- 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 du côté front-end (en principe). 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, ce qui rend le code un peu plus lisible.
- 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 au minimum 2 fichiers :
- data.js : contient des objets et tableaux d'objets avec la même structure que ceux manipulés au niveau de l'API
- 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 : data.js, controller.js.
Remarques :
- controller.js ne contient que deux méthodes pour obtenir le tableaux des virus et se loguer. D'autres seront ajoutées au cours du TP.
- ces méthodes ne renvoient pas les données brutes issues de la source locale mais un objet structuré tel que décrit dans le cadre IMPORTANT de la section 2.1, qui correspond à ce que l'API renverrait si elle existait.
- c'est aussi pourquoi le champ data de la réponse ne contient pas forcément toutes les données d'un objet demandé. Par exemple, quand la fonction associée au login trouve un objet dans le tableau shopuser, il ne renvoie pas l'objet complètement mais uniquement certaines informations.
2.3°/ Intermède sur la structuration des données pour le front (pour information car non utilisé dans ce TP)
- 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 :
- Cette façon de découpler le format des données utilisé par le front de celui des données reçues de l'API n'est réellement utile quand une application ne s'adresse qu'à une seule API dont on a la maîtrise du développement.
- Dans ce cas, on manipule généralement directement les données reçues via l'API, sans les transformer. Cependant, on perd tous les avantages des classes et du découplage des données entre front et back.
- Dans ce TP, on va effectivement utiliser directement les données de l'API (ou celles de la source locale)
3°/ Une première application
3.1°/ Objectif
- L'objectif de cette version est de pouvoir afficher :
- une page contenant la liste des virus disponibles à la vente
- une page permettant de se loguer à la boutique
- 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/shop/items, on affiche la liste des items en vente, c.a.d. des virus,
- localhost:8080/shop/login, on affiche un formulaire de login classique et en dessous, les informations de l'utilisateur si l'authentification est correcte.
- Pour réaliser cet objectif, il faut écrire un fichier pour configurer vue-router à cet effet.
- En effet, il est possible dans une application vue d'utiliser un principe de "routage", basé sur l'URL demandée, pour afficher tel ou tel composant dans le navigateur.
- C'est possible puisque dans une application SPA, l'URL peut changer sans pour autant que cela provoque une nouvelle demande de page au serveur.
- Dans ce TP, toutes les données sont en mémoire, via l'import des tableau de data.js
- L'application doit donc récupérer ces données grâce aux méthodes de service.
- Les questions fondamentales sont :
- où appelle-t-on ces méthode de service ?
- où va-t'on stocker ce que l'on récupère ?
- Réponse aux 2 questions : soit dans chaque composant devant manipuler des données issues de data.js, 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, afin de comprendre un peu comme il fonctionne. Mais en réalité, le store pourrait uniquement contenir bien moins d'informations.
3.2°/ Mise en place initiale
- effacer les fichiers dans components, et dans views.
- en étant à la racine du projet, installer le module js uuid : npm install uuid
- 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/ShopLoginView.vue et views/VirusesView.vue
- router/index.js
- store/index.js
- Si vous avez fait correctement ces étapes, Idea devrait recompiler les nouvelles sources sans erreur.
- La page centrale affiche un logo et un message de bienvenu.
- Vous pouvez tester les URLs indiquées plus haut :
- localhost:8080/shop/items,
- localhost:8080/shop/login. Rq : si vous tapez un login incorrect, un message d'erreur apparaît dans la console de l'inspecteur.
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 /shop/items, 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>. En l'occurence, cette balise est placée dans le template de App.vue.
- 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/shop/items va déclencher la création d'une instance du composant VirusesView 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 getAllViruses(). Quand App appelle cette méhode, cela appelle la fonction getAllViruses() 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.
VirusesView.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 viruses, qui a la même valeur que la valeur de viruses définie dans le state du store.
3.3°/ Affichage du solde d'un compte bancaire
NB : cette partie est à réaliser en autonomie et vous devez intervenir sur plusieurs fichiers, certains devant être créés, d'autres seulement modifiés.
controller.js :
- importer le tableau bankaccounts (exporté par data.js),
- créer une fonction getAccountAmount(number). Le paramètre number est une chaîne de caractère représentant l'identifiant d'un compte( cf. champ number dans data.js). Elle doit :
- vérifier que number est défini, non vide. Sinon, elle renvoie un objet d'erreur (comme dans shopLogin)
- vérifier que number existe dans bankaccounts et si oui, renvoyer un objet dont error = 0, et data contient la valeur du champ amount. Sinon, elle renvoie un objet d'erreur (comme dans shopLogin)
- ajouter cette fonction à l'export.
bankaccount.service.js (à créer) : il faut écrire un code très similaire à celui de shop.service.js, avec comme différences :
- définir les fonctions getAccountAmountFromLocalSource(number) et getAccountAmout(number).
- la première fait appel à LocalSource.getAccountAmount(number).
- la deuxième appelle la première et renvoie soit un résultat, soit un objet d'erreur (comme dans getAllViruses() ),
- exporter getAccountAmount().
store/index.js :
- importer le service des comptes bancaires (par ex, sous le nom BankService),
- ajouter dans le state une variable accountAmount, initialisée à 0,
- en s'inspirant de updateShopUser() ajouter dans mutations, une fonction updateAccountAmount() qui permet de mettre à jour accountAmount.
- en s'inspirant de shopLogin(), ajouter dans actions une fonction getAccountAmount() qui utilise BankService.getAccountAmount()afin de récupérer le solde d'un compte bancaire
views/BankAccountView.vue (à créer) :
- copier/coller le code de ShopLoginView
- modifier le template pour n'avoir qu'un seul champ de saisie, pour mettre l'identifiant du compte et afficher le contenu de accountAmount se trouvant dans le store (cf. le mappage ci-dessous pour y accéder)
- mapper la variable du state accountAmount (au lieu de shopUser)
- mapper l'action getAccountAmount() (au lieu de shopLogin() )
- quand on clique sur le bouton, appeler la fonction getkAccountAmount()
router/index.js :
- en s'inspirant de la route /shop/login, ajouter une route /bank/amount qui permet d'afficher le composant BankAccountView.
- Si vous avez écrit correctement ces différents codes, l'URL localhost:8080/bank/amount devrait permettre d'afficher le formulaire et si vous fournissez un id correct, d'afficher le détail du compte.
3.4°/ Affichage des transactions liées à un compte bancaire
NB : cette partie est à réaliser en autonomie et vous devez modifier plusieurs fichiers.
L'objectif est de modifier BankAccountView afin de lister les transactions liées à un numéro de compte. Cela impose :
- de modifier controller.js pour ajouter la méthode qui récupère les transactions à partir d'un numéro de compte,
- de modifier bankaccount.service.js pour définir les fonctions qui vont appeler celles de controller.js
- de modifier store/index.js pour ajouter un champ de type tableau (initialisé vide) dans le state (par exemple nommé accountTransactions), ainsi que la mutation et l'action permettant de le mettre à jour
- de modifier le template de BankAccountView pour ajouter un bouton qui va utiliser l'action dans le store pour chercher les transactions (au lieu du solde), et afficher celles-ci sous forme de liste à puce (cf. VirusesView pour un exemple), uniquement s'il existe des transactions (cf. ShopLoginView et le v-if comme exemple)
Remarque : cette fonctionnalité ne nécessite pas de nouvelle route puisque c'est au travers d'interaction avec les boutons de BankAccountView que l'on peut charger les données qu'il affiche.
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 qui manque à cette première application. Pour cela, on utilise des plugins proposant des composants graphiques plus ou moins complexes, comme par exemple vuetify, qui sera abordé dans un cours dédié.