Try   HackMD

Développer une application VueJS

The Progressive JavaScript Framework

© Julien Noyer - All rights reserved for educational purposes only


Introduction

Vue.js (aussi appelé plus simplement Vue), est un framework JavaScript open-source utilisé pour construire des interfaces utilisateur et des applications Web. Vue.js a été créé par Evan You et est maintenu par lui et le reste des membres actifs de l’équipe principale travaillant sur le projet et son écosystème.

Source Wikipedia.org

Environement de travail

La méthode la plus communément utilisée pour développer des applications avec Vue.js est celle qui consiste à utiliser une suite de CLI nommée Vue CLI qu'il est possible d'installer avec la commande suivante :

npm install -g @vue/cli

Les instructions qui suivent dans ce document sont basées sur l'utilisation de Vue CLI pour simplifier la démonstration des notions qu'il aborde.


Créer un projet Vue.js

Utilisation de base de l'interface d'invite de commande de Vue.js

Démarrer un nouveau projet Vue.js

Pour bien débuter le développement d'une application avec Vue.js il est important de respecter une structure et de correctement organiser les différents composants qui seront utilisés dans l'application. Il est donc important de sélectionner un dossier spécifique sur la machine locale dans laquelle sera développée l'application Vue.js.

Nous sélectionnons le dossier contenant nos différents projets pour y créer notre application Vue.js avec la commande suivante :

vue create vue-app

Définissez les options que vous souhaitez et laissez le CLI créer l'application

Le CLI de Vue.js installe des composants de bases lors de la création du nouveau projet, ce document va volontairement ignorer ces composants et présenter la mise en place d'une application à partir des fichiers de configuration de base.

Liste non-exhaustive commandes de Vue CLI

Avant de taper ces commande, nous vous conseillons de régarder la structure du fichier package.json dans lequel plusieurs scripts sont présents.

# Create new application
vue create vue-app

# Launch application in local server
vue-cli-service serve

# Build application
vue-cli-service build

# Checking All Available Commands
npx vue-cli-service help

Toutes les commandes sont disponible sur le site officiel de Vue.js : https://cli.vuejs.org/guide/cli-service.html


Gestion des routes de l'application

Mise en place d'une structure de base d'application Web

Associer des composants aux endpoints

Contrairement à des routes serveur, celles utilisées dans Vue.js n’ont pas pour but de correspondre à des requêtes HTTP mais plutôt d’utiliser les endpoints des URL, pour afficher dans le DOM un composant spécifique. Dans la mesure ou une application Vue.js s’affiche dans une seule, est unique page HTML, il s’agit donc d’être capable d’afficher les bons éléments du DOM pour le bon endpoint.

Avant de configurer le routeur de notre application, nous allons dans un premier temps créer des composants qui correspondront aux différentes routes de notre application. il est important de noter à cette étape que nous considérons le composant d’une route comme un composant principal dans lequel nous intégrerons d’autres composants.

Avant de créer nos composants principaux, nous allons structurer notre application en créant dans le dossier src un dossier nommé views dans lequel nous créerons les composants des routes :

mkdir src/views

Une fois ce dossier créé nous pouvons générer nos composants principaux avec la commande suivante :

touch src/views/HomeView.vue

Nous allons à présent éditer le fichier HomeView.vue que nous venons de créer pour y placer le code suivant :

<template> <div class="home-view-component" /> </template> <script> export default { name: 'HomeView', components: { }, data(){ return {} }, methods:{}, created(){}, mounted(){} } </script>

La structure présentée ci-dessous vous servira de base pour créer les autre vues de votre application, pensez à modifier l'attribut name.

Configurer le router

Le principe de routeur pour une application Web est un fichier de configuration qui permet de définir le composant à utiliser selon le endpoint invoqué dans le navigateur.

En Vue.js ce fichier est structuré d'une certaine manière, avec des composants spécifique, et il doit être placé dans un dossier nommé router.

Pour initier le router de votre application, tapez les commandes suivantes :

# Créer la structure du router
mkdir src/router
touch src/router/index.js

# Installer la dépendance `vue-router`
npm i vue-router

