fullstack/vuejs2

partie 1 - partie 2 - partie 3

Routage des URL front-end

Problématique du routage back-end / front-end

Un serveur de production héberge le plus souvent le back-end et le front-end. S’il est destiné à être utilisé sur Internet, on souhaitera généralement que toutes les requêtes soient faites sur les ports HTTP standards 80 (http://) ou 443 (https://) afin d’éviter d’être bloqué par des pare-feux

Le serveur frontal (Apache, Nginx, Node/Express, IIS, etc.) effectue en premier une forme de méta-routage : pour chaque nom de domaine, par exemple yesno.wtf, un fichier de configuration propre à ce domaine fournit les règles qui lui permettent de distinguer les url back-end des url front-end. Par exemple l’url http://yesno.wtf/api est un url de back-end, alors que https://yesno.wtf/ ou https://yesno.wtf/assets/application-dc53a8eb45b5db278e3c1ba040413b83.css sont des url front-end ; cette sélection est faite à partir du chemin d’accès de l’url, c’est à dire la partie située après le protocole et le nom de domaine, et avant l’ancre (#)

  • les url back-end sont redirigés par le serveur fontal vers des serveurs spécifiques d’application, par exemple Python/Django/wsgi, ou NodeJS/natif. Ils sont associés à des webservices, ou produisent des pages web à partir de templates (Django, etc.), ou produisent des documents pdf, etc. Les verbes HTTP utilisés sont de tous types : GET, POST, PUT, etc.
    Par exemple, l’url https://yesno.wtf/api est associé à un webservice ; son chemin /api le fait classer par le serveur frontal comme un url de back-end, pour cette application yesno.wtf

  • les url front-end, comme https://yesno.wtf/, ou https://yesno.wtf/assets/application-dc53a8eb45b5db278e3c1ba040413b83.css sont servis comme de simples fichiers statiques que le client charge et interprète. Seules des requêtes GET et OPTIONS sont effectuées. Des serveurs comme Apache ou Nginx se chargent souvent de servir directement ces fichiers statiques, de façon très efficace.

Le back-end et le front-end appliquent ensuite chacun leur propre politique de routage des url, mais seulement pour les url qui les concernent, du fait du tri initial effectué par le serveur frontal. Côté back-end, on connait le système de routage appliqué par Node/Express ou par Python/Django. Côté front-end, après le chargement de la première page et le lancement du script associé, chaque changement de la variable javascript window.location est détecté et déclenche la mise en oeuvre du système de routage front-end.

Le routage front-end : push-state ou hash-based

La partie chemin d’accès d’une url (après le protocole, le nom de domaine et le port) a la forme suivante :

/<chemin>/.../<chemin>/#/<chemin>/.../<chemin>/

Dans tous les framework front-end modernes comme React, Angular ou Vue, le choix est proposé d’opérer le routage front-end, soit sur la partie avant le hash #, soit sur la partie après le hash.

Quand on prend en compte seulement la partie avant le hash, on parle de mode historique HTML5, ou push-state ; par exemple : http://mon.domaine/register
Quand on prend en compte la partie après le hash, on parle de routage en mode hash ; par exemple : http://mon.domaine/client/#/register

En mode push-state :

  • les url sont plus naturels
  • chaque changement est mémorisé dans l’historique de navigation
  • chaque changement d’url amène potentiellement un rechargement de la page

En mode hash :

  • les url sont moins naturels
  • les changements ne sont pas mémorisés dans l’historique de navigation
  • les changements d’url n’amène pas de rechargement de la page

Vue-router

Le routage des url par le client n’est pas assuré par VueJS lui-même, mais par un module tiers, généralement vue-router.

<template>
   <div id="app">
      <a href='#/componenta'>composant A</a>
      <a href='#/componentb'>composant B</a>
      <router-view></router-view>
   </div>
</template>

router/index.js

import Vue from 'vue'
import Router from 'vue-router'

import ComponentA from '@/components/ComponentA'
import ComponentB from '@/components/ComponentB'

Vue.use(Router)


const router = new Router({
   mode: 'hash',
   base: '/',
   routes: [
      {
         path: '/componenta',
         name: 'componenta',
         component: ComponentA,
      },
      {
         path: '/componentb',
         name: 'componentb',
         component: ComponentB,
      },
   ]
})

export default router

components/ComponentA.vue

<template>
  <p>Hello composant A</p>
</template>

components/ComponentB.vue

<template>
  <p>Hello composant B</p>
</template>

main.js

import router from './router'

...

new Vue({
  ...
  router
}).$mount('#app')

Algorithmique

  • à chaque fois qu’un nouvel url est poussé (router.push, lien <router-link>), ou modifié (router.go), la table de routage est consultée et la première règle qui s’applique est prise en compte. Toutes les autres sont ignorées.
  • le tag <router-view /> de plus haut niveau est remplacé par le composant spécifié par la règle active. Selon la valeur de props (booléen, objet, fonction), les props du composant sont affectées (voir plus loin)
  • si la route contient des children, le même algorithme est appelé récursivement pour peupler les <router-view /> emboités

Le plugin Vue dans le navigateur permet de suivre les associations entre routes et instances de <router-view />

Passage des props aux composants d’une route

Mode booléen

Indiquer props: true passe au composant référencé par la route, tout l’objet route.params en tant que props. Par exemple :

    {
      path: '/user/:id/posts/:postid',
      component: Post,
      props: true
    }

Ici c’est l’objet { id: <id>, postid: <postid> } qui est passé comme props au composant Post associé à la route

Mode objet

Quand props dans la définition de la route est un objet, c’est lui qui sera passé tel-quel au composant

Mode fonction

Quand props dans la définition de la route est une fonction, c’est la valeur de cette fonction qui sera passée au composant. Cette fonction prend comme argument l’objet route. Par exemple :

props: (route) => ({ query: route.query.q })

The Full Navigation Resolution Flow

  • Navigation triggered.
  • Call beforeRouteLeave guards in deactivated components.
  • Call global beforeEach guards.
  • Call beforeRouteUpdate guards in reused components.
  • Call beforeEnter in route configs.
  • Resolve async route components.
  • Call beforeRouteEnter in activated components.
  • Call global beforeResolve guards.
  • Navigation confirmed.
  • Call global afterEach hooks.
  • DOM updates triggered.
  • Call callbacks passed to next in beforeRouteEnter guards with instantiated instances.

Vuex - gestion centralisée de l’état

Quand faut-il utiliser une gestion centralisée d’état ?

Linus Borg: " Personally, I try and use local state as long as it works - which means, as long as this state is really only needed by one component (and it’s children). As soon as the data really is needed in more than one place, I move it to the store."

Principes généraux

  • un store vuex contient l’état partagé d’une application

  • contrairement à l’utilisation d’un objet global, les expressions calculées qui sont basées sur l’état du store, seront mises à jour de façon réactive, dans tous les composants

  • l’état d’un store doit être modifié par appel de fonctions spéciales de mutation, qui permettent notamment de tracer l’évolution de l’état. Il est interdit de modifier directement l’état d’un store

Exemple :

store.js

import Vue from "vue"
import Vuex from "vuex"

Vue.use(Vuex)

const state = {
   ...
   messages: undefined,
   ...
}

const mutations = {
   ...
   ADD_USER_MESSAGE (state, message) {
      // add message to state.messages
      Vue.set(state.messages, message.id, message)
   },
   ...
}

const actions = {
   ...
   POST_USER_MESSAGE async function({ commit, state }, message) {
     let response = await axios({
         method: 'post',
         url: MESSAGES_URL,
         data: message,
      })
      commit('ADD_USER_MESSAGE', response.data)
   },
   ...
}

export default new Vuex.Store({
   state,
   mutations,
   actions,
})

Messages.vue


<template>
   <div id="app">
      ...
      <button @click="postMessage">Post message</button>
      ...
   </div>
</template>

<script>
export default {
   ...
  methods: {
    postMessage: function() {
      this.$store.dispatch("POST_USER_MESSAGE", message)
    }
  },
  ...
}
</script>

Actions

  • les actions peuvent contenir des méthodes asynchrones
  • les actions commitent des mutations

Réactivité des tableaux

https://vuejs.org/v2/guide/list.html#Caveats

Des limitations de JavaScript empêchent Vue de détecter certains changements d’un tableau, que ce soit dans un état local ou dans un store Vuex

  • modification d’une valeur à un index, e.g. vm.items[indexOfItem] = newValue
  • modification directe de la taille, e.g. vm.items.length = newLength

Parades :

Vue.set(example1.items, indexOfItem, newValue)
Vue.delete(example1.items, indexOfItem)
Vue.delete(example1.items, key)

Normaliser les données

Render cycle