1°/ Créer un projet vue v3 avec Vite

  • Vite est un environnement de développement créé par le concepteur de vuejs, afin d'accélérer le développement de projet Web.
  • Il permet essentiellement de gérer la compilation des composants d'une application de façon "paresseuse",  c'est-à-dire uniquement si le composant devient nécessaire.
  • Comparé à webpack, le gain de temps au lancement de l'application peut être prodigieux notamment quand il y a beaucoup de composants à compiler.
  • De plus, Vite fonctionne également avec d'autres framework JS, tels que React.

 1.1°/ Création initiale

  • La façon la plus simple de commencer un projet avec vite est de lancer la commande :
npm create vite@latest
  • Ensuite, il suffit d'indiquer le nom du répertoire racine du projet, quel type d'application : Vue, et quel langage (par ex, javascript).
  • Comme indiqué dans le résultat, il faut ensuite aller dans le répertoire du projet et installer tous les paquets node : npm install.

1.2°/ Installer les plugins

  • Le problème de cette méthode est qu'elle ne permet pas d'intégrer directement les plugins tels que vue-router et pinia (c.a.d. le successeur de vuex).
  • Cela dit, il suffit de les installer puis de modifier le fichier de "lancement" de l'application.
  • Dans le répertoire racine du projet, taper :
npm install vue-router
npm install pinia
  •  Attention, pour vue 3, il faut avoir la v4 de vue-router. Afin de vérifier si tout est bien installé : npm ls
  • Il suffit de vérifier que la version de vue >= 3.0.0 et que vue-router >= 4.0.0, comme c'est le cas dans l'exemple ci-dessous.
npm ls
testvue3@0.0.0 ...
├── @vitejs/plugin-vue@4.0.0
├── pinia@2.0.33
├── vite@4.1.4
├── vue-router@4.1.6
└── vue@3.2.47

1.3°/ mettre en place vue-router

  • Afin de retrouver une structure identique à celle des projet en v2, on crée un répertoire router pour contenir la définition des routes un répertoire views, pour contenir les composants qui sont affichés dans les balises <router-view>.
  • Ensuite, on crée le fichier router/index.js avec dedans :
import {createRouter, createWebHistory} from 'vue-router'
// import des composants. ATTENTION, mettre le chemin complet avec extension, par ex ../views/MyView.vue

const routes = [
    // définition des routes, comme avec vue v2   
]

const router = createRouter({
  history: createWebHistory(),
  routes, // short for `routes: routes`
})

export default router
  • On constate que l'on peut définir les routes exactement comme dans les projets avec le version 2 de vue.

 

  • Ensuite, il faut mettre à disposition le router à toute l'application vue.
  • Pour cela, il suffit de modifier src/main.js comme suivant :
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "./router";

createApp(App).use(router).mount('#app')

 

1.4°/ Mettre en place pinia

  •  Afin de retrouver une structure identique à celle des projet en v2, on crée un répertoire store pour contenir la définition des différentes stores.
  • Cependant, il n'y a pas de notion de modules comme dans vuex. Si l'on veut plusieurs store, qui peuvent interagir entre eux, il suffit de créer plusieurs fichiers JS, chacun créant un store.
  • Pour que tous les composants de l'application puissent accéder au store, il suffit de modifier src/main.js comme suivant :
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "./router";
import { createPinia } from 'pinia'
const pinia = createPinia()

createApp(App).use(router).use(pinia).mount('#app')
  • Ensuite, il faut créer les fichiers JS représentant les stores.
  • Exemple basique, dans store/counter.js :
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
    state: () => ({ count: 0, name: 'toto' }),
    getters: {
        doubleCount: (state) => state.count * 2,
    },
    actions: {
        increment() {
            this.count++
        },
    },
})

Remarques :

  • le premier paramètre de defineStore() est l'identifiant du store. Il doit être unique parmi tous les stores définis.
  • Cette fonction renvoie une fonction qui sera utilisée dans les composants ou d'autres stores, afin d'accéder au state, getters et actions du store ainsi défini.
  • Le nom de cette fonction est laissé libre, mais l'usage veut qu'on la nomme avec use+Nom_du_store+Store.
  • Globalement, les principes de pinia sont les mêmes que vuex, excepté le fait qu'il n'y a plus de mutations, justes des actions, qui peuvent être synchrones ou asynchrones.
  • En revanche, les getters doivent toujours être synchrones.