Nous allons à présent éditer le fichier index.js que nous venons de créer pour y placer le code suivant :

/* Imports and config */ // Vue import Vue from 'vue'; import VueRouter from 'vue-router'; Vue.use(VueRouter); // /* Router definitions */ // Routes collection const routes = [ { path: '/', name: 'HomeView', component: () => import('../views/HomeView.vue'), }, ] // Create router const router = new VueRouter({ mode: 'history', routes }) // /* Export Router */ export default router //

Pour créer d'autres vues, il suffit d'ajouter un objet dans la constante routes et en éditant les propriétés.

Implémenter le router dans l'application

Les éléments que nous venons de créer permettent de définir le comportement de notre application selon le endpoint invoqué dans le navigateur, il nous faut à présent indiquer où nous souhaitons que les vues s'affichent dans notre application.

Nous allons tout d'abord éditer le fichier main.js qui est le point d'entrée principal de votre application. Ce fichier est utilisé pour définir la configuration générale de l'application, c'est ici que nous devons importer notre router de la manière suivante :

/* Imports */ // Vue import Vue from 'vue'; // Inner import App from './App.vue'; //=> Import app router import router from "./router"; // /* Set configuration */ Vue.config.productionTip = false; // /* Create app */ new Vue({ store, //=> Include app router router, render: h => h(App), }) .$mount('#app'); //

Dernière étape de l'implémentation du router de notre application, nous devons à présent définir le fichier dans lequel nous souhaitons afficher les composants des vues.

En principe, et dans la grande majorité des cas, c'est le fichier principal qui est utilisé. En Vue.js, il s'agit du fichier App.vue que nous allons éditer de la manière suivante :

<template> <div id="app"> <main> <!-- Use router-view directive to display route views --> <router-view :key="$route.fullPath"/> </main> </div> </template> <script> /* Define component */ export default { name: 'App', data(){ return{} }, methods: {}, created(){}, mounted(){} } // </script>

Les propriétés $route et $router sont disponibles dans les composants, elles permettent de récupérer des informations sur les routes et le router.

Créer des liens entre les vues

L'exemple le plus probant à expliquer la mise en place de liens dans une application Vue.js est la mise en place d'une navigation.L'exemple le plus probant à expliquer la mise en place de liens dans une application Vue.js est la mise en place d'une navigation.

La mise en place de lien avec Vue.js est très proche des autres cadritiels, il s'agit d'utiliser une directive spécifique qui permet à Vue.js de gérer le passage entre les vues. La directive à utiliser est router-link, elle s'utilise de la façon suivante :

<router-link to="/"> Home page </router-link>

La directive router-link dispose d'options présenté sur le lien suivant : https://router.vuejs.org/api/#router-link


Gestion des composant de l'application

Mise en place des composant de base d'une application Web

Principe de construction en "poupée russe"

Le principe de développement à observer dans la mise en place d'une application Web et celui des poupées russes : nous imbriquons des composants les uns dans les autres. Cette méthode de travail est issue des bonnes pratiques de développement Web mises en place depuis plusieurs années, la "disruption" n'a pas en ce domaine son mot à dire. En effet, dans la mesure où le développement via des cadritiels induit une notion d'abstraction supplémentaire et une complexité due à la structure du cadritiel, les bonnes pratiques sont dans ce cas l'unique solution pour réussir à comprendre rapidement les notions du cadritiel.

Nous allons donc prendre en compte deux familles de composants : les composants parent et le composants enfant. Les spécificités de ces composants sont définies dans les chapitres ci-dessous.

Définition d'un composant "parent"

Un composant parent est celui qui représente la vue affichée dans le navigateur, c'est-à-dire un composant qui présente d'autres composants afin de créer la mise en page d'une vue. Pour faire le rapprochement avec PHP, une vue serait le fichier index.php dans lequel des fonctions include permettent de charger un autre fichier PHP.

Nous avons déjà créé précédemment la vue HomeViex.vue que nous avons placé dans le dossier views, nous vous conseillons de toujours nommer le dossier de vos composants parent de cette manière.

Qu'il soit parent ou enfant, les composants Vue.js ont toujours la même structure. Il sont constitués d'un template, d'un script et en option d'un style. Nous allons à présent reprendre le fichier HomeView.vue afin d'indiquer l'utilité de chacun des éléments d'un composant :

<template> <div class="home-view-component" /> </template> <script> export default { // Used to identify the component name: 'HomeView', // Used to import child component components: { }, // Used to inject value in the classe data(){ return {} }, // Used to inject methods in the class methods:{}, // Hook called when component is created created(){}, // Hook called when component is mounted mounted(){} } </script>

Définition d'un composant "enfant"

Un composant enfant peut être utilisé dans plusieurs composants parent différents. Ils sont en règles générale constitué de peu de code, l'objectif étant d'avoir des composants enfant les plus réutilisables possibles, c'est pourquoi il est conseillé d'avoir une idée générale assez précise des composants enfant à créer pour une application.

Les composants enfant doivent être placé dans le dossier components à l'intérieur du dossier src de votre application. Vous avez tout le loisir d'organiser à l'intérieur du dossier components des sous-dossiers pour les composants que vous allez créer.

Nous allons prendre en exemple de composant enfant celui qui va nous permettre de mettre en place la navigation de notre application. Nous allons commencer par créer notre composant avec la commande suivante :

touch src/component/HeaderApp.vue

Nous allons profiter de la mise en place de notre navigation pour définir certaines fonctionnalités importantes. Nous ouvrons donc le fichier HeaderApp.vue pour y ajouter le code suivant :

<template> <!-- Use v-if attribute for DOM condition --> <header class="header-app-component" v-if="title !== undefined" > <!-- Use @click attribute to bind event --> <h1 @click="onClickTitle" > <!-- Use the double bracket to display property value --> {{ title }} </h1> <nav> <ul> <!-- Use v-for attribute for DOM loop The key attribute is mandatory --> <li v-for="(item, i) of navLinks" :key="'item-' + i" > <!-- Use :ATTR to bind variable value --> <router-link :to="item.path"> {{ item.name }} </router-link> </li> </ul> </nav> </header> </template> <script> export default { name: 'HeaderApp', // Used to bind value from parent component props: { title: { type: String, required: false, default: undefined, }, }, data(){ return { // Create a collection navLinks: [ { name: 'Home page', path: '/' } ] } }, methods:{ // Create method to bind click event onClickTitle: function(){ // The $emit property is used to create custom events this.$emit('User clicked on title'); } }, created(){}, mounted(){} } </script>

Utiliser un composant "enfant" dans un composant "parent"

Un composant enfant peut être utilisé dans plusieurs composants parent, contrairement à un composant parent qui lui n'est utilisé qu'une seule fois. Nous allons à présent utiliser le composant HeaderApp pour illustrer la méthode qui permet d'intégrer un composant enfant.

Le rôle de notre composant HeaderApp est d'afficher la navigation de notre application, la règle veut qu'elle soit placée toujours au même endroit pour permettre aux utilisateurs de se repérer facilement. Nous allons donc éditer le composant principal de notre application - le composant parent le plus éloigné - pour intégrer notre composant.

Nous ouvrons donc le fichier App.vue pour l'éditer de la manière suivante :

<template> <div id="app"> <!-- Use child components like they are HTML tags ":title" = Inject value in child component "@clickTitle" = Bind event from child component --> <HeaderApp :title="mainTitle" @clickTitle="onClickTitle" /> <main> <router-view :key="$route.fullPath"/> </main> </div> </template> <script> /* Import component */ import HeaderApp from "./components/app/HeaderApp"; // /* Define component */ export default { name: 'App', // Used to inject child components components: { HeaderApp }, data(){ return{ // Define properties mainTitle: 'My first Vue.js application' } }, methods: { // Create method to bind data from child component onClickTitle: function(event){ console.log('From HeaderApp', event) }, }, created(){}, mounted(){} } // </script>

Chaque attribut de balise, lorsqu'ils sont plusieurs, doivent être chacun sur une ligne différente.


Gestion de l'information en Vue.js

Gestion des requêtes asynchrones dans une application Web

Le principe de "Single page application"

Développer une application Web en Javascript c'est avant tout travailler sur une structure de code qui permet de nourrir un DOM avec des informations, suite à une interaction utilisateur. Ce principe nécessite à la fois de connaître l'état de l'interface graphique et celui de l'information afin de présenter dans le navigateur les éléments qui correspondent à une requête utilisateur.

Le principe théorique à mettre en place ne date pas d'hier, dans la mesure où l'informatique moderne, qui se base sur le pattern MVC, a déjà dû répondre à cette problématique. Le pattern MVC est donc la base sur laquelle nous devons baser toutes nos réflexions en terme de développement. C'est en revanche les contraintes liées au Web qui doivent diriger notre réflexion quant à l'adaptation du pattern MVC dans le développement d'une application Web.

Les différents cadritiels Front ont des techniques qui se rapprochent, tant la problématique est commune à tous les projets qui gèrent des données. C'est en la matière les équipes qui développent React qui ont mis défini un nouveau pattern qui est à présent utilisé, en Vue.js par exemple : le pattern FLUX.

Initialement développé pour résoudre une problématique de mise à jour de notifications pour des messages chat, le pattern FLUX présente une méthodologie basse sur le principe d'observable, qui permet de définir des états et d'y associer des méthodes spécifiques. En Vue.js, ce pattern est mis en place grâce au module VueX.

Mise en place d'un "Store" et gestion du CRUD

Notre application sera nourrie en informations par une API, nous devons donc être capables de récupérer ces informations grâce à des requêtes asynchrone et donc gérer des promise afin de pouvoir ensuite traiter ces informations dans notre application.

En règle générale, nous avons besoin de 4 types de requêtes : POST, GET, PUT et DELETE. Ces requêtes représentent ce que l'on appelle un CRUD, acronyme pour "Create, Read, Update, Delete". Ce support n'aborde pas la mise en place d'une API, mais la structure qu'il présente est adaptable à souhait.

Pour créer notre store en Vue.js, nous allons devoir installer le module VueX et organiser un dossier à la racine de notre application. Ce dossier doit se nommer store et contient un sous-dossier modules, nous devons ajouter un fichier index.js dans le dossier store et un fichier crud.js dans le dossier modules :

# Create store folder and main store file
mkdir /src/store
touch /src/store/index.js

# Create modules folder and CRUD store file
mkdir /src/store/modules
touch /src/store/modules/crud.js

# Install VueX modules & plugins
npm i vuex vuex-persistedstate

Vous aurez à terme plusieurs modules, c'est pourquoi il vous est fortement conseillé de respecter cette structure.

Une fois ces dossiers et fichiers créés, nous pouvons éditer le fichier main.js pour y intégrer l'utilisation du store dans l'application de la façon suivante :

/* Imports */ // Vue import Vue from 'vue'; // Inner import App from './App.vue'; import router from "./router"; //=> Import app store import store from './store/index'; // /* Set configuration */ Vue.config.productionTip = false; // /* Create app */ new Vue({ router, //=> Include app router store, render: h => h(App), }) .$mount('#app'); //

Les propriété $store est disponible dans les composants, elle permet de récupérer de manipuler le store directement dans les composants.

Configuration du fichier principal du "Store"

Comme indiqué dans le commentaire ci-dessus, le fichier index.js est le fichier principal de votre store, il vous permet de créer une structure facilement maintenable et d'organiser proprement la gestion de l'information dans votre application. Nous allons éditer le fichier index.js de la façon suivante :

/* Imports and config */ // Import Vue import Vue from "vue"; // Import VueX modules import Vuex from "vuex"; import createPersistedState from "vuex-persistedstate"; // Import App store module import crud from "./modules/crud"; // Set up Vuex Vue.use(Vuex); // /* Export store */ export default new Vuex.Store({ // Used to add App store modules modules: { crud }, // Used to add VueX plugins plugins: [ createPersistedState() ] }); //

Le plugin VueX vuex-persistedstate permet de conserver les informations de façon persistante dans la navigateur en LocalStorage.

Configuration du module "CRUD"

Nous allons à présent ouvrir le fichier crud.js pour y ajouter le code nécessaire pour gérer les différentes requêtes. Le code qui va vous être proposé est commenté pour expliquer chacune des méthodes mises en place, vous pouvez l'adapter à vos besoins mais nous vous conseillons de le conserver tel quel car il à été prévu pour s'adapter dans presque toutes les situations.

La mise en place du module "CRUD" ce fait de la façon suivante :

export default { state: { // Define states: each state is a subjet to subscribe flashnote: undefined, }, getters: { // Define getters: each states must have getters flashnote: (state) => state.flashnote, }, mutations: { // Define mutations: each states must have mutation FLASHNOTE( state, payload ){ state.flashnote = payload.data }, }, actions: { // CRUD: read one readObject(context, data){ fetch( `https://hackmd.io/${data.path}/${data.id}`, { method: `GET`, }) .then( response => response.json(response)) // Parse response .then( async apiResponse => { // Call action methond within action method }) .catch( apiError => { }) }, // CRUD: read all readAllObjects(context, data){ fetch( `https://hackmd.io/${data.path}`, { method: `GET`, }) .then( response => response.json(response)) // Parse response .then( async apiResponse => { // Call action methond within action method }) .catch( apiError => { }) }, // CRUD: create one createObject(context, data){ fetch( `https://hackmd.io/${data.path}`, { method: 'POST', body: JSON.stringify(data.content), headers: { 'Content-Type': 'application/json' }, credentials: `include` }) .then( response => response.json(response)) // Parse response .then( async apiResponse => { // Call action methond within action method }) .catch( apiError => { }) }, // CRUD: update one updateObject({context, dispatch}, data){ fetch( `https://hackmd.io/${data.path}/${data.id}`, { method: 'PUT', body: JSON.stringify(data.content), headers: { 'Content-Type': 'application/json' }, credentials: `include` }) .then( response => response.json(response)) // Parse response .then( async apiResponse => { // Call action methond within action method }) .catch( apiError => { }) }, // CRUD: delete one deleteObject(context, data){ fetch( `https://hackmd.io/${data.path}/${data.id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, credentials: `include` }) .then( response => response.json(response)) // Parse response .then( async apiResponse => { // Call action methond within action method }) .catch( apiError => { }) }, // Action to update flashnote state updateFlashNote(context, data){ // Change flashnote state value context.commit(`FLASHNOTE`, { data }) }, } }

Utilisation du store dans le composants

Que ce soit pour un composant parent ou enfant, il est possible à présent d'avoir accès aux méthodes et aux valeurs des différents modules du store que nous aurons créé. Il est important cependant de garder en tête que les informations qui permettent de gérer le contenu global d'une vue doit être capté depuis un composant parent alors que l'action qui permet de mettre à jour un état dans le store peut être appelée depuis n'importe quel type de composant.

Pour illustrer l'utilisation du store, nous allons à présent éditer le fichier HomeView.vue afin d'y intégrer les différentes techniques de manipulation de l'information avec VueX.

<template> <div v-if="flashnote" class="home-view-component" > <h1>{{ flashnote }}</h1> <!-- Change store state from the DOM --> <button @click="$store.dispatch('updateFlashNote', undefined)"> Close flashnote </button> </div> </template> <script> export default { name: 'HomeView', components: { }, data(){ return { flashnote: undefined } }, methods:{ getAllObjectFromStore: function(){ /* Call action within the store @params 'readAllObjects' : the store action name @params '{ path: 'post' }': the defined action param */ this.$store.dispatch('readAllObjects', { path: 'post' }); } }, created(){ // Subscribe to store mutations this.$store.subscribe((mutations, state) => { // Check mutation type if( mutations.type === "FLASHNOTE" ){ // Get state value from store getters this.flashnote = this.$store.getters.flashnote; } }) }, mounted(){ // Call method when component is loaded this.getAllObjectFromStore(); } } </script>

Le principe à respecter est de ne jamais modifier un state depuis un composant et de toujours passer par les getters pour connaître la valeur d'un state.


Principales techniques en Vue.js

Techniques de bases et avancées en développement d'application Web

Les conditions, le boucles et le déclenchement d'événement

Comme nous l'avons vu dans la mise en place des composants de notre application, il est possible d'utiliser des directives spécifiques en Vue.js qui permettent de gérer le DOM avec la souplesse du Javascript. Le code suivant vous récapitule ces différentes notions :

<template> <!-- Directice v-if Used to conditionnaly display HTML markup --> <div v-if="display" class="base-component" > <!-- Bouble brackets Used to display property value --> <h1>{{ flashnote }}</h1> <ul> <!-- Directice v-for Use to create loop, the key attribute is mandatory --> <li v-for="(item, i) of collection" :key="'item-' + i" > <!-- Event binding Bind all events with @ --> <button @click="item.active = !item.active"> <!-- Attribute class Used to add classes dynamicaly --> <span :class="{ 'active': item.active }" > {{ item }} </span> </button> </li> </ul> </div> </template> ...

Les variables d'environnement

Certaines valeurs que nous utilisons peuvent être relative à l'environnement dans lequel nous exécutons notre code, c'est pourquoi il est recommandé de définir des variables d'environnement pour le deux principaux environnement que nous allons utiliser : local et production. Pour ce faire, nous allons créer deux fichiers différents avec les commandes suivantes :

# Local environnement
touch .env

# Production environnement
touch .env.prod

Ces deux fichiers doivent contenir exactement les mêmes variables, le nom doit être me même alors que les valeurs sont différentes, pour correspondre à l'environnement local ou de production. Voici à titre d'exemple le contenu d'un fichier .env de base :

# SERVEUR
VUE_APP_API_URL=https://hackmd.io

Il est important de noter que le nom des variables sont en majuscule et commence par "VUE_". Le préfixe est très important, car c'est la seule manière pour Vue.js de repérer les variables d'environnement. Pour utiliser ces variables, nul besoin d'importer le fichier .env, c'est Vue.js qui s'en occupe par lui-même, il s'agit donc simplement d'utiliser les variables de la manière suivante :

process.env.VUE_APP_API_URL

La propriété process est présente dans tous es les fichiers de l'application, il faut penser à relancer la compilation lorsque des nouvelles variables sont ajoutées.

En phase de développement, Vue.js charge automatiquement le fichier .env, et sans modification du script de déploiement, Vue.js fer la même chose lors de la publication de votre application. Pour faire en sorte d'utiliser le fichier .env.prod lord de la compilation, vous devez éditer le script build du fichier package.json de la manière suivante :

"build": "vue-cli-service build --mode prod",

Le drapeau --mode suivie du nom de l'environnement permet de charger le bon fichier. Vous pouvez définir autant d'environnement que nécessaire.

Mettre en place des composant de base

Certains composants de votre application vous seront utiles à plusieurs endroits, vos boutons "CTA" par exemple, ce qui induit que devrez les importer dans tous les composants parent qui les utilisent, ce qui est loin et difficilement maintenable.

Pour faire en sorte d'utiliser plus facilement certains composants enfant dont vous aurez souvent besoin dans vos composant parent, il est conseillé de créer un dossier base dans le dossier components pour référencer ces composants.

Il est ensuite possible d'éditer le fichier main.js afin d'importer tous les composants enfant pour les injecter directement dans l'application de la manière suivante :

/* Imports */ // Vue import Vue from 'vue'; // Inner import App from './App.vue'; // /* Set configuration */ // Set up production tip Vue.config.productionTip = false; // Set up BaseApp components const requireComponent = require.context( // The relative path of the components folder './components/base', // Whether or not to look in subfolders false, // The regular expression used to match base component filenames /Base[A-Z]\w+\.(vue|js)$/ ) requireComponent.keys().forEach(fileName => { // Get component config const componentConfig = requireComponent(fileName) // Get PascalCase name of component const componentName = fileName.split('/').pop().replace(/\.\w+$/, '') // Register component globally Vue.component( componentName, componentConfig.default || componentConfig ) }) // /* Create app */ new Vue({ router, render: h => h(App) }).$mount('#app') //

Les composants de base ainsi importés s'utilisent de la même manière que les autres, sans avoir à les importer dans un composant parent.

Ressources

Index des liens vers les sujets traités dans ce support :

Support en cours de rédaction - Plus d'information : Julien Noyer