Créer son propre plugin source GatsbyJS pour consommer une API REST — 2 : Cache et noeuds persistants

Mélanie Paque
Publicis Sapient France
8 min readFeb 21, 2023

Cet article est la suite de l’article : Créer son propre plugin source GatsbyJS pour consommer une API REST — 1 : création du plugin source

Nous avons vu dans le précédent article comment créer un plugin source Gatsby. Par le biais de celui-ci, nous avons créé une nouvelle source de données consommable par Gatsby.

Nous allons voir ici comment utiliser le cache afin de ne réaliser la génération des nœuds de données que lorsque celle-ci est nécessaire.

Utilisation du cache

Gatsby fournit une API de cache permettant de mettre en cache des objets JSON et de les récupérer entre des builds successifs. Ces données sont conservées dans le fichier .cache, généré lors du build.

L’API de cache est transmise via les Node APIs de Gatsby.

Pour pouvoir l’utiliser, nous n’avons qu’à l’ajouter à la suite des helpers que nous avons déjà utilisé précédemment :

./plugins/gatsby-source-countries/gatsby-node.js

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache,
}) => {
//...
}

Pourquoi utiliser le cache ?

La récupération et le traitement de données peuvent avoir une incidence lourde sur les performances. Afin de limiter celle-ci, il paraît alors judicieux de ne réaliser un traitement que si les données ont été modifiées. Nous éviterons ainsi, à chaque build, de recréer l’ensemble des noeuds.

Pour cela nous devons pouvoir déterminer si les données ont été mises à jour entre le build courant et le build précédent.

Il existe plusieurs moyens de savoir si la réponse d’une API a changé. Par exemple :

  • en-tête HTTP : la réponse d’une API possède généralement dans son header des entêtes tel que le ETag ou If-Modified-Since.
  • paramètre transmis à l’API : d’autres API vous permettront de faire une requête avec en paramètre la dernière date de modification, etc.

Concernant notre exemple, et parce que l’API REST Countries est un service relativement simple qui ne propose pas de système indiquant une date de mise à jour dans sa réponse comme au sein de ses données, nous utiliserons une simple comparaison des réponses entre celle reçue au moment du build et celle reçue lors du précédent build.

Réalisons notre première mise en cache

Afin de comparer la réponse de l’API lors du build courant avec celle du build précédent, nous allons stocker cette dernière dans le cache.

Nous allons d’abord définir la clé de cache que nous utiliserons et à laquelle nous stockerons la réponse.

Dans notre fichier gatsby-node.js, nous ajoutons une variable CACHE_KEY qui contiendra nos clés.

L’appel à REST Countries que nous avons implémenté dans notre plugin pour la récupération de données visant à obtenir une liste de tous les pays, notre clé sera all-last-response :

./plugins/gatsby-source-countries/gatsby-node.js

const fetch = require('node-fetch')
const CACHE_KEY = 'restcountries-last-response'

Nous pouvons alors attribuer une valeur en cache avec la fonction set :

./plugins/gatsby-source-countries/gatsby-node.js

const fetch = require('node-fetch')
const CACHE_KEY = 'restcountries-last-response'