1.5°/ Lancer le projet

Pour lancer un serveur de développement et tester l'application, il suffit de taper :

npm run dev

 

Pour lancer la compilation d'une version de production, il faut taper :

npm run build

 Le résultat se trouve dans le répertoire dist.

2°/ Coder en vue 3

2.1°/ Définition d'un composant SFC

  • La structure d'un composant reste la même avec les parties template, script et style.
  • Globalement, le contenu de template ne change pas : les directives vue du type v-for, v-if, v-model, v-on etc restent les mêmes, à part quelques possibilités en plus. De même pour la partie style.
  • C'est la partie script qui est complètement différente.

 

  • Si on jette un oeil au composant HelloWorld généré par vite, on constate que la partie script est bien différente de celle en vue v2.
  • En voici une version légèrement modifiée :
<script setup>
  import { ref } from 'vue'

  const p = defineProps({
    msg: String,
  })

  const count = ref(0)
  console.log(p.msg)
</script>
  •  La première chose a remarqué est l'apparition d'un attribut setup pour <script>.
  • Cet attribut permet de grandement faciliter la vie du développeur lorsqu'il définit des composants de type SFC.
  • Cet attribut permet de raccourcir l'écriture de la partie <script> car Vite (ou webpack) va être capable de générer tout seul les instructions "manquantes".
  • A titre d'exemple, voici ce qu'il faudrait écrire sans utiliser l'attribut setup :
<script>
  import { ref } from 'vue'
  export default {
    props: { msg: String },
    setup(props) {
      console.log(props.msg)
      const count = ref(0)

      // expose count pour le template
      return {
        count
      }
    }
  }
</script>
  • Même dans ce simple exemple, on voit tout de suite ce qu'apporte setup en facilité d'écriture.

 

  • Si un composant a besoin d'un ou plusieurs autres composants/fichiers, le principe d'importation/utilisation change également un peu, surtout si on utilise Vite comme builder.
  • En effet, par défaut, Vite impose d'utiliser des chemins vers le composant/fichiers avec son extension, et soit absolus dans le système de fichiers, soit relatifs au composant qui veut importer.
  • En revanche, il n'y a plus besoin de champ components pour spécifier que l'on veut utiliser un composant importé dans la partie template. On l'utilise directement.
  • Par exemple, s'il existe un composant components/MyList.vue et que views/ItemsView.vue veut l'importer et l'utiliser dans le template, on écrit :
<template>
  <MyList />
  ...
</template>
<script setup>
  import MyList from '../components/MyList.vue'
  ...
</script>

 

2.2°/ La syntaxe par "composition"

  • Même si les concepts fondamentaux d'observation de variables, de mise à jour automatique du DOM, ... sont toujours là, la syntaxe de codage change radicalement.
  • La syntaxe de vue v2 est appelée par options : la partie script est définie grâce à des "options" telles que props, data, methods, computed, ... qui sont en fait les champs d'un objet définissant le composant.
  • En v3, même si au final on définit également un objet décrivant le composant, on "compose" cet objet comme si on écrivait un script JS, en définissant des variables, des fonctions et des instructions manipulant ces variables et fonctions.
  • C'est la raison pour laquelle il n'y a plus besoin d'utiliser this. comme préfixe chaque fois que l'on manipule une variable ou une fonction du composant.
  • En revanche, ce n'est pas juste un script JS et pour mettre en place les principes fondamentaux : variables observées, computed, watcher, props, etc. vue v3 impose l'utilisation de fonctions décrites dans les section suivantes.

 

  • Cette nouvelle syntaxe apporte également la notion de "composable". Un composable est une fonction permettant de faire un traitement réutilisable dans plusieurs composants, mais qui ne se base pas sur l'état de ces composants pour faire son traitement.
  • Un composable peut en revanche renvoyer un objet réactif au composant qui l'utilise. C'est même souvent le cas.
  • Qui plus est un composable peut en utiliser un autre, comme dans l'exemple de la documentation vue, qui présente un composable permettant de mettre en place l'écoute des événements souris et leur traitement avec un callback, et un autre composable qui l'utilise, afin de récupérer les coordonnées x,y de la souris. Cet exemple est utilisé dans la démonstration téléchargeable.
  • Cela dit, on peut parfaitement se passer des composables, même si c'est bien pratique pour réduire le volume de code des parties script et pour réutiliser du code dans différents composants.

