1°/ Préambule
Dans une application SPA créée par exemple en vuejs, le principe de fonctionnement basique consiste à demander des données à une ou plusieurs API afin de modifier l'affichage de la fenêtre de navigation.
L'envoi de requêtes à une API se fait de façon asynchrone, de façon à ce que l'application ne soit pas bloquée le temps que la requête soit traitée et que la réponse arrive. Pour envoyer la requête, il y a 2 solutions très utilisées :
- axios
- fetch
Les deux solutions proposent des fonctions pour faire des requêtes http asynchrones. Il est donc possible d'interroger une API REST pour obtenir des données.
Elles ont chacune leur avantages/inconvénients, mais si l'on privilégie la portabilité et la simplicité d'utilisation, Axios a un léger avantage. En revanche, Axios est à l'origine un module node, donc du code JS. Utiliser Axios implique d'intégrer son code l'application front-end. Quand celle-ci est envoyée au navigateur, elle sera donc plus volumineuse que la même application faite avec fetch, dont les fonctions sont déjà intégrées dans les navigateurs modernes (mais pas les anciens !)
2°/ Principes de base d'axios
Pour utiliser axios dans une application JS, il suffit d'installer le paquet axios :
npm install axios
Ensuite, dans les fichiers JS où l'on a besoin d'axios, il faut importer un objet qui permet d'accéder à toutes les fonctionnalités d'axios :
import axios from 'axios'
Enfin, on utilise les méthodes de cet objet pour faire des requêtes asynchrones. Les méthodes principales sont celles qui permettent de faire des requêtes http et elles sont toutes basées sur le principe des promesse afin de gérer le fait que le résultat de la requête n'arrive pas forcément immédiatement. Il faut donc attendre ce résultat pour ensuite faire quelque chose avec.
Pour gérer cette attente, il y a deux solutions syntaxiques : "à l'ancienne" avec les fonctions then(), catch() et des callbacks, ou bien "moderne", avec try/catch. De nombreux exemples sur le net utilisent la syntaxe à l'ancienne. Même si cette syntaxe devient rapidement illisible en cas d'enchaînements de traitements, il est important de la connaître, ne serait-ce que pour la transformer en try/catch.
Exemple axios "à l'ancienne" :
// requete GET : 1 param. = URL demandée
axios.get( "https://monsite.org/products/get/12345" )
.then( res => { // faire qqchose avec res.data })
.catch( err => { // faire qqchose avec err, par exemple afficher err.message})
// requete POST : 2 param. obligatoires = URL demandée + données à envoyer, 3ème param = options de config. de la requête
axios.post( "https://monsite.org/products/create", { name: "enclume", price: 1000}, {headers: 'MY-HEADER'} )
.then( res => { // faire qqchose avec res.data })
.catch( err => { // faire qqchose avec err, par exemple afficher err.message})
// requête DELETE : 2 param : URL + options de config.
axios.delete( "https://monsite.org/products/remove", { data: { name: "enclume"} } ) // data = objet intégré dans le corps de la requête,
.then( res => { // faire qqchose avec res.data })
.catch( err => { // faire qqchose avec err, par exemple afficher err.message})
Même exemple avec try/catch, bien plus concis et lisible :
let answer = null
try {
answer = await axios.get("https://monsite.org/products/get/12345")
// faire qqchose avec answer.data qui contient les données de réponse
answer = await axios.post("https://monsite.org/products/create", { name: "enclume", price: 1000})
// faire qqchose avec answer.data qui contient les données de réponse
answer = axios.delete( "https://monsite.org/products/remove", { data: { name: "enclume"} } )
// faire qqchose avec answer.data qui contient les données de réponse
}
catch(err) {
// faire qqchose avec err, par exemple afficher err.message
...
}
ATTENTION ! Comme indiqué dans les commentaires, les fonctions axios ne renvoient pas directement ce qui est renvoyé par l'API. Elles renvoient un objet dont le champ data contient la réponse de l'API. Dans l'exemple ci-dessus, la réponse de l'API est donc accessible grâce à answer.data.
3°/ Axios dans une application SPA
3.1°/ Créer un service axios
Dans le cadre d'un projet "professionnel", on n'utilise pas axios comme montré ci-dessus, en l'important dans les composants qui l'utilisent et en faisant des requêtes dans la partie <script>. On cherche avant tout la modularité et la réutilisabilité. C'est pourquoi, l'importation d'axios ne se fait qu'une seule fois, dans un fichier service, par exemple axios.service.js, que l'on met avec les autres fichiers de service.
Dans ce fichier, on va spécifier une (ou plusieurs) configuration d'utilisation d'axios commune à un ensemble de requêtes. Par exemple, il est possible de fixer une URL de base pour ces requêtes, ce qui évite de la fournir à chaque appel des fonctions qui font les requêtes HTTP. On peut également inclure des entêtes http, des cookies, etc. Pour chaque configuration voulue, on crée un "agent axios" auquel on donne un objet JSON décrivant la configuration. Si le front-end ne s'adresse qu'à une seule API, on ne crée généralement qu'un seul agent.
Exemple de fichier axios.service.js basique :
import axios from 'axios'
const axiosAgent = axios.create({
baseURL: 'https://apidemo.iut-bm.univ-fcomte.fr/apidemo',
// décommenter la ligne suivante si des cookies d'authentification doivent être renvoyés (cf. TD auth+jwt)
// withCredentials: true,
});
De plus, dans un cas simple mais très courant où un seul agent est créé pour toutes les requêtes vers l'API, on peut également créer des fonctions outils, permettant d'exécuter les requêtes HTTP courantes, ainsi qu'une fonction permettant de traiter les cas d'erreurs. Ces fonctions outils vont "masquer" l'utilisation d'axios car ce sont elles qui sont exportées et utilisées par le reste de l'application.
Exemple avec un "wrapper" pour les requêtes post :
...
// name is used for debug messages
async function postRequest(uri, data, config = {}, name) {
let answer = null
try {
answer = await axiosAgent.post(uri, data, config)
} catch (err) {
answer = handleError(err, name);
}
return answer.data;
}
...
export {
postRequest,
}
3.2°/ Utilisation du service axios
Toujours pour assurer une grande modularité/réutilisabilité, les composants n'utilisent JAMAIS le service axios et les fonctions exportées. Ce sont les autres services qui vont utiliser le service axios en définissant des fonctions pour interroger l'API. Généralement, on fait une fonction par route possible sur l'API, mais ce n'est pas une obligation.
Pour aller encore plus loin dans la modularité et permettre de développer le front en même temps que le back-end, il est conseillé de créer une fonction "wrapper", qui appelle soit la fonction qui utilise l'API, soit celle qui utilise la source locale. C'est ce wrapper qui est exporté et disponible au reste de l'application. Cette structuration permet de commencer le développement avec une source de données locale, comme un fichier, et de basculer facilement vers une source de type API quand celle-ci est implémentée.
Exemple (tiré de la démonstration) dans towns.service.js :
import {getRequest} from "@/services/axios.service";
import LocalSource from "@/datasource/controller"
async function getAllTownsFromAPI() {
return getRequest('/towns/get', 'GETALLTOWNS')
}
async function getAllTownsFromLocalSource() {
return LocalSource.getAllTowns()
}
async function getAllTowns() {
let answer = await getAllTownsFromAPI()
//let answer = await getAllTownsFromLocalSource(id)
return answer
}
export {
getAllTowns
}
3.3°/ Utilisation des services dépendants d'axios
Si on suit l'organisation donnée ci-dessus, les composants du front-end peuvent importer les fonctions des différents services afin de récupérer des données, sans même savoir si elles proviennent de l'API ou de la source locale = modularité/réutilisabilité maximale. Reste cependant une question : où doit-on importer et appeler ces fonctions ?
La réponse est simple et considère seulement deux cas : si les données récupérées grâce à une fonction d'un service sont utilisées
- par un seul et unique composant => on importe et appelle la fonction directement dans ce composant.
- dans plusieurs composants => on importe la fonction dans le store et on l'appelle dans une action qui stocke les données reçues dans le state. Les composants utilisent ensuite les mappers pour accéder à l'action en question et au state.
Exemple (tiré de la démonstration) dans le cas du store :
import Vue from 'vue'
import Vuex from 'vuex'
import {getAllTowns} from "@/services/towns.service";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
towns: [],
},
mutations: {
setTowns(state, towns) {
state.towns.splice(0)
towns.forEach(t => state.towns.push(t))
},
},
actions: {
async getTowns({commit}) {
console.log("STORE: get all towns")
let result = null
try {
result = await getAllTowns()
if (result.error === 0) {
commit('setTowns',result.data)
}
else {
console.log(result.data)
}
}
catch(err) {
console.log("Cas anormal dans getTowns()")
}
},
}
}
3.4°/ Intercepteurs
- Dans certaines situations, l'envoi de requête et/ou la réception du résultat nécessite un traitement particulier qui n'est pas statique.
- L'exemple classique est celui où l'on doit envoyer des informations d'authentification à chaque requête.
- Le problème est que ces informations ne sont pas statiques : on ne les écrit pas en dur dans le code de l'application web mais on les reçoit du serveur après avoir envoyé une requête afin de s'authentifier.
- Prenons le cas d'un identifiant de session renvoyé par le serveur. Pour les requêtes suivantes, il est fréquent d'utiliser une entête HTTP (un header) spéciale, dont la valeur est cet identifiant.
- Il faut donc configurer toutes les requêtes pour qu'elles envoient cette entête.
- Comme ce n'est pas une valeur "en dur", il n'est pas possible de configurer l'instance d'axios que l'on crée. Il faut forcément configurer chaque requête. Par exemple :
axios.get( url, { headers: {session-id: 'abcdefghij'} } )
- Comme cela implique beaucoup de copier/coller, axios fournit un mécanisme appelé intercepteurs. Le premier permet de modifier "à la volée" la configuration d'une requête sortante et l'erreur quand cette requête n'est pas reçue par l'API. Le deuxième permet de modifier la réponse de l'API, que la requête ait réussi ou échoué.
Exemple 1, pour ajouter des entêtes à chaque requête (dans axios.service.js) :
...
axiosAgent.interceptors.request.use(
config => { return { ...config, headers: { "api-key": "1234azer" } } },
error => { return Promise.reject(error) }
)
- Dans cet exemple, on suppose que l'on doit envoyer à chaque requête une chaîne de caractère "secrète" dans une entête api-key.
- Comme on le constate, interceptors.request.use() prend en paramètre 2 fonctions : une pour modifier la configuration de toutes les requêtes, l'autre pour modifier les erreurs de requête.
- Dans le cas présent, on modifie la configuration par défaut en ajoutant (d'où les ... devant config) des entêtes lors de l'envoi et on ne fait rien de spécial en cas d'erreur.
Exemple 2, pour loguer toute réponse/erreur reçue :
...
axiosAgent.interceptors.response.use(
res => {
console.log("OK => "+JSON.stringify(res))
return res
},
error => {
console.log("ERROR => "+JSON.stringify(error))
return Promise.reject(error)
}
)
- Le principe est similaire aux modifications de requêtes. On définit une fonction qui est utilisée en cas de succès de la requête, c.a.d. un statut HTTP valant 2XX, et une autre fonction quand la requête a échoué, avec un statut != 2XX.
4°/ Démonstration
Le code de l'application est téléchargeable [ ici ].
Il ne contient que le répertoire src des sources. Il faut donc au préalable créer un projet vide, avec seulement vuex comme plugin. Une fois le projet créé, il faut encore ajouter axios, puis remplacer le répertoire src avec celui dans l'archive.