PWA : Intercepter les requĂȘtes HTTP et les mettre en cache
dimanche 24 décembre 2017
Le terme Progressive Web Apps (PWA) vient de Frances Berriman et Alex Russel. Le but derriĂšre ce nouveau mot est de promouvoir une façon de penser et de concevoir des sites webs. Ce nâest donc pas une technologie. Le but de ce terme est purement marketing afin de convaincre les gens de faire mieux : sâil apparaĂźt dans tous les fils de discussion, on est bien obligĂ© de sây intĂ©resser Ă un moment ou Ă un autre. Et ils y arrivent plutĂŽt bien. On le voit partout.
Voici donc une sĂ©rie dâarticles oĂč jâessaierai de vous prĂ©senter ce que câest et surtout comment le mettre en place pour vos utilisateurs. Accrochez-vous, ça fait pas mal de choses Ă dĂ©couvrir :
- Rendre un site web disponible grĂące aux Services Workers
- Déclarer un Service Worker et gérer son cycle de vie
- Intercepter les requĂȘtes HTTP et les mettre en cache (Vous ĂȘtes ici.)
- Proposer une expérience hors ligne (à paraßtre)
Dans les articles prĂ©cĂ©dents, je prĂ©sentais ce quâest une PWA et un Service Worker. Puis, nous avons vu comment dĂ©clarer un Service Worker et gĂ©rer la liaison avec une page web.
Maintenant, il est temps dâutiliser tout ça pour commencer Ă amĂ©liorer lâexpĂ©rience de nos utilisateurs
. Pour cela, nous allons faire en sorte quâun utilisateur puisse revenir sur notre site et accĂ©der au contenu quel que soit lâĂ©tat de sa connexion internet.Je vais ainsi commencer par prĂ©senter la fonctionnalitĂ© phare des Service Workers qui permet dâatteindre ce but : lâinterception de requĂȘtes. Ensuite, nous verrons comment nous pouvons coupler cela Ă la Cache API pour rĂ©utiliser des requĂȘtes dĂ©jĂ Ă©mises et ainsi Ă©viter dâĂȘtre dĂ©pendant de la connexion internet.
A la fin de cet article vous aurez donc toutes les billes Ă votre disposition pour rendre votre site disponible. Cependant, nous nâen serons encore quâĂ la prĂ©sentation des dĂ©tails techniques de ces fonctionnalitĂ©s. Il faudra attendre le dernier article pour voir comment assembler tout ça pour proposer une version hors ligne de votre site Ă vos utilisateurs
.Intercepter les requĂȘtes HTTP
Commençons donc par lâinterception de requĂȘtes. LâidĂ©e de cette fonctionnalitĂ© est de se positionner entre le site web et internet pour pouvoir Ă©viter de passer par internet quand câest possible.
PrĂ©cĂ©demment, nous avons vu quâil y a des Ă©vĂšnements sur lesquels nous pouvons nous brancher dans un Service Worker. Il y a par exemple install
et activate
qui offrent la possibilitĂ© de gĂ©rer le cycle de vie du Service Worker. Pour mettre en place lâinterception de requĂȘtes, il existe un autre Ă©vĂšnement : fetch
. Celui-ci est Ă©mis Ă chaque fois quâune page liĂ©e Ă votre Service Worker Ă©met une requĂȘte HTTP.
Ainsi, en se branchant Ă fetch
, nous pouvons voir ce qui est demandé par la page (event.request
) et modifier la réponse en utilisant event.respondWith
.
Ainsi, si je veux faire en sorte dâintercepter la requĂȘte vers /toto
dans ma page, il faut que jâĂ©crive le code suivant :
// fichier /service-worker.js
// On se branche sur chaque requĂȘte Ă©mise
self.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
// Si la requĂȘte est bien celle que l'on
// veut simuler
if (requestUrl.pathname === '/toto') {
// Alors, on modifie la réponse
event.respondWith(
// Ici je crée une réponse à partir
// de rien qui contient uniquement
// "Hello Toto"
new Response(new Blob(['Hello Toto'], { type: 'text/html' }), {
status: 200,
statusText: 'OK',
headers: {
'Content-Type': 'text/html'
}
})
);
}
});
Si vous executez cet exemple, et que vous allez sur /toto
, votre navigateur affichera âHello Totoâ. Si ce nâest pas le cas, câest que votre Service Worker nâa pas Ă©tĂ© dĂ©clarĂ© (cf. lâarticle prĂ©cĂ©dent pour voir comment dĂ©clarer le Service Worker).
Il nous est donc possible dâintercepter une requĂȘte et afficher du contenu sans passer par le serveur. En lâoccurence, câest une page HTML que lâon affiche, mais cela pourrait ĂȘtre nâimporte quoi : des fichiers CSS, des fichiers JS, des requĂȘtes API, etc.
Prévoir une solution de secours
LĂ tout de suite, vous ne vous en rendez peut-ĂȘtre pas compte Ă©tant donnĂ© quâon ne renvoie quâun âHello Totoâ, mais câest trĂšs puissant comme fonctionnalitĂ©.
Il devient par exemple assez tentant de vouloir ajouter des traitements sur les requĂȘtes au niveau du Service Worker. On peut rapidement en devenir dĂ©pendant. Cependant il est trĂšs important de se dĂ©brouiller pour que le site fonctionne sans Service Worker.
Il est par exemple tout Ă fait possible que le navigateur de lâutilisateur ne supporte pas encore les Service Workers ou que ceux-ci soient dĂ©sactivĂ©s. Mais il est aussi possible que votre page se retrouve dĂ©tachĂ©e du Service Worker pour on ne sait quelle raison (a.k.a. un bug).
Ainsi, il est interdit de faire en sorte que certaines requĂȘtes ne fonctionnent que si un Service Worker est prĂ©sent. Il faudra plutĂŽt mettre en place un serveur qui fournisse /toto
pour que la requĂȘte fonctionne aussi lorsque le Service Worker est absent. Ainsi, sâil y a un Service Worker, on amĂ©liore les performances ressenties. Sâil nây en a pas, on a toujours accĂšs au contenu.
Intercepter pour mettre en cache
Ok, mais lâinterception de requĂȘte pour faire des rĂ©ponses sorties de nulle part, ce nâest pas trĂšs intĂ©ressant dans la vie de tous les jours. Attaquons nous donc au coeur du sujet.
Notre but initial est de rendre notre site web disponible, afin que lâutilisateur continue dâaccĂšder au site en Ă©tant hors ligne (ou lorsque le WiFi a sautĂ© ). Pour y arriver, nous allons rĂ©utiliser les donnĂ©es dĂ©jĂ rĂ©cupĂ©rĂ©es depuis le serveur et les servir Ă nouveau si lâutilisateur fait la mĂȘme requĂȘte. Câest le principe de mise en cache.
Cache API
Ca tombe bien, il y a une API prévue pour ça : la Cache API. Celle-ci est disponible via la variable caches
. LâidĂ©e derriĂšre cette API est de pouvoir stocker une rĂ©ponse pour une requĂȘte donnĂ©e. Ainsi, quand on reçoit une nouvelle requĂȘte, on peut vĂ©rifier sâil y a dĂ©jĂ une rĂ©ponse stockĂ©e ou non.
// Stockage d'une réponse pour
// une requĂȘte spĂ©cifique
// https://developer.mozilla.org/fr/docs/Web/API/Cache/put
cache.put(request, response.clone()).then(() => {
console.log(`
Réponse mise en cache et
associĂ©e Ă la requĂȘte donnĂ©e.
`);
});
// RĂ©cupĂ©ration d'une requĂȘte
// depuis le cache
// https://developer.mozilla.org/fr/docs/Web/API/Cache/match
cache.match(request).then((response) => {
if (response) {
console.log('Réponse déjà en cache.', response);
} else {
console.log(`
Pas encore de cache pour cette requĂȘte
`);
}
});
// Suppression d'un élément
// du cache
// https://developer.mozilla.org/fr/docs/Web/API/Cache/delete
cache.delete(request).then(() => {
console.log(`
Cache supprimĂ© pour la requĂȘte donnĂ©e.
`);
});
A noter quâon a bien fait attention Ă cloner la requĂȘte avant de la stocker (.clone()
). Cela permet dâĂ©viter tout effet de bord lors de la rĂ©cupĂ©ration du corps de la requĂȘte (cf. Response.clone() sur MDN).
Un deuxiĂšme point Ă noter est quâil est possible de sĂ©parer les diffĂ©rents de types de caches. Cela facilite notamment la suppression dâune partie du cache de maniĂšre ciblĂ©e. Il est par exemple souvent pertinent dâavoir deux caches diffĂ©rents pour les assets et les requĂȘtes API.
Lexique : Je vais rĂ©guliĂšrement parler dâassets et de requĂȘtes API. Dans le cadre de cet article, il faudra comprendre ces termes comme suit :
- Asset : tout fichier statique permettant dâafficher votre page web (javascript, css, images, etc.)
- RequĂȘte API : toute requĂȘte qui permet de rĂ©cupĂ©rer le contenu dynamique de la page. GĂ©nĂ©ralement il sâagit dâobjets JSON, mais ça peut ĂȘtre du HTML, du GraphQL, etc.
Ainsi, si lâon veut gĂ©rer les requĂȘtes/rĂ©ponses dâune partie du cache seulement, il faut utiliser open
 :
