Aller au contenu

c

Obtenir des données du serveur

Depuis un certain temps, nous ne travaillons que sur le "frontend", c'est-à-dire la fonctionnalité côté client (navigateur). Nous commencerons à travailler sur le "backend", c'est-à-dire les fonctionnalités côté serveur dans la troisième partie de ce cours. Néanmoins, nous allons maintenant faire un pas dans cette direction en nous familiarisant avec la façon dont le code s'exécutant dans le navigateur communique avec le backend.

Utilisons un outil destiné à être utilisé lors du développement logiciel appelé JSON Server pour agir en tant que notre serveur.

Créez un fichier nommé db.json dans le répertoire racine du projet de notes précédent avec le contenu suivant :

{
  "notes": [
    {
      "id": 1,
      "content": "HTML is easy",
      "date": "2022-1-17T17:30:31.098Z",
      "important": true
    },
    {
      "id": 2,
      "content": "Browser can execute only JavaScript",
      "date": "2022-1-17T18:39:34.091Z",
      "important": false
    },
    {
      "id": 3,
      "content": "GET and POST are the most important methods of HTTP protocol",
      "date": "2022-1-17T19:20:14.298Z",
      "important": true
    }
  ]
}

Vous pouvez installer le serveur JSON globalement sur votre ordinateur à l'aide de la commande npm install -g json-server. Une installation globale nécessite des privilèges administratifs, ce qui signifie qu'elle n'est pas possible sur les ordinateurs des professeurs ou les ordinateurs portables des étudiants de première année.

Cependant, une installation globale n'est pas nécessaire. Depuis le répertoire racine de votre application, nous pouvons exécuter le json-server en utilisant la commande npx :

npx json-server --port 3001 --watch db.json

Le json-server commence à s'exécuter sur le port 3000 par défaut ; mais comme les projets créés à l'aide de create-react-app réservent le port 3000, nous devons définir un port alternatif, tel que le port 3001, pour le serveur json.

Naviguons jusqu'à l'adresse http://localhost:3001/notes dans le navigateur. Nous pouvons voir que json-server retourne les notes que nous avons précédemment écrites dans le fichier au format JSON :

fullstack content

Si votre navigateur ne permet pas de formater l'affichage des données JSON, installez un plugin approprié, par ex. JSONView pour vous faciliter la vie.

À l'avenir, l'idée sera de sauvegarder les notes sur le serveur, ce qui signifie dans ce cas de les sauvegarder sur le serveur json. Le code React récupère les notes du serveur et les affiche à l'écran. Chaque fois qu'une nouvelle note est ajoutée à l'application, le code React l'envoie également au serveur pour que la nouvelle note persiste en "mémoire".

json-server stocke toutes les données dans le fichier db.json, qui réside sur le serveur. Dans le monde réel, les données seraient stockées dans une sorte de base de données. Cependant, json-server est un outil pratique qui permet d'utiliser les fonctionnalités côté serveur dans la phase de développement sans avoir besoin de programmer quoi que ce soit.

Nous nous familiariserons plus en détail avec les principes d'implémentation des fonctionnalités côté serveur dans la partie 3 de ce cours.

Le navigateur comme environnement d'exécution

Notre première tâche consiste à récupérer les notes déjà existantes dans notre application React à partir de l'adresse http://localhost:3001/notes.

Dans l'exemple de projet de la partie 0, nous avons appris un moyen de récupérer des données à partir d'un serveur à l'aide de JavaScript. Le code de l'exemple récupérait les données à l'aide de XMLHttpRequest, également connu sous le nom de requête HTTP effectuée à l'aide d'un objet XHR. Il s'agit d'une technique introduite en 1999, que tous les navigateurs supportent depuis un bon moment maintenant.

L'utilisation de XHR n'est plus recommandée, et les navigateurs supportent déjà largement la méthode fetch, qui est basée sur les promises, au lieu du modèle événementiel utilisé par XHR.

