# VENoM : Vue Express Node MongoDB
# Rappels sur JavaScript, Vue et Vue-router
Cf. partie dédiée
# Vuex
# Qu'est-ce qu'un état de l'application
L'état de l'application est un objet contenant des données concernant toute l'application, autrement dit, des données que plusieurs composants de l'application pourront consulter.
On parle de store pour désigner une bibliothèque de gestion de l'état : un store va contenir l'état et les méthodes pour modifier cet état.
# La solution officielle de l'écosystème Vue : Vuex
Vuex 4 (opens new window) est la librairie officielle pour gérer le store dans une application Vue. Attention, pour la version 3 de vue (que je recommande d'utiliser), il faut la version 4.
# Les différentes parties du store :
Un store est composé de plusieurs parties :
- l'état, qui est un objet dans la propriété
state
dont les propriétés sont réactives ; - les mutations, qui sont dans un objet dans la propriété
mutations
dont les propriétés sont des fonctions qui vont changer l'état ; - les actions, qui sont dans un objet dans la propriété
actions
dont les propriétés sont des fonctions qui vont appeler des mutation ; - les getters, qui sont dans un objet dans la propriété
getters
dont les propriétés sont des fonctions qui sont l'équivalent descomputed
des composants : elles vont calculer des données en fonctions d'autres données de l'état (ou même d'autres propriétés calculées) ; - enfin les modules qui sont dans un objet dans la propriété
modules
qui vont contenir des sous-stores.
# L'état (state)
export default createStore({
state: {
user: undefined
},
(...)
})
Ici, user
est une propriété réactive de l'état (state
) du store.
Dans tous les composants Vue, si le store est ajouté dans un composant parent (il est en général ajouté au composant
App, composant parent de tous les autres), le store est accessible dans this.$store
.
Dans un composant, user
pourra donc être récupéré de cette façon :
<template>
<div>
Bienvenue, {{ user && user.login }}
</div>
<router-view/>
</template>
<script>
export default {
name: 'AppHeader',
computed: {
user () {
return this.$store.state.user // propriété `user` du `state` du `store`
}
}
}
</script>
Cela est tellement courant qu'une fonction utilitaire existe pour cela, mapState
de vuex
:
<template>
<div>
Bienvenue, {{ user && user.login }}
</div>
<router-view/>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'AppHeader',
computed: mapState(['user']) //
}
</script>
mapState
retourne un objet avec une propriété user
qui sera la propriété user
du state
du store
.
# Les mutations
Les mutations servent à muter l'état. Pour changer la propriété user
, par exemple, on peut créer ces mutations :
export default createStore({
state: {
user: undefined
},
mutations: {
setUser (state, user) {
state.user = user
},
resetUser (state) {
state.user = undefined
}
},
(...)
})
Il ne doit pas y avoir de logique dans les mutations, et il ne doit pas y avoir de code asynchrone. Elles doivent rester des fonctions très simples.
# Les actions
Les actions sont des fonctions dans lequel il peut y avoir de la logique et même du code asynchrone. Les actions
vont appeler (on parle de commit
) des mutations. Ce sont les composants, ou éventuellement le router,
qui va appeler (on parle de dispatch
) des actions.
export default createStore({
state: {
user: undefined
},
mutations: {
setUser (state, user) {
state.user = user
},
resetUser (state) {
state.user = undefined
}
},
actions: {
login ({ commit }, credentials) {
api.login(credentials)
.then(data => {
const { success, user, token, message } = data
if (!success) {
// TODO: Afficher proprement le message contenu dans `message` dans l'interface
// et non dans la console comme ici
console.error(message)
return
}
localStorage.setItem('token', token)
commit('setUser', user)
})
},
logout ({ commit }) {
localStorage.removeItem('token')
commit('resetUser')
}
},
(...)
})
# Les getters
Les getters sont des fonctions que l'on utilise comme des propriétés, exactement comme les computed
des composants.
export default createStore({
state: {
user: undefined
},
mutations: {
setUser (state, user) {
state.user = user
},
resetUser (state) {
state.user = undefined
}
},
actions: {
login ({ commit }, credentials) {
api.login(credentials)
.then(data => {
const { success, user, token, message } = data
if (!success) {
// TODO: Afficher proprement le message contenu dans `message` dans l'interface
// et non dans la console comme ici
console.error(message)
return
}
localStorage.setItem('token', token)
commit('setUser', user)
})
},
logout ({ commit }) {
localStorage.removeItem('token')
commit('resetUser')
}
},
getters: {
isLoggedIn (state) {
return !!state.user
}
}
})
Ce getter isLoggedIn
pourra donc s'utiliser comme suit dans un composant :
<template>
<div>
Bienvenue, {{ isLoggedIn && user.login }}
</div>
<router-view/>
</template>
<script>
export default {
name: 'AppHeader',
computed: {
user () {
return this.$store.state.user // propriété `user` du `state` du `store`
},
isLoggedIn () {
return this.$store.getter.isLoggedIn
}
}
}
</script>
Il existe aussi une fonction mapGetters
dans vuex
qui permet d'accéder plus facilement aux getters. Attention,
comme mapState
et mapGetters
sont des fonctions qui renvoient des objets, si on veut utiliser les deux, il faudra
utiliser la syntaxe de décomposition :
<template>
<div>
Bienvenue, {{ isLoggedIn && user.login }}
</div>
<router-view/>
</template>
<script>
export default {
name: 'AppHeader',
computed: {
...mapState(['user']), // Décomposition de toutes les propriétés de l'objet retourné par mapState('user')
...mapGetters(['isLoggedIn']) // Décomposition de toutes les propriétés de l'objet retourné par mapGetters(['isLoggedIn'])
}
}
</script>
# Le découpage en modules
Lorsque le store a trop de responsabilités, il convient de le diviser en sous-store, appelés modules
dans la
terminologie vuex.
Voici un exemple :
store/index.js
export default createStore({
modules: {
user
}
})
store/user-module.js
export default {
state: {
data: undefined,
isFetching: false
},
mutations: {
setUser (state, user) {
state.data = user
},
resetUser (state) {
state.data = undefined
}
},
actions: {
login ({ commit }, credentials) {
api.login(credentials)
.then(data => {
const { success, user, token, message } = data
if (!success) {
// TODO: Afficher proprement le message contenu dans `message` dans l'interface
// et non dans la console comme ici
console.error(message)
return
}
localStorage.setItem('token', token)
commit('setUser', user)
})
},
logout ({ commit }) {
localStorage.removeItem('token')
commit('resetUser')
}
},
getters: {
isLoggedIn (state) {
return !!state.data
}
}
}
Désormais, les infos de l'utilisateur seront accessible comme ceci :
<template>
<div>
Bienvenue, {{ isLoggedIn && user.login }}
</div>
<router-view/>
</template>
<script>
export default {
name: 'AppHeader',
computed: {
user () {
- return this.$store.state.user // propriété `user` du `state` du `store`
+ return this.$store.state.user.data // propriété `user` du `state` du `store`
},
isLoggedIn () {
return this.$store.getter.isLoggedIn
}
}
}
</script>
ou bien :
<template>
<div>
- Bienvenue, {{ isLoggedIn && user.login }}
+ Bienvenue, {{ isLoggedIn && user.data.login }}
</div>
<router-view/>
</template>
<script>
export default {
name: 'AppHeader',
computed: {
...mapState(['user']),
...mapGetters(['isLoggedIn'])
}
}
</script>
ou bien encore :
<template>
<div>
Bienvenue, {{ isLoggedIn && user.login }}
</div>
<router-view/>
</template>
<script>
export default {
name: 'AppHeader',
computed: {
- ...mapState(['user']),
+ ...mapState({ // Ici, ce n'est plus un tableau (Array) mais un objet (Object litteral)
+ user: state => state.user.data // Chaque propriété est une fonction qui reçoit le state global en paramètre
+ }),
...mapGetters(['isLoggedIn'])
}
}
</script>
# L'authentification
La méthode la plus utilisée aujourd'hui pour gérer l'authentification est par les JWT. Cf. cours du premier semestre.
# Garder les données utilisateurs dans le state après l'authentification
Il s'agit tout d'abord d'envoyer les identifiants (nom d'utilisateur et mot de passe) avec XHR ou fetch, de récupérer le JWT renvoyé par le serveur dans le corps de la réponse, et ensuite de le stocker dans le localStorage.
Le serveur devrait aussi renvoyer des infos concernant l'utilisateur. En général au moins son nom d'utilisateur, souvent en plus un identifiant (celui de la base de données, par exemple), et éventuellement sa date de naissance, ses préférences, etc.
# Regarder dans le localStorage et vérifier le token
Si jamais l'utilisateur s'est authentifié il y a peu, le JWT est sans doute encore valable. Il ne faudrait donc pas lui redemander de s'authentifier.
L'application devrait donc, au chargement, regarder dans le localStorage s'il y a JWT, et si c'est le cas, il faudrait vérifier sa validité.
# Rediriger le cas échéant (se souvenir du chemin demandé)
Si le JWT est toujours valide, l'utilisateur devrait être redirigé vers la home ou une page protégée (qui nécessite une authentification).
Exercice :
Si le JWT n'est pas valide et que l'utilisateur a demandé une page protégée :
- le chemin vers la page protégée devrait être mémorisée,
- l'utilisateur devrait être redirigé vers une page d'authentification,
- et si celle-ci réussit, il devrait être redirigé vers la page demandée initialement
# MongoDB Partie 1
### Qu'est-ce qu'une base de données orientée documents