caches.open('Nom du cache').then((cache) => {
// Ici on a accÚs aux méthodes
// cache.put, cache.match et
// cache.delete
});
Mise en place du cache des requĂȘtes
Maintenant que nous sommes capables de mettre des choses dans le cache, il faut se demander quoi mettre en cache, dans quel contexte, etc. On va parler de stratégies.
Câest intimement liĂ© Ă la nature du site web. Potentiellement, on pourrait imaginer une infinitĂ© de stratĂ©gies diffĂ©rentes. Cela dit, les principales stratĂ©gies sont :
- Network Only : on ne veut pas de cache car lâopĂ©ration est critique/ne peut pas fonctionner hors ligne. Si ce nâest quâune partie de lâapplication, il est important dâexpliquer clairement au niveau de lâinterface pourquoi la fonctionnalitĂ© nâest pas disponible.
- Cache First : on rĂ©cupĂšre en prioritĂ© depuis le cache. Sâil nây a pas encore de cache, on va chercher sur le rĂ©seau et on stocke la rĂ©ponse dans le cache. LâintĂ©rĂȘt est quâune fois quâon a mis quelque chose en cache, on est capable de le servir trĂšs rapidement Ă lâutilisateur. La performance ressentie sâen retrouve grandement amĂ©liorĂ©e.
- Network First : on rĂ©cupĂšre en prioritĂ© depuis le rĂ©seau. Si le rĂ©seau ne rĂ©pond pas, on sert le cache afin dâafficher du contenu. Cela permet dâafficher du contenu qui nâest peut-ĂȘtre plus Ă jour, mais qui a le mĂ©rite dâĂȘtre lĂ .
- Stale While Revalidate : on rĂ©cupĂšre le cache et on lâenvoie. Le contenu est ainsi directement disponible. Ensuite, on va chercher la requĂȘte sur le rĂ©seau pour que ce soit Ă jour la prochaine fois quâon fait la requĂȘte.
Ces quelques méthodes sont disponibles dans Workbox. Vous pouvez aussi retrouver un tout en tas de stratégies dans The offline cookbook de Jake Archibald qui est une trÚs bonne resource sur le sujet.
Comme vous pouvez le voir, chaque stratégie a ses avantages et ses inconvénients. Pour choisir laquelle est la bonne, personnellement, je me pose les questions suivantes :
- Est-ce que la ressource doit ĂȘtre dĂ©livrĂ©e trĂšs rapidement ? Dans ce cas je privilĂ©gie le cache en premier.
- Est-ce que la fiabilité est plus importante que la vitesse ? Dans ce cas, je privilégie le réseau.
Pour faire simple, la rapidité passe par le cache. La fiabilité passe par le réseau.
Cas dâapplication : Cache First
Dans cet article, je nâaurai ni le temps ni la place de dĂ©tailler chacune des stratĂ©gies. Cependant, en partant de lâimplĂ©mentation dâune stratĂ©gie, il est gĂ©nĂ©ralement possible de lâadapter en modifiant lâordre dâexecution pour en arriver Ă une autre.
LâimplĂ©mentation que nous allons voir ensemble est Cache First. Pour rappel, le principe est de voir si une requĂȘte est dĂ©jĂ prĂ©sente dans le cache pour pouvoir la servir la plus rapidement possible. Si elle nây est pas, on fait la requĂȘte rĂ©seau et on met Ă jour le cache.
const ASSETS_CACHE_NAME = 'assets';
// En premier, deux méthodes d'aide
// pour faciliter la lecture de
// `getResponseFromCacheFirst`
const getResponseFromCache = (cacheName, request) => {
// On ouvre le bon cache
return caches.open(cacheName).then((cache) => {
// Et on récupÚre la réponse
// correspondant Ă la requĂȘte
return cache.match(request);
});
};
const setResponseCache = (cachename, request, response) => {
// On ouvre le bon cache
return caches.open(cacheName).then((cache) => {
// Et on stocke la nouvelle
// rĂ©ponse pour la requĂȘte donnĂ©e
return cache.put(request, response);
});
};
// Mise en place de la stratégie
// Cache First pour la requĂȘte
// donnée
const getResponseFromCacheFirst = (cacheName, request) => {
// Cette méthode permet de récupérer la
// rĂ©ponse d'une requĂȘte. Si celle-ci est
// déjà en cache, on répond avec le cache
// en prioritĂ©. Sinon, on fait la requĂȘte,
// on met en cache la réponse, puis on
// renvoie la réponse.
// Récupération depuis le cache
const response = getResponseFromCache(cacheName, request).then((response) => {
if (response) {
// Si la requĂȘte est dĂ©jĂ en cache,
// on renvoie la réponse trouvée
return response;
} else {
// Sinon, on fait la vraie requĂȘte
return fetch(request).then((response) => {
// Une fois qu'on a reçu la
// réponse, on met en cache
// pour la prochaine fois.
// On n'oublie pas de cloner
// la réponse pour pouvoir la
// mettre en cache.
setResponseCache(cacheName, request, response.clone());
// Et on retourne la réponse
return response;
});
}
});
return response;
};
self.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
// Quand on intercepte une requĂȘte,
// si c'est un asset, on applique la
// stratégie Cache First
if (requestUrl.pathname.startsWith('/assets')) {
event.respondWith(getResponseFromCacheFirst(ASSETS_CACHE_NAME, event.request));
}
});
Et voilà , on a mis en place notre premiÚre stratégie de cache ! :)
Pour vĂ©rifier que vous avez bien compris le fonctionnement de ce code, un bon exercice serait de le reprendre et dâimplĂ©menter une autre stratĂ©gie.
Ne pas intercepter nâimporte quoi
Dans lâexemple ci-dessus, vous pouvez constater que je nâai mis en cache que les assets. Il est trĂšs important de ne toucher quâaux URLs que lâon maĂźtrise. Sinon, on peut se retrouver dans des situations dĂ©licates.
Imaginons un instant que nous nâayons pas mis de filtre et que nous interceptions toutes les requĂȘtes pour les mettre en cache. Dans ce cas, au rafraĂźchissement de la page, on nâirait plus chercher les infos sur le serveur. Tout est dĂ©jĂ en cache et va trĂšs vite. Cool !
Cependant, un dĂ©tail auquel on nâa pas pensĂ©, câest quâau chargement de la page, on fait une requĂȘte qui va chercher le nombre de notifications non lues. Etant donnĂ© quâil nây a pas de filtre au niveau de la mise en cache, cette requĂȘte se retrouve elle aussi en cache. Ainsi, si une nouvelle notification arrive, on rĂ©cupĂšre tout de mĂȘme lâancienne valeur. Oups.
Il faut donc faire trĂšs attention. Dâautant plus quâon nâest pas toujours maĂźtre de tout ce qui est sur son site web. Câest par exemple le cas pour les outils dâanalytics, les librairies externes, etc. Mais câest aussi le cas quand une autre Ă©quipe travaille sur une autre partie de lâapplication.
En tant que dĂ©veloppeurDevTools de nos navigateurs sont lĂ vous aider Ă repartir dâun Ă©tat stable :
, lorsque lâon met en place son propre Service Worker, croyez-moi, on finit toujours par perdre du temps sur ce genre dâerreurs. Heureusement, les- Sur Firefox, ouvrez un nouvel onglet Ă lâURL
about:debugging#workers
et cliquez surunregister
sur le service worker qui vous intĂ©resse. - Sur Chrome, dans les DevTools (F12), allez dans lâonglet
Application
> ``Service Workerset cliquez sur
Unregister`.
Cela dit, pour les utilisateurs
de votre site, il nây a pas vraiment de solution miracle. ils seront obligĂ© s dâattendre la mise Ă jour votre Service Worker et la remise Ă plat du cacheâŠMais comment faire cette remise Ă plat ?
Mettre Ă jour le cache
MĂȘme si nous avons tout bien fait, il y a toujours un moment oĂč nous souhaitons invalider/vider le cache. Cela peut se produire quand on publie une nouvelle version du site par exemple. Câest aussi le cas quand le contenu Ă afficher change dans le temps (nouveau commentaire, notification, etc.).
LâidĂ©e est alors dâappeler cache.delete()
. Mais oĂč et quand faut-il le faire ?
Comme dâhabitude, la rĂ©ponse est : ça dĂ©pend. En effet, si câest une requĂȘte API ou un asset, vraisemblablement, ce sera trĂšs diffĂ©rent.
Les assets, par exemple, ont de fortes chances dâĂȘtre mis Ă jour au mĂȘme moment que le Service Worker. LâidĂ©e est alors de tenir une liste de tous les assets Ă aller rĂ©cupĂ©rer lors de lâinstallation de votre nouveau Service Worker.
Au contraire, si des requĂȘtes API ont Ă©tĂ© mises en cache, le sujet devient tout de suite beaucoup plus complexe. Câest tout un systĂšme de synchronisation quâil faut mettre en place si on veut ĂȘtre sĂ»r dâavoir toujours la derniĂšre version. Câest donc un sujet suffisamment difficile pour mĂ©riter un ou plusieurs articles Ă lui tout seul. Cependant, parfois le fait de choisir la bonne stratĂ©gie de cache peut suffire (Network First ?).
Mettre Ă jour le cache des assets
Afin de ne pas trop nous éparpiller, je me contenterai dans cet article de vous présenter une implémentation possible pour la mise en cache de fichiers statiques.
Evidemment, cela dépend de vos assets, mais en général cela se fait en trois étapes :
- Pour chaque version, on liste la totalité des assets à télécharger.
- Lors de la tĂąche dâinstallation du Service Worker, on met en cache tous les nouveaux assets. Ainsi, lorsque le Service Worker est activĂ©, on nâest pas dĂ©pendant des pages qui ont dĂ©jĂ Ă©tĂ© visitĂ©es sur le site : tout est directement disponible.
- Lors de la tĂąche dâactivation du Service Worker, on retire tous les caches qui ne sont plus utiles afin dâĂ©viter que le cache grossisse indĂ©finiment.
Cela donnerait donc quelque chose qui ressemblerait à ça :
const ASSETS_CACHE_NAME = 'assets';
// (1) On recense l'ensemble des assets
// dans une variable.
const assetsList = ['/static/css/main.ez84s6df.css', '/static/js/main.aze4sd31.js'];
// (2) Méthode permettant de mettre en
// cache un asset s'il n'est pas dĂ©jĂ
// en cache
const cacheAsset = (url) => {
// On travaille dans le cache dédié aux
// assets
return caches.open(ASSETS_CACHE_NAME).then((cache) => {
const request = new Request(url);
// On regarde si l'asset est dĂ©jĂ
// en cache (ex: déjà utilisé
// dans le précédent Service Worker)
return cache.match(request).then((response) => {
if (!response) {
// Si pas de cache existant
// on récupÚre l'asset
return fetch(request).then((response) => {
// Et on le met en cache
return cache.put(request, response.clone());
});
}
});
});
};
self.addEventListener('install', (event) => {
// (2) On considĂšre qu'un Service Worker est
// installé une fois que tous les assets
// ont été mis en cache
event.waitUntil(Promise.all(assetsList.map((url) => cacheAsset(url))));
});
// (3) Méthode permettant de retirer du
// cache tous les assets qui ne sont plus
// utilisés
const removeUnusedAssets = () => {
return caches.open(ASSETS_CACHE_NAME).then((cache) => {
// On rĂ©cupĂšre toutes les requĂȘtes
// stockées dans le cache
return cache.keys().then((requests) => {
// On ne veut retirer que les
// requĂȘtes qui ne sont plus
// dans `assetsList`
const unusedRequests = requests.filter((request) => {
const requestUrl = new URL(request.url);
return assetsList.indexOf(requestUrl.pathname) === -1;
});
// Et on retire ces requĂȘtes
// une par une
return Promise.all(
unusedRequests.map((request) => {
return cache.delete(request);
})
);
});
});
};
self.addEventListener('activate', (event) => {
// (3) Une fois qu'un Service Worker est
// utilisé il faut penser à nettoyer le
// cache pour qu'il ne grossisse pas
// indéfiniment
event.waitUntil(removeUnusedAssets());
});
Cette implĂ©mentation est plus complexe que les ressources que lâon a lâhabitude de trouver sur le sujet. En effet, souvent, câest plutĂŽt le nom de cache qui va changer de version en version. Ainsi, Ă lâinstallation, on rĂ©cupĂšre tous les assets sans distinction et on les met dans le cache assets-vXXX
. Et Ă lâactivation, on supprime tous les caches sauf assets-vXXX
. Câest dâailleurs lâexemple proposĂ© dans MDN.
Cependant lâavantage avec lâimplĂ©mentation que je vous ai montrĂ© est quâon Ă©conomise des requĂȘtes serveur. On ne va pas chercher deux fois la mĂȘme URL. LâinconvĂ©nient, par contre, câest quâon considĂšre que si une URL mise en cache a le mĂȘme nom, câest quâelle nâa pas changĂ©. Il faut donc bien faire attention Ă utiliser la bonne implĂ©mentation au bon moment.
Avec les mĂ©thodes de cache busting aujourdâhui, câest assez sĂ»r dâutiliser cette implĂ©mentation sur les fichiers JS/CSS. Mais ça peut par exemple ĂȘtre piĂ©geux de le faire sur un fichier HTML qui ressemble Ă un asset mais qui change Ă chaque fois que les fichiers JS/CSS importĂ©s changent.
Toute fois, si vous avez bien compris lâimplĂ©mentation de cette stratĂ©gie, je pense que vous serez capables dâimplĂ©menter nâimporte quelle stratĂ©gie de mise Ă jour du cache.
Conclusion
Si on rĂ©capitule, nous avons interceptĂ© une requĂȘte et nous sommes maintenant capables de renvoyer des informations du cache plutĂŽt que dâaller rĂ©cupĂ©rer les donnĂ©es depuis le serveur.
Ainsi, potentiellement, si les bonnes requĂȘtes ont Ă©tĂ© mises en cache, il est maintenant possible sur vos sites webs dâafficher du contenu malgrĂ© une connexion hasardeuse. Mieux que ça, grĂące aux Service Workers, vous ĂȘtes capables de dĂ©tecter les mises Ă jour de vos sites et prĂ©parer le cache pour les futures utilisations de votre site.
Si vous avez tout compris, et que vous vous sentez Ă lâaise pour utiliser chacun de ces outils, alors ma mission est finie !
Cela dit, mĂȘmes si nous sommes rentrĂ©s dans les dĂ©tails techniques, nous sommes restĂ©s trĂšs vagues en rĂ©pondant Ă beaucoup de questions par âça dĂ©pendâ. Nous avons donc les armes pour la suite mais je ne pense pas que tout le monde se sente prĂȘt Ă faire un site web accessible hors ligne dĂšs demain.
Pour cette raison, jâai prĂ©vu une derniĂšre partie Ă cette sĂ©rie dâarticle : une mise en pratique de tout ce quâon a vu pour transformer un site web standard en un site web qui propose une expĂ©rience hors ligne.
Cependant, cette derniÚre partie demandera certainement autant de préparation que les trois premiÚres réunies ! Je vous donne donc rendez-vous courant janvier pour lire la suite.
En attendant, je vous souhaite Ă tous dâexcellentes fĂȘtes ! Si vous avez des questions ou des commentaires, nâhĂ©sitez pas Ă mâen faire part sur Twitter ou Github. Je me ferai une joie de vous rĂ©pondre.
Sources complémentaires :
- The offline cookbook par Jake Archibald
- Workboxâs strategies
- Caching strategies par Mozilla
Si vous voulez suivre mes publications, il paraĂźt que j'ai un feed RSS, Mastodon et un Twitter.
Si vous pensez à d'autres méthodes que vous voudriez que je mette en place (pigeon voyageur, avion en papier, etc.), n'hésitez pas à me les proposer :)