2.3°/ La réactivité

  • Comme en vue 2, un composant définit des variables locales qui sont observées et donc réactives, au sens où leur changement de valeur provoque automatiquement des traitements tels que le rafraîchissement du DOM.
  • En revanche, la déclaration des ces variables locales se fait différemment, grâce aux fonctions : reactive et ref. Ces 2 fonctions doivent être importées pour être utilisée dans <script>
  • La différence entre les 2 est que reactive permet uniquement de définir un objet ou un tableau réactif, alors qu'avec ref, c'est une valeur de n'importe quel type. Cependant, cette valeur est quand même encapsulée dans un objet, dont le champ value est affecté avec cette valeur.
  • Quand on veut manipuler un variable créée avec ref dans <script>, il faut donc utiliser nom_var.value pour accéder à sa valeur. En revanche, dans le <template>, on peut directement utiliser son nom.
  • Exemple :
<script setup>
  import {ref, reactive} from 'vue'
  const maref = ref(23);
  console.log(maref.value) // affiche 23
  const mareact = reactive({count :1})
  console.log(mareact.count) // affiche 1
</script>
<template>
  {{ maref }} = 23
  {{marect.count}}  = 1
</template>

 

Remarque : pour les cas d'utilisation plus complexes (par ex, une ref utilisée dans une reactive), se référer à la documentation.

 

2.4°/ props

  • Comme indiqué plus haut, si on utilise une partie <script setup>,  la déclaration de props se fait par l'intermédiaire d'une fonction : defineProps(). Rq : pas besoin d'importer cette fonction.
  • L'objet fournit en paramètre à cette fonction est le même que celui que l'on définirait avec vue v2 : un objet avec des noms de props et leur type.
  • Parfois, donner le type ne suffit pas et l'on voudrait plutôt voir si la valeur est correcte. Dans ce cas, il est possible de fournir une fonction à la place d'un type. Cette fonction fait office de validateur et doit renvoyer true ou false en fonction de si la valeur est bonne ou non. Si ce n'est pas le cas, un message sera affiché dans la console, comme quand le type ne correspond pas.

 

  • Quand on veut manipuler dans la partie template une props, on fait comme en v2, directement avec le nom de la props.
  • En revanche, quand on veut manipuler la props dans la partie script, il faut forcément utiliser l'objet retourné par defineProps(), comme on peut le voir dans l'exemple de la section 2.1.

2.5°/ computed

  • Pour définir une variable calculée (c.a.d. computed), il faut importer puis utiliser la fonction computed().
  • Cette fonction prend simplement en paramètre une fonction callback qui calcule un résultat. Ce résultat est une variable réactive de type ref.
  • Si cette fonction utilise elle-même des variables réactives, chaque changement de valeurs de celles-ci provoquera un recalcul de la variable calculée, comme en v2.
  • Comme la variable calculée est de type ref, on peut directement utiliser son nom dans le template.

2.6°/ Les fonctions "normales"

  • Plus besoin de créer un attribut methods contenant les fonctions "normales" du composant.
  • Il suffit de les déclarer comme dans un script, avec le mot clé function
  • Elles sont ensuite directement appelable dans la partie template
  • Par exemple :
<scrip setup>
  const count = ref(0)
  function setCount(value ) {
    count.value = value
  }
  ...
<script>
<template>
  ...
  <button @click="setCount(0)">Reset counter</button>
  ...
</template>

 

2.7°/ watcher

  • Pour définir un watcher, il faut importe puis utiliser la fonction watch().
  • Les possibilité sont plus étendues qu'en v2. En effet, on peut observer les changements non pas d'une simple variable mais d'une source, qui peut être une variable, un tableau, le résultat d'une fonction, du moment que cette source est réactive.
  • Le premier paramètre de watch() est la source, et le deuxième est la fonction callback qui est appelée lors d'une modification de la source. Cette fonction prend en paramètre la nouvelle valeur de la source et éventuellement l'ancienne valeur.