exports.onPreInit = () => console.log("Loaded gatsby-source-countries")

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache
}) => {
const {createNode} = actions

try {
const response = await fetch('https://restcountries.com/v3.1/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const countries = await response.json()
cache.set(CACHE_KEY, JSON.stringify(countries))
//...
}

ou récupérer la valeur en cache avec la fonction get :

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache
}) => {
const {createNode} = actions

try {
const response = await fetch('https://restcountries.com/v3.1/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const countries = await response.json()
const responseCached = await cache.get(CACHE_KEY)
cache.set(CACHE_KEY, JSON.stringify(countries))
//...
}

Notre objectif étant de savoir si les données renvoyée par l’API ont changé entre nos builds successifs, nous voyons apparaître trois cas de figure possibles :

  1. Aucune valeur n’est en cache à la clé restcountries-last-response, aucune réponse n’a donc été stockée en cache et nous allons l’initialiser.
  2. Il existe une valeur en cache à la clé restcountries-last-response et celle-ci est identique à la réponse obtenue par l’API REST Countries, les données n’ont pas changé depuis le build précédent.
  3. Il existe une valeur en cache à la clé restcountries-last-response et celle-ci est différente de la réponse obtenue par l’API REST Countries, les données ont changé depuis le build précédent.

./plugins/gatsby-source-countries/gatsby-node.js

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache
}) => {
const {createNode} = actions

try {
const response = await fetch('https://restcountries.com/v3.1/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const countries = await response.json()
const responseCached = await cache.get(CACHE_KEY)

if (!responseCached) {
console.log('init restcountries-last-response cached value')
} else {
if (JSON.stringify(countries) === responseCached) {
console.log('data has not changed since last build')
} else {
console.log('data has changed since last build')
}
}

cache.set(CACHE_KEY, JSON.stringify(countries))
//...
}

Pour tester, lancer vos build successivement afin de voir en console les messages correspondant s’affichant :

Au premier build, le message “init restcountries-last-response cached value” s’affiche en console, puis aux builds suivants “data has not changed since last build”.

Pour nettoyer votre cache, vous pouvez lancez la commande suivante :

npm run clean

ou si le script clean n’a pas été définis dans votre package.json :

gatsby clean

Nous allons conserver le résultat de cette comparaison dans une variable hasChanged.

Pour cela nous allons créer un objet execution. Il comprendra le type de nœud de l’exécution ainsi que notre fameux hasChanged, un booléen que nous définissons par défaut à false.

Nous passerons hasChanged à true lorsque nous entrerons dans le cas d’initialisation de la valeur en cache ou le cas où les données ont été modifiées.

./plugins/gatsby-source-countries/gatsby-node.js

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache
}) => {
const {createNode} = actions

const execution = {
nodeType: 'RestcountriesCountry',
hasChanged: false,
}

try {
const response = await fetch('https://restcountries.com/v3.1/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const countries = await response.json()
const responseCached = await cache.get(CACHE_KEY)

if (!responseCached) {
cache.set(CACHE_KEY, JSON.stringify(countries))
execution.hasChanged = true
} else {
if (JSON.stringify(countries) !== responseCached) {
execution.hasChanged = true
}
}

cache.set(CACHE_KEY, JSON.stringify(countries))
//...
}

Nous pourrons désormais savoir si nos données ont évolué entre les builds successifs.

Cache et noeuds persistants

Qu’est ce que les noeuds persistants ?

Comme leur nom l’indique, les noeuds persistants sont des noeuds qui persistent entre chaque build. Tous les noeuds le sont automatiquement.

Lors du développement d’un plugin source, nous pouvons utiliser cet avantage offert par le cache pour ne traiter que les données ayant changé depuis le dernier build. Cela représente un gain de temps.

En effet, lorsque les données n’ont pas été modifiées, il n’est pas utile de recréer une nouvelle fois tous les nœuds alors qu’ils sont identiques au précédent build.

Préparations des noeuds et petite réorganisation du code

Avant d’aller plus loin, nous allons réorganiser un peu notre code afin de séparer les étapes suivantes :

  • préparation des noeuds (via l’usage des noeuds persistants)
  • récupération des données
  • traitement des données

Commençons par la récupération des données :

Nous allons déplacer notre appel à l’API dans une fonction asynchrone fetchCountries.

Nous ajoutons à notre objet execution un tableau items, dans lequel nous stockons notre tableau de countries récupéré via l’appel API.

Puis nous appelons la fonction fetchCountries dans l’instruction try.

./plugins/gatsby-source-countries/gatsby-node.js

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache
}) => {
const {createNode} = actions

const execution = {
nodeType: 'RestcountriesCountry',
hasChanged: false,
items: [],
}

const fetchCountries = async () => {
const response = await fetch('https://restcountries.com/v3.1/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const countries = await response.json()
execution.items = countries
const responseCached = await cache.get(CACHE_KEY)

if (!responseCached) {
console.log('init restcountries-last-response cached value')
execution.hasChanged = true
} else {
if (JSON.stringify(countries) !== responseCached) {
execution.hasChanged = true
}
}

cache.set(CACHE_KEY, JSON.stringify(countries))
}

try {
await fetchCountries()
//...
}

Voyons maintenant à l’étape de préparation des noeuds :

Nous avons vu précédemment ce qu’étaient les noeuds persistant. Bien que les nœuds persistent automatiquement entre les builds successif, il y a tout de même une action à réaliser : appeler la fonction touchNode sur chacun de ces nœuds. Cette action va indiquer à Gatsby que nous souhaitons garder les nœuds et de ne pas mettre ceux-ci dans le garbage collector bien qu’ils n’aient pas été ni modifiés ni supprimés.

Nous allons réaliser cette action dans une fonction que nous nommerons prepareNodes et que nous appelons avant d’entrer dans l’instruction try.

Nous déclarons ainsi la fonction prepareNodes qui prend en paramètre l’exécution.

Nous y récupérons les nodes persistant de type RestcountriesCountry grâce au helper getNodesByType. Nous ajoutons à execution un tableau nodes, dans lequel stockons les noeuds ainsi récupérés.

Pour chacun de ces nœuds, nous appelons touchNode.

./plugins/gatsby-source-countries/gatsby-node.js

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache
}) => {

const {createNode, touchNode} = actions

const execution = {
nodeType: 'RestcountriesCountry',
hasChanged: false,
items: [],
nodes: [],
}

const prepareNodes = (execution) =>
new Promise((resolve) => {
const {nodeType} = execution
execution.nodes = getNodesByType(nodeType)
for (const node of execution.nodes) {
touchNode(node)
}
resolve()
})

const fetchCountries = async () => {
//...
}

await prepareNodes(execution)

try {
await fetchCountries()
//...
}

Enfin, l’étape de traitement des données :

Nous allons déplacer la création de nos noeuds dans une fonction processData, dans laquelle nous réaliserons les traitements des nœuds.

ProcessData prendra en paramètre l’exécution.

Nous accédons au tableau items de l’exécution, soit les données récupérées via l’API, itérons et créons un noeud pour chacun des items.

Nous appelons processData après avoir effectué la récupération de données via l’API.

./plugins/gatsby-source-countries/gatsby-node.js

const fetchCountries = async () => {
//...
}

const processData = (execution) =>
new Promise((resolve) => {
const {hasChanged, items} = execution

if (hasChanged) {
for (const item of items) {
const nodeContent = JSON.stringify(item)

createNode({
...item,
id: createNodeId(`restcountries-country-${item.name.common}`),
parent: null,
children: [],
internal: {
type: 'RestcountriesCountry',
mediaType: `application/json`,
content: nodeContent,
contentDigest: createContentDigest(item),
},
})
}
}
resolve()
})

await prepareNodes(execution)

try {
await fetchCountries()
await processData(execution)
}

Au final nous obtenons :

./plugins/gatsby-source-countries/gatsby-node.js

const fetch = require('node-fetch')
const CACHE_KEY = 'restcountries-last-response'

exports.onPreInit = () => console.log('Loaded gatsby-source-countries')

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache,
}) => {

const { createNode, touchNode } = actions

const execution = {
nodeType: 'RestcountriesCountry',
hasChanged: false,
items: [],
nodes: [],
}

const prepareNodes = (execution) =>
new Promise((resolve) => {
const { nodeType } = execution
execution.nodes = getNodesByType(nodeType)
for (const node of execution.nodes) {
touchNode(node)
}
resolve()
})

const fetchCountries = async () => {
const response = await fetch('https://restcountries.com/v3.1/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const countries = await response.json()

execution.items = countries

const responseCached = await cache.get(CACHE_KEY)
if (!responseCached) {
console.log('init restcountries-last-response cached value')
execution.hasChanged = true
} else {
if (JSON.stringify(countries) !== responseCached) {
execution.hasChanged = true
}
}
cache.set(CACHE_KEY, JSON.stringify(countries))
}

const processData = (execution) =>
new Promise((resolve) => {
const { hasChanged, items } = execution

if (hasChanged) {
for (const item of items) {
const nodeContent = JSON.stringify(item)

createNode({
...item,
id: createNodeId(`restcountries-country-${item.name.common}`),
parent: null,
children: [],
internal: {
type: 'RestcountriesCountry',
mediaType: `application/json`,
content: nodeContent,
contentDigest: createContentDigest(item),
},
})
}
}
resolve()
})

await prepareNodes(execution)

try {
await fetchCountries()
await processData(execution)
} catch (e) {
console.error(e)
reporter.error(e.message)
process.exit()
}
}

Pour conclure

En utilisant le cache et en effectuant la génération de données uniquement lorsque celle-ci est nécessaire, nous avons amélioré notre plugin et ses performances.

Le traitement n’est pour autant pas encore optimal. En effet, lorsque nous détectons que les données ont été mises à jour, nous recréons tous les noeuds. Or tous les nœuds n’ont pas forcément été mis à jour. Certains ont pu être modifiés, d’autres supprimés, etc. Nous devrons donc affiner notre traitement des données.

Ce sera le sujet du prochain article de cette série.

Ressources :

Vous pouvez retrouver l’ensemble du code du tutoriel sur la page GitHub.

Gatsby — cache API

Gatsby — touchNode

--

--