1°/ Les API Web
- Une API Web se différencie d'un site web par le fait qu'elle n'envoie généralement pas de page Web au navigateur mais simplement des données.
- Ces données sont généralement issues d'une base de données et sont très souvent envoyées au format JSON. Cependant, cela peut être un autre format, du moment que la navigateur peut lire nativement ce format, ou bien que du code javascript sache le faire.
- Dans sa forme la plus courante, une API Web est donc juste un site web que l'on peut interroger avec des URLs bien précises, qui vont provoquer des requêtes en base de données et renvoyer le résultat au navigateur.
- Une API doit donc fixer quel est le format des URLs et le contenu de la requête http que doit envoyer le navigateur pour demander une fonctionnalité précise.
- Pour cela, il existe différentes solutions, mais la plus courante est d'utiliser les principes de REST (Representational State Transfer).
- Une API REST se base sur les types de requête http pour définir le type d'opération sur la base de données.
- Par exemple :
- GET : pour récupérer des données,
- POST : pour insérer des données,
- PUT : pour remplacer/insérer des données,
- DELETE : pour supprimer des données.
- De plus, le format des URLs permet dé définir à quelle table/collection on souhaite accéder, voire à quel enregistrement.
- Par exemple http://monsite.org/mabase/matable/19 : pourraît correspondre à retourner l'enregistrement d'id = 19 dans la table matable de la base mabase.
- Cependant, REST n'est pas un norme figée. Il est donc possible de ne pas suivre le exemples donnés ci-dessus.
- Ce n'est pas grave du moment que le protocole de communication de l'API est rendu public.
manipulation :
- ouvrir un navigateur et aller à l'URL : http://api.dut-info.cf/town/streets/getall
- cette URL permet d'aller chercher tous les documents dans la collection "streets, de la base "town" sur le serveur api.dut-info.cf
- aller à l'URL : http://api.dut-info.cf/town/items/get/axe
- cette URL permet d'aller chercher l'item nommé "axe" dans la collection "item", de la base "town".
- dans les deux cas, la réponse est envoyée au format JSON.
Remarques :
- Dans les manipulations ci-dessus, on observe qu'il y a partout des attributs _id, qui ne sont pas présent dans notre modèle de données utilisé jusqu'à présent (cf. model.js).
- Cet attribut existe dans la base de données et est renvoyé au navigateur. Cela permet de retrouver plus facilement des enregistrements grâce à leur id ... si l'API le permet !
- La base de données utilisée est mongodb. C'est une BdD non-relationnelle, qui permet de stocker des documents directement au format JSON.
2°/ Axios
2.1°/ Principes
2.1°/ Principes
- En HTML basique, quand on clique sur un lien, cela conduit forcément à une requête HTTP qui changer ou recharger la page courante.
- Or, javascript permet d'éviter cela. En effet, il est possible d'émettre une requête HTTP "en tâche de fond" tout en conservant la page courante. On dit que cette requête est asynchrone car on n'attend pas son résultat. Sinon, cela bloquerait l'exécution de tout autre bout de code javascript, par exemple ceux associés à la gestion des événements.
- Quand le serveur renvoie le résultat, du code javascript peut utliser ce dernier pour mettre à jour le DOM et ainsi changer l'aspect de la page sans pour autant l'avoir rechargée.
- C'est ce principe qui est utilisé par AJAX et qui est implémenté dans des bibliothèques/modules tels que JQuery, ou axios.
- Axios est un module pour vuejs qui permet de faire des requêtes HTTP asynchrones, en utilisant des principes de programmation js "modernes" (i.e. les promesses).
- Grâce à axios, interroger une API Web devient un processus simpe, facilement reproductible.
- Pour intégrer axios dans une page, il suffit d'inclure son code js.
- Ensuite, il suffit d'appeler une méthode correspondant au type HTTP, en lui fournissant une URL, plus éventuellement des données.
- Le code qui va s'exécuter après réception du résultat est donné grâce à un appel à la fonction then().
Exemple :
Remarques :
1 2 3 4 5 6 |
let params = { id:1234 } axios.get('http://myapi/bdd/collec/get',params).then( response => { console.log(response.data); } ); |
- cet exemple envoie une requête de type GET, avec comme données le contenu de l'objet params.
- le paramètre fourni à la fonction then() doit être lui-même une fonction qui va permettre de traiter la réponse.
- généralement, on écrit cette fonction grâce à la notation fléchée.
- dans cet exemple, on a appellé response l'unique paramètre de cette fonction fléchée, et pour son code, on se contente d'afficher les données de la réponse du serveur.
- ATTENTION : l'objet renvoyé par le serveur (i.e. response) ne contient pas que les données issues du traitement de la requête.
- Comme on peut le voir, les données se trouvent dans l'attribut data.
2.2°/ Mon premier exemple réel
- L'API est accessible sur 2 machines. Celle qui est utilisée dans les démonstrations nécessite de "brancher" son accès VPN Université.
- Cependant, vous pouvez également utiliser la 2ème machine en commentant la 1ère ligne, et décommentant la 2ème.
- La troisième ligne peut vous servir si vous mettez vous-même en place une BdD mongo et nodejs sur votre machine personnelle. Pour cela, référez-vous à l'article disponible sur cours-info.
Démonstration :
- créer un fichier index-01.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var url = 'http://prodigy.iut-bm.univ-fcomte.fr:3333/town/'; //var url = 'http://api.dut-info.cf/town/'; // adresse alternative //var url = 'http://localhost:3333/town/'; // si vous faites tourner l'API chez vous var app = new Vue({ el: '#mydiv', data: { items }, methods: { getItems : function() { axios.get(url+'items/getall') .then(response => { items.splice(0,items.length); // to empty items array if needed response.data.forEach(e => {items.push(Item.fromObject(e))}); }) } } }); |
- créer un fichier model.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
var itemCats = [ 'helmet', 'crown', 'armor', 'clothes', 'weapon', 'lighter', 'purse', 'potion', 'spell', 'food']; class Item { constructor(_id, name, type, price, effect) { this._id = _id; this.name = name; //let idx = itemCats.findIndex(e => e == type); //var testType = function(el) {return el == type}; function testType(el) { return el == type;} let idx = itemCats.findIndex(testType); if (idx != -1) { this.type = type; } else { this.type = ''; } if (price>=0) { this.price = price; } else { this.price = 0; } this.effect = effect; } static fromObject(obj) { let it = new Item(obj._id, obj.name, obj.type,obj.price, obj.effect); return it; } } class Shop { constructor(_id, name, type) { this._id = _id; this.name = name; this.type = type; this.items = []; } static fromObject(obj) { let sh = new Shop(obj._id, obj.name, obj.type); obj.items.forEach(item => { sh.items.push(Item.fromObject(item)); }); return sh; } } class Street { constructor(_id, name) { this._id = _id; this.name = name; this.shops = []; } static fromObject(obj) { let st = new Street(obj._id, obj.name); obj.shops.forEach(shop => { st.shops.push(Shop.fromObject(shop)); }); return st; } } var items = []; var streets = []; |
- créer un fichier exe-01.html et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="model.js"></script> </head> <body> <div id="mydiv"> <ol> <li v-for="it in items">{{it.name}} : {{it.price}} gp</li> </ol> <button @click="getItems()">get items</button> <p v-if="items[0]">{{items[0]}}</p> </div> <script src="index-01.js"></script> </body> </html> |
Manipulations
- cliquer sur le bouton pour faire apparaître la liste des items.
- un 2ème clic refait la demande mais comme on vide items avant, la liste ne s'aggrandit pas.
Remarques :
- Noter la balise <script> dans l'entête qui permet d'inclure axios.
- Pour faire simple, on pourrait se contenter de stocker dans items le résultat de la requête puisque c'est déjà du JSON.
- Le problème de cette approche est qu'il n'y a aucune méthode associée aux items ainsi créés.
- Pour résoudre ce problème, on parcourt l'ensemble des objets présent dans la réponse, et pour chacun on crée une instance de la classe Item.
- Cette création est faite grâce à la méthode static fromObject(), qui extrait la valeur des attributs de l'objet pour appeler le constructeur de Item.
- Généralement, on donne le même nom aux attributs de la classe que ceux qui sont reçus. Mais ce n'est pas obligatoire.
- C'est un autre avantage de cette méthode : on crée un découplage entre valeurs reçues et objets manipulés par l'application vue.
2.3°/ Exemple avec plusieurs requêtes
- Dans cet exemple, on veut tester la fonctionnalité de l'API : créer un item.
- Pour cela, il faut faire une requête de type PUT, avec en paramètre un objet au format : {name: String, type: String, price: Number, effect: String}. Le nom et le type sont limités à 50 caractères, le type devant être une des catégories définie dans model.js, et l'effet est limité à 5 caractères, avec comme format une lettre, suivi d'un + ou -, suivi d'un nombre compris entre 1 et 999.
Démonstration :
- créer un fichier index-02.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
var url = 'http://prodigy.iut-bm.univ-fcomte.fr:3333/town/'; //var url = 'http://api.dut-info.cf/town/'; // adresse alternative //var url = 'http://localhost:3333/town/'; // si vous faites tourner l'API chez vous var app = new Vue({ el: '#mydiv', data: { items, newItemName : '', newItemType : '', newItemPrice : '', newItemEffect : '', }, methods: { getItems : function() { axios.get(url+'items/getall') .then(response => { // empty items if already filled items.splice(0,items.length); response.data.forEach(e => {items.push(Item.fromObject(e))}); }) }, createItem : function() { let it = { name:this.newItemName, type:this.newItemType, price:parseInt(this.newItemPrice), effect:this.newItemEffect } axios.put(url+'items/create',it) .then(response => { /* CAUTION: as stated by the API response.data is an object: { err:0/1, data:object} if err = 0, data contains a json object that may be the answer to the request, or an ack. if err = 1, resquest failed and data is useless */ if (response.data.err == 1) { alert("cannot create an item"); } else { alert("item creation: success"); this.items.push(Item.fromObject(response.data.data)); } }) } } }); |
- créer un fichier exe-02.html et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="model.js"></script> </head> <body> <div id="mydiv"> <ol> <li v-for="it in items">{{it.name}} : {{it.price}} gp</li> </ol> <button @click="getItems()">get items</button> <hr> <h1>Create an item</h1> <label>Name:</label><input v-model="newItemName"> <label>Type:</label><input v-model="newItemType"> <label>Price:</label><input v-model="newItemPrice"> <label>Effect:</label><input v-model="newItemEffect"><br> <button v-if="newItemName!=''" @click="createItem()">create item named {{newItemName}}</button> </div> <script src="index-02.js"></script> </body> </html> |
- remplir les champs puis cliquer sur "create". L'item est créer et ajouté dans la liste.
- cliquer sur "get items" : l'item a bien été ajouté en BdD
Remarques :
- Le format de la réponse est entièrement fixé par l'API et n'est pas forcément identique pour toutes les requêtes.
- Dans le cas présent, pour obtenir la liste des items, cette liste est directement donnée dans response.data.
- Mais pour la création d'un item, response.data contient un objet plus complexe au format {err:Number, data:Object} : le 1er attribut indique s'il y a eu une erreur, l'autre donne l'item créé.
- Dans index-02.js, on voit que s'il n'y a pas d'erreur, on prend le contenu de response.data.data pour créer une nouvelle instance de la classe Item et l'ajouter à items.
2.4°/ Requête lors du chargement de la page
Démonstration :
2.5°/ exemple complet
- Dans les deux premiers exemples, il faut charger les items en cliquant sur un bouton.
- Ce n'est pas forcément très pratique. Il serait plus commode d'avoir les données déjà chargées au moment d'afficher la page.
- C'est possible grâce à la façon dont vuejs construit la page : par phases.
- Or, avant/après chaque phase, il existe une méthode avec un nom précis que l'on peut définir pour faire un traitement particulier.
- Par exemple, la méthode mounted() sera appelée juste après que la page soit chargé et que le contenu de l'attribut el soit associé à un élément du DOM.
- C'est donc un moment possible pour faire une requête via axios afin de remplir le tableau items et ainsi pouvoir afficher son contenu.
Démonstration :
- créer un fichier index-03.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var url = 'http://prodigy.iut-bm.univ-fcomte.fr:3333/town/'; //var url = 'http://api.dut-info.cf/town/'; // adresse alternative //var url = 'http://localhost:3333/town/'; // si vous faites tourner l'API chez vous var app = new Vue({ el: '#mydiv', data: { items, streets, }, mounted() { // get all items axios.get(url+'items/getall') .then(response => { items.splice(0,items.length); response.data.forEach(e => {items.push(Item.fromObject(e))}); }) // get all streets axios.get(url+'streets/getall') .then(response => { streets.splice(0,streets.length); response.data.forEach(e => {streets.push(Street.fromObject(e))}); }) } }); |
- créer un fichier exe-03.html et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="model.js"></script> </head> <body> <div id="mydiv"> <table> <tr> <td> <ol> <li v-for="st in streets">{{st.name}}</li> </ol> </td> <td> <ol> <li v-for="it in items">{{it.name}}</li> </ol> </td> </tr> </table> </div> <script src="index-03.js"></script> </body> </html> |
Remarque :
- ATTENTION : noter le mounted() et surtout pas mounted: comme on utilise avec les attributs déjà abordés (el, data, methods, ...).
- Dans cet exemple, on pourrait aussi utiliser la méthode created() qui est appelée juste après la création de data. En effet, ces requêtes ne modifient que les propriétés de data.
2.5°/ exemple complet
- l'API permet en fait de gérer des rues, avec dedans des magasin proposants des items.
- Quand on récupère un objet représentant une rue, il contient donc un tableau de magasins, contenant eux-mêmes un tableu d'items.
- Cet exemple permet de créer des magasins pour une rue donnée, et de remplir les stocks d'un magasin.
Démonstration :
Remarques :
- créer un fichier index-04.js et copier/coller dedans (NB : dans discord, il faut faire un copier/coller en 2 fois car trop long) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
var url = 'http://prodigy.iut-bm.univ-fcomte.fr:3333/town/'; //var url = 'http://api.dut-info.cf/town/'; // adresse alternative //var url = 'http://localhost:3333/town/'; // si vous faites tourner l'API chez vous var app = new Vue({ el: '#mydiv', data: { items, streets, idItem: '', idShop: '', newShopName : '', newShopType : '', idStreet : '' }, computed: { currentStreet : function() { return this.streets[this.idStreet-1]; }, currentShop : function() { if (this.currentStreet != undefined) { return this.currentStreet.shops[this.idShop-1]; } else { return undefined; } }, currentItem : function() { return this.items[this.idItem-1]; } }, methods: { createShop : function() { let data = { id:this.currentStreet._id, name:this.newShopName, type:this.newShopType } axios.put(url+'shops/create',data) .then(response => { if (response.data.err == 1) { alert("cannot create a shop"); } else { alert("shop creation: success"); this.currentStreet.shops.push(Shop.fromObject(response.data.data)); } }) }, stockInShop : function() { let data = { id:this.currentShop._id, idItem:this.currentItem._id } axios.post(url+'shops/add',data) .then(response => { if (response.data.err == 1) { alert("cannot add an item to current shop"); } else { alert("add item to current shop: success"); this.currentShop.items.push(this.currentItem); } }) } }, mounted() { // get all items axios.get(url+'items/getall') .then(response => { items.splice(0,items.length); response.data.forEach(e => {items.push(Item.fromObject(e))}); }) // get all streets axios.get(url+'streets/getall') .then(response => { streets.splice(0,streets.length); response.data.forEach(e => {streets.push(Street.fromObject(e))}); }) } }); |
- créer un fichier exe-04.html et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="model.js"></script> </head> <body> <div id="mydiv"> <table> <tr> <td> <ol> <li v-for="st in streets">{{st.name}}</li> </ol> <label>#Street</label><input v-model="idStreet"><br> <hr> <div v-if="currentStreet"> <h1>{{currentStreet.name}}</h1> <ol> <li v-for="sh in currentStreet.shops">{{sh.name}}</li> </ol> <label>#Shop: </label><input v-model="idShop"><br> <div v-if="currentShop"> Shop proposes:<br> <ol> <li v-for="it in currentShop.items">{{it.name}}</li> </ol> </div> <hr> <p>Create a new shop</p> <label>name: </label><input v-model="newShopName"><label>type: </label><input v-model="newShopType"><button @click="createShop">Create shop {{newShopName}}</button><br> <hr> </div> </td> <td> <ol> <li v-for="it in items">{{it.name}}</li> </ol> <label>#Item</label><input v-model="idItem"><button v-if="currentItem && currentShop" @click="stockInShop">Stock in current shop {{currentItem.name}}</button><br><br> </td> </tr> </table> </div> <script src="index-04.js"></script> </body> </html> |
Remarques :
- Pour bien faire, il faudrait que la page soit construite grâce à des composants.
- l'API permet également de créer une nouvelle rue, sans boutique :
- à envoyer : requête de type PUT, URL = .../streets/create, paramètres : {name: String}, où name donne le nom de la nouvelle rue.
- à recevoir : un objet {err:0/1, data:Object}. Si err = 0, alors data contient un objet JSON représentant la nouvelle rue.
- Grâce à ces informations, vous pouvez facilement ajouter la fonctionnalité "créer une rue". A vous de jouer.