ATTENTION : même si les champs d'un objet définit via reactive() sont observés, ils ne sont pas la "source" de la réactivité. Ils ne peuvent donc pas être utilisés comme source. L'astuce consiste à utiliser une fonction qui retourne ces champs. Par exemple :

<script setup>
  import {reactive, watch} from 'vue'
  const o = reactive({name:'toto', age:15})
// on ne peut pas observer directement o.age => on utilise une fct qui renvoie sa valeur
  watch( ()=> o.age, (new, old) => console.log("new value : "+new+", old = "+old))
  ...
</script>

 

2.8°/ Les événements

  • Le principe de "capture" d'un événement, soit natif, soit utilisateur, se fait exactement comme en vue 2, à savoir en utilisant v-on ou son raccourci @, dans la balise qui instancie le composant qui émet l'événement.
  • Par exemple, supposons un composant CompoB qui envoie un événement nommé myevent. CompoA instancie CompoB et veut récupérer l'événement. Dans ce cas, dans CompoA.vue on écrit :
<template>
  ...
  <CompoB @myevent="..."></CompoB>
  ...
</template>

 

  • Pour émettre un évenement depuis la partie <template> d'un composant, le principe reste également le même. On utilise la fonction $emit(). Par exemple, dans CompoB.vue, on écrirait
<template>
  ...
  <button @click="$emit('myevent',value)> Ok </button>
  ...
</template>

 

  • En revanche, pour émettre un événement depuis la partie <script>, le principe est différent.
  • En effet, il faut déclarer l'événement grâce à defineEmits(). Cette fonction prend en paramètre un tableau de nom d'événement, ou bien un objet décrivant l'événement, et renvoie une fonction permettant d'envoyer ces événements. L'usage veut que l'on appelle cette fonction emit.
  • Par exemple :
<script setup>
  ...
  const emit = defineEmits(['myevent'])
  ...
  emit('myevent', 10)
  ...
</script>
  •  Dans cet exemple, on déclare juste le nom de l'événement. Grâce à la syntaxe par objet, il est possible d'associer au nom de l'événement une fonction de validation. Celle-ci sert généralement à tester des conditions pour émettre l'événement ou non. Elle doit retourner true ou false en fonction de si les conditions sont bonnes ou pas.
  • Par exemple :
<script setup>
  ...
  const emit = defineEmits({
    myevent: val => { 
      if (val%2===0) return true 
      else { console.log("invalid value"); return false }
    }
  })
  ...
</script>

 

2.9°/ Le cycle de vie vuejs

  • En v3, les fonctions mounted(), created(), ... ont disparu. Elles ont été remplacées par d'autres qui prennent en paramètre un callback.
  • Par exemple, il y a onBeforeMounted(), onMounted(), onUpdated() ...
  • Il existe également une fonction nextTick() qu'il est possible d'importer est d'utiliser pour attendre la mise à jour du DOM.
  • En conclusion, pour gérer le cycle de vie, c'est le même principe qu'en v2 mais avec une syntaxe légèrement différente.
  • Par exemple :
<scrip setup>
  import {nextTick, onMounted } from 'vue'
  const v = ref(0)
  onMounted( () => v.value = 50 )
  function increment() {
    v.value++
    nextTick(() => { ... do something ...  })
  }
  ...
<script>
  

 

2.10°/ Et le reste ?

  • Globalement, les slots, les directives, l'injection, ... fonctionnement de façon identiques à la v2, à part quelques petites différences et/ou possibilités supplémentaires. 

 

3°/ Démonstration

  • Le code de démonstration est téléchargeable [ ici ]
  • Il contient uniquement le répertoire src.
  • Explorer le code du router et du store pinia : c'est quasi identique à une code en v2 avec vuex.
  • Explorer le code des différents composants : App, TestView, WelcomeView et HelloWorld. Ils illustrent toutes les nouvelles syntaxes exposées en section 2.1 -> 2.9