The Progressive JavaScript Framework
© Julien Noyer - All rights reserved for educational purposes only
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
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.
Utilisation de base de l'interface d'invite de commande de 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.
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
Mise en place d'une structure de base d'application Web
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
.
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.
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.
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
Mise en place des composant de base d'une application Web
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.
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>
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>
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 des requêtes asynchrones dans une application Web
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
.
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.
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 enLocalStorage
.
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 })
},
}
}
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 lesgetters
pour connaître la valeur d'unstate
.
Techniques de bases et avancées en développement d'application Web
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>
...
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.
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
.
Index des liens vers les sujets traités dans ce support :
Support en cours de rédaction - Plus d'information : Julien Noyer