Pour rappel de la partie 0 (qu'il faut en fait se souvenir de ne pas utiliser sans raison impérieuse), les données ont été récupérées en utilisant XHR de la manière suivante :

const xhttp = new XMLHttpRequest()

xhttp.onreadystatechange = function() {
  if (this.readyState == 4 && this.status == 200) {
    const data = JSON.parse(this.responseText)
    // gérer la réponse qui est enregistrée dans la variable data
  }
}

xhttp.open('GET', '/data.json', true)
xhttp.send()

Dès le début, nous enregistrons un gestionnaire d'événements sur l'objet xhttp représentant la requête HTTP, qui sera appelée par le runtime JavaScript chaque fois que l'état de xhttp changements d'objet. Si le changement d'état signifie que la réponse à la demande est arrivée, alors les données sont traitées en conséquence.

Il convient de noter que le code du gestionnaire d'événements est défini avant que la requête ne soit envoyée au serveur. Malgré cela, le code du gestionnaire d'événements sera exécuté ultérieurement. Par conséquent, le code ne s'exécute pas de manière synchrone "de haut en bas", mais le fait de manière asynchrone. JavaScript appelle le gestionnaire d'événements qui a été enregistré pour la demande à un moment donné.

Une manière synchrone de faire des requêtes qui est courante dans la programmation Java, par exemple, se déroulerait comme suit (NB, ce n'est pas du code Java fonctionnel):

HTTPRequest request = new HTTPRequest();

String url = "https://studies.cs.helsinki.fi/exampleapp/data.json";
List<Note> notes = request.get(url);

notes.forEach(m => {
  System.out.println(m.content);
});

En Java, le code s'exécute ligne par ligne et s'arrête pour attendre la requête HTTP, c'est-à-dire attendre la fin de la commande request.get(...). Les données renvoyées par la commande, dans ce cas les notes, sont ensuite stockées dans une variable, et nous commençons à manipuler les données de la manière souhaitée.

D'autre part, les moteurs JavaScript ou les environnements d'exécution suivent le modèle asynchrone. En principe, cela nécessite que toutes les opérations IO (à quelques exceptions près) soient exécutées de manière non bloquante. Cela signifie que l'exécution du code se poursuit immédiatement après l'appel d'une fonction IO, sans attendre son retour.

Lorsqu'une opération asynchrone est terminée, ou, plus précisément, à un moment donné après son achèvement, le moteur JavaScript appelle les gestionnaires d'événements enregistrés à l'opération.

Actuellement, les moteurs JavaScript sont à thread unique, ce qui signifie qu'ils ne peuvent pas exécuter de code en parallèle. Par conséquent, il est nécessaire en pratique d'utiliser un modèle non bloquant pour l'exécution des opérations IO. Sinon, le navigateur "se bloquerait" pendant, par exemple, la récupération de données à partir d'un serveur.

Une autre conséquence de cette nature à thread unique des moteurs JavaScript est que si l'exécution de certains codes prend beaucoup de temps, le navigateur restera bloqué pendant toute la durée de l'exécution. Si nous ajoutions le code suivant en haut de notre application :

setTimeout(() => {
  console.log('loop..')
  let i = 0
  while (i < 50000000000) {
    i++
  }
  console.log('end')
}, 5000)

tout fonctionnerait normalement pendant 5 secondes. Cependant, lorsque la fonction définie comme paramètre pour setTimeout est exécutée, le navigateur sera bloqué pendant toute la durée d'exécution de la longue boucle. Même l'onglet du navigateur ne peut pas être fermé pendant l'exécution de la boucle, du moins pas dans Chrome.

Pour que le navigateur reste réactif, c'est-à-dire qu'il puisse réagir en permanence aux opérations de l'utilisateur avec une vitesse suffisante, la logique du code doit être telle qu'aucun calcul ne peut prendre trop de temps.

Il existe une foule de documents supplémentaires sur le sujet à trouver sur Internet. Une présentation particulièrement claire du sujet est le discours d'ouverture de Philip Roberts intitulé Qu'est-ce que c'est que la boucle d'événement de toute façon ?

Dans les navigateurs d'aujourd'hui, il est possible d'exécuter du code parallélisé à l'aide de ce qu'on appelle des web workers. La boucle d'événements d'une fenêtre de navigateur individuelle n'est cependant toujours gérée que par un fil unique.

npm

Revenons au sujet de la récupération des données du serveur.

Nous pourrions utiliser la fonction basée sur la promise mentionnée précédemment fetch pour extraire les données du serveur. Fetch est un excellent outil. Il est standardisé et supporté par tous les navigateurs modernes (hors IE).

Cela étant dit, nous utiliserons plutôt la bibliothèque axios pour la communication entre le navigateur et le serveur. Axios fonctionne comme fetch, mais est un peu plus agréable à utiliser. Une autre bonne raison d'utiliser axios est que nous nous familiarisons avec l'ajout de bibliothèques externes, appelées packages npm, aux projets React.

De nos jours, pratiquement tous les projets JavaScript sont définis à l'aide du gestionnaire de packages de noeuds, alias npm. Les projets créés à l'aide de create-react-app suivent également le format npm. Un indicateur clair qu'un projet utilise npm est le fichier package.json situé à la racine du projet :

{
  "name": "notes",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.1",
    "@testing-library/react": "^12.1.2",
    "@testing-library/user-event": "^13.5.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "5.0.0",
    "web-vitals": "^2.1.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

À ce stade, la partie dépendances nous intéresse le plus car elle définit les dépendances, ou bibliothèques externes, du projet.

Nous voulons maintenant utiliser axios. Théoriquement, on pourrait définir la librairie directement dans le fichier package.json, mais il vaut mieux l'installer depuis la ligne de commande.

npm install axios

NB Les commandes npm doivent toujours être exécutées dans le répertoire racine du projet, où se trouve le fichier package.json.

Axios est désormais inclus parmi les autres dépendances :

{
  "name": "notes",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.1",
    "@testing-library/react": "^12.1.2",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^0.24.0",    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "5.0.0",
    "web-vitals": "^2.1.3"
  },
  // ...
}

En plus d'ajouter axios aux dépendances, la commande npm install a également téléchargé le code de la bibliothèque. Comme pour les autres dépendances, le code se trouve dans le répertoire node_modules situé à la racine. Comme on a pu le remarquer, node_modules contient une bonne quantité de choses intéressantes.

Faisons un autre ajout. Installez json-server en tant que dépendance de développement (utilisée uniquement pendant le développement) en exécutant la commande :

npm install json-server --save-dev

et faire un petit ajout à la partie scripts du fichier package.json :

{
  // ... 
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "server": "json-server -p3001 --watch db.json"  },
}

Nous pouvons maintenant, sans définitions de paramètres, démarrer le json-server à partir du répertoire racine du projet avec la commande :

npm run server

Nous nous familiariserons davantage avec l'outil npm dans la troisième partie du cours.

NB Le serveur json précédemment démarré doit être terminé avant d'en démarrer un nouveau ; sinon il y aura des problèmes :

fullstack content

Le rouge dans le message d'erreur nous informe du problème :

Cannot bind to port 3001. Please specify another port number either through --port argument or through the json-server.json configuration file

Comme nous pouvons le voir, l'application n'est pas capable de se lier au port. La raison étant que le port 3001 est déjà occupé par le serveur json précédemment démarré.

Nous avons utilisé la commande npm install deux fois, mais avec de légères différences :

npm install axios
npm install json-server --save-dev

Il y a une fine différence dans les paramètres. axios est installé en tant que dépendance d'exécution de l'application, car l'exécution du programme nécessite l'existence de la bibliothèque. D'autre part, json-server a été installé en tant que dépendance de développement (--save-dev), puisque le programme lui-même n'en a pas besoin. Il est utilisé pour l'assistance lors du développement de logiciels. Il y aura plus sur les différentes dépendances dans la prochaine partie du cours.

Axios et promises

Nous sommes maintenant prêts à utiliser axios. À l'avenir, json-server est supposé s'exécuter sur le port 3001.

NB : Pour exécuter simultanément json-server et votre application React, vous devrez peut-être utiliser deux fenêtres de terminal. L'une pour maintenir json-server en cours d'exécution et l'autre pour exécuter react-app.

La bibliothèque peut être mise en service de la même manière que d'autres bibliothèques, par ex. React, c'est-à-dire en utilisant une instruction import appropriée.

Ajoutez ce qui suit au fichier index.js :

import axios from 'axios'

const promise = axios.get('http://localhost:3001/notes')
console.log(promise)

const promise2 = axios.get('http://localhost:3001/foobar')
console.log(promise2)

Si vous ouvrez http://localhost:3000 dans le navigateur, cela devrait être imprimé sur la console

fullstack content

Remarque : lorsque le contenu du fichier index.js change, React ne le remarque pas toujours automatiquement, vous devrez donc peut-être actualiser le navigateur pour voir vos modifications ! Une solution de contournement simple pour que React remarque automatiquement le changement consiste à créer un fichier nommé .env dans le répertoire racine du projet et à ajouter cette ligne FAST_REFRESH=false. Redémarrez l'application pour que les modifications appliquées prennent effet.

La méthode get d'Axios renvoie une promise.

La documentation sur le site de Mozilla indique ce qui suit à propos des promises:

Une Promise est un objet représentant l'achèvement ou l'échec éventuel d'une opération asynchrone.

En d'autres termes, une promise est un objet qui représente une opération asynchrone. Une promise peut avoir trois états distincts :

  1. La promise est en attente : cela signifie que la valeur finale (l'une des deux suivantes) n'est pas encore disponible.
  2. La promise est tenue : cela signifie que l'opération est terminée et que la valeur finale est disponible, ce qui est généralement une opération réussie. Cet état est parfois aussi appelé résolu.
  3. La promise est rejetée : cela signifie qu'une erreur a empêché la détermination de la valeur finale, ce qui représente généralement une opération échouée.

La première promise dans notre exemple est fulfilled, représentant une requête axios.get('http://localhost:3001/notes') réussie. La seconde, cependant, est rejetée, et la console nous en donne la raison. Il semble que nous essayions de faire une requête HTTP GET à une adresse inexistante.

Si, et quand, nous voulons accéder au résultat de l'opération représentée par la promise, nous devons associer un gestionnaire d'événements à la promise. Ceci est réalisé en utilisant la méthode then :

const promise = axios.get('http://localhost:3001/notes')

promise.then(response => {
  console.log(response)
})

Ce qui suit est renvoyé sur la console :

fullstack content

L'environnement d'exécution JavaScript appelle la fonction de rappel enregistrée par la méthode then en lui fournissant un objet response en tant que paramètre. L'objet response contient toutes les données essentielles liées à la réponse d'une requête HTTP GET, qui inclurait les données renvoyées, le code d'état et en-têtes.

Stocker l'objet promise dans une variable est généralement inutile, et il est plutôt courant d'enchaîner l'appel de méthode then à l'appel de méthode axios, de sorte qu'il le suive directement :

axios.get('http://localhost:3001/notes').then(response => {
  const notes = response.data
  console.log(notes)
})

La fonction callback prend maintenant les données contenues dans la réponse, les stocke dans une variable et affiche les notes sur la console.

Une façon plus lisible de formater les appels de méthode chaînés consiste à placer chaque appel sur sa propre ligne :

axios
  .get('http://localhost:3001/notes')
  .then(response => {
    const notes = response.data
    console.log(notes)
  })

Les données renvoyées par le serveur sont du texte brut, essentiellement une seule longue chaîne. La bibliothèque axios est toujours capable d'analyser les données dans un tableau JavaScript, puisque le serveur a spécifié que le format de données est application/json ; charset=utf-8 (voir image précédente) en utilisant l'en-tête content-type.

Nous pouvons enfin commencer à utiliser les données récupérées sur le serveur.

Essayons de demander les notes à notre serveur local et de les rendre, initialement en tant que composant App. Veuillez noter que cette approche présente de nombreux problèmes, car nous n'affichons l'intégralité du composant App que lorsque nous récupérons avec succès une réponse :

import React from 'react'
import ReactDOM from 'react-dom/client'
import axios from 'axios'
import App from './App'

axios.get('http://localhost:3001/notes').then(response => {
  const notes = response.data
  ReactDOM.createRoot(document.getElementById('root')).render(<App notes={notes} />)
})

Cette méthode pourrait être acceptable dans certaines circonstances, mais elle est quelque peu problématique. Déplaçons plutôt la récupération des données dans le composant App.

Ce qui n'est pas immédiatement évident, cependant, c'est où la commande axios.get doit être placée dans le composant.

Hooks d'effet

Nous avons déjà utilisé les state hooks qui ont été introduits avec la version React 16.8.0, qui fournissent un état aux composants React définis comme des fonctions - les soi-disant composants fonctionnels. La version 16.8.0 introduit également les Hooks d'effet en tant que nouvelle fonctionnalité. Selon la doc officielle :

Le hook d'effet vous permet d'effectuer des effets secondaires sur les composants fonctionnels. La récupération de données, la configuration d'un abonnement et la modification manuelle du DOM dans les composants React sont tous des exemples d'effets secondaires.

En tant que tels, les hooks d'effet sont précisément le bon outil à utiliser lors de la récupération de données à partir d'un serveur.

Supprimons la récupération des données de index.js. Puisque nous allons récupérer les notes du serveur, il n'est plus nécessaire de transmettre des données en tant qu'accessoires au composant App. Donc index.js peut être simplifié en :

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

Le composant App change comme suit :

import { useState, useEffect } from 'react'import axios from 'axios'import Note from './components/Note'

const App = () => {  const [notes, setNotes] = useState([])  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  useEffect(() => {    console.log('effect')    axios      .get('http://localhost:3001/notes')      .then(response => {        console.log('promise fulfilled')        setNotes(response.data)      })  }, [])  console.log('render', notes.length, 'notes')
  // ...
}

Nous avons également ajouté quelques impressions utiles, qui clarifient la progression de l'exécution.

Ceci est affiché sur la console :


render 0 notes
effect
promise fulfilled
render 3 notes

Tout d'abord, le corps de la fonction définissant le composant est exécuté et le composant est rendu pour la première fois. À ce stade, render 0 notes est imprimé, ce qui signifie que les données n'ont pas encore été extraites du serveur.

La fonction ou l'effet suivant dans le jargon React :

() => {
  console.log('effect')
  axios
    .get('http://localhost:3001/notes')
    .then(response => {
      console.log('promise fulfilled')
      setNotes(response.data)
    })
}

est exécuté immédiatement après le rendu. L'exécution de la fonction entraîne l'impression de l'effet sur la console, et la commande axios.get lance la récupération des données du serveur et enregistre la fonction suivante en tant que un gestionnaire d'événements pour l'opération :

response => {
  console.log('promise fulfilled')
  setNotes(response.data)
}

Lorsque les données arrivent du serveur, le runtime JavaScript appelle la fonction enregistrée en tant que gestionnaire d'événements, qui affiche promise fulfilled sur la console et stocke les notes reçues du serveur dans l'état à l'aide de la fonction setNotes(response.data).

Comme toujours, un appel à une fonction de mise à jour d'état déclenche le nouveau rendu du composant. Par conséquent, render 3 notes est affiché sur la console et les notes récupérées sur le serveur sont rendues à l'écran.

Enfin, regardons la définition du hook d'effet dans son ensemble :

useEffect(() => {
  console.log('effect')
  axios
    .get('http://localhost:3001/notes').then(response => {
      console.log('promise fulfilled')
      setNotes(response.data)
    })
}, [])

Réécrivons le code un peu différemment.

const hook = () => {
  console.log('effect')
  axios
    .get('http://localhost:3001/notes')
    .then(response => {
      console.log('promise fulfilled')
      setNotes(response.data)
    })
}

useEffect(hook, [])

Maintenant, nous pouvons voir plus clairement que la fonction useEffect prend en fait deux paramètres. Le premier est une fonction, l'effet lui-même. Selon la documentation :

Par défaut, les effets s'exécutent après chaque rendu terminé, mais vous pouvez choisir de ne les déclencher que lorsque certaines valeurs ont changé.

Ainsi, par défaut, l'effet est toujours exécuté après le rendu du composant. Dans notre cas, cependant, nous ne voulons exécuter l'effet qu'avec le premier rendu.

Le deuxième paramètre de useEffect est utilisé pour spécifier la fréquence d'exécution de l'effet. Si le deuxième paramètre est un tableau vide [], alors l'effet n'est exécuté qu'avec le premier rendu du composant.

Il existe de nombreux cas d'utilisation possibles pour un hook d'effet autres que la récupération de données à partir du serveur. Cependant, cette utilisation nous suffit, pour l'instant.

Repensez à la séquence d'événements dont nous venons de parler. Quelles parties du code sont exécutées ? Dans quel ordre? À quelle fréquence? Comprendre l'ordre des événements est essentiel !

Notez que nous aurions également pu écrire le code de la fonction d'effet de cette façon :

useEffect(() => {
  console.log('effect')

  const eventHandler = response => {
    console.log('promise fulfilled')
    setNotes(response.data)
  }

  const promise = axios.get('http://localhost:3001/notes')
  promise.then(eventHandler)
}, [])

Une référence à une fonction de gestion d'événements est affectée à la variable eventHandler. La promise renvoyée par la méthode get d'Axios est stockée dans la variable promise. L'enregistrement du rappel se produit en donnant la variable eventHandler, faisant référence à la fonction de gestion d'événements, en tant que paramètre de la méthode then de la promise. Il n'est généralement pas nécessaire d'assigner des fonctions et des promises aux variables, et une manière plus compacte de représenter les choses, comme vu plus haut, est suffisante.

useEffect(() => {
  console.log('effect')
  axios
    .get('http://localhost:3001/notes')
    .then(response => {
      console.log('promise fulfilled')
      setNotes(response.data)
    })
}, [])

Nous avons toujours un problème dans notre application. Lors de l'ajout de nouvelles notes, elles ne sont pas stockées sur le serveur.

Le code de l'application, tel que décrit jusqu'à présent, peut être trouvé dans son intégralité sur github, sur la branche part2-4.

Environnement d'exécution de développement

La configuration de l'ensemble de l'application est devenue de plus en plus complexe. Passons en revue ce qui se passe et où. L'image suivante décrit la composition de l'application

fullstack content

Le code JavaScript composant notre application React est exécuté dans le navigateur. Le navigateur obtient le JavaScript du serveur de développement React, qui est l'application qui s'exécute après l'exécution de la commande npm start. Le dev-server transforme le JavaScript dans un format compris par le navigateur. Entre autres choses, il assemble le JavaScript de différents fichiers en un seul fichier. Nous aborderons le dev-server plus en détail dans la partie 7 du cours.

L'application React s'exécutant dans le navigateur récupère les données au format JSON à partir de json-server s'exécutant sur le port 3001 de la machine. Le serveur à partir duquel nous interrogeons les données - json-server - obtient ses données à partir du fichier db.json.

À ce stade du développement, toutes les parties de l'application résident sur la machine du développeur, également appelée localhost. La situation change lorsque l'application est déployée sur Internet. Nous le ferons dans la partie 3.