Au-delà des applications monopages : architectures alternatives pour votre PWA

Parlons de... l'architecture ?

Je vais aborder un sujet important, mais potentiellement mal compris: l'architecture que vous utilisez pour votre application Web. Plus précisément, comment vos décisions en matière d'architecture entrent en jeu lorsque vous créez une progressive web app.

Une "architecture" peut sembler vague, et sa raison d'être n'est pas forcément claire au premier abord. Pour vous représenter l'architecture, posez-vous les questions suivantes: lorsqu'un utilisateur visite une page de mon site, quel code HTML est chargé ? Qu'est-ce qui est chargé lorsqu'ils consultent une autre page ?

Les réponses à ces questions ne sont pas toujours simples, et une fois que vous commencez à penser aux progressive web apps, elles peuvent devenir encore plus compliquées. Mon objectif est donc de vous présenter une architecture possible que j'ai trouvée efficace. Tout au long de cet article, j'étiqueterai les décisions que j'ai prises comme étant "mon approche" pour créer une progressive web app.

Vous êtes libre d'utiliser mon approche pour créer votre propre PWA, mais il existe toujours d'autres alternatives valides. J'espère que le fait de voir comment toutes les pièces s'assemblent vous inspirera et que vous vous sentirez en mesure de le personnaliser en fonction de vos besoins.

PWA Stack Overflow

Pour accompagner cet article, j'ai créé une PWA Stack Overflow. Je passe beaucoup de temps à lire et à contribuer à Stack Overflow, et je voulais créer une application Web qui faciliterait la consultation des questions fréquentes sur un sujet donné. Il repose sur l'API Stack Exchange publique. Il est Open Source. Pour en savoir plus, consultez le projet GitHub.

Applications multipages (MPA)

Avant d'entrer dans les détails, définissons quelques termes et expliquons les éléments de technologie sous-jacents. Tout d'abord, je vais couvrir ce que j'aime appeler les « applications multipages », ou « MPA ».

MPA est un nom sophistiqué de l'architecture traditionnelle utilisée depuis la naissance du Web. Chaque fois qu'un utilisateur accède à une nouvelle URL, le navigateur affiche progressivement le code HTML spécifique à cette page. Aucune tentative n'est effectuée pour conserver l'état de la page ni le contenu entre les navigations. Chaque fois que vous consultez une nouvelle page, vous repartez de zéro.

Ce fonctionnement diffère du modèle d'application monopage (SPA) pour la création d'applications Web, dans lequel le navigateur exécute du code JavaScript pour mettre à jour la page existante lorsque l'utilisateur visite une nouvelle section. Les applications monopages (SPA) et les MPA sont des modèles tout aussi valides. Toutefois, pour cet article, j'ai voulu explorer les concepts des PWA dans le contexte d'une application multipage.

Rapide et fiable

Comme d'innombrables autres personnes, j'ai déjà utilisé l'expression "progressive web app" (application Web progressive) ou PWA. Vous connaissez peut-être déjà certains des documents de référence, disponibles sur d'autres pages de ce site.

Une PWA est en quelque sorte une application Web qui offre une expérience utilisateur de première classe et qui gagne véritablement une place sur l'écran d'accueil de l'utilisateur. L'acronyme FIRE (pour Fast, Integrated, Reliable et Engaging) résume tous les attributs à prendre en compte lors de la création d'une PWA.

Dans cet article, nous allons nous concentrer sur un sous-ensemble de ces attributs: Rapide et Fiable.

Rapide:même si "rapide" signifie des choses différentes selon le contexte, je vais vous parler des avantages en termes de vitesse du chargement le moins possible depuis le réseau.

Fiable:Mais la vitesse brute ne suffit pas. Pour ressembler à une PWA, votre application Web doit être fiable. Elle doit être suffisamment résiliente pour toujours charger quelque chose, même s'il s'agit simplement d'une page d'erreur personnalisée, quel que soit l'état du réseau.

Rapidité et fiabilité:Pour finir, je vais reformuler légèrement la définition de la PWA et observer ce que signifie créer une application fiable et rapide. La rapidité et la fiabilité ne sont pas suffisantes uniquement sur un réseau à faible latence. Une vitesse fiable signifie que la vitesse de votre application Web est constante, quelles que soient les conditions du réseau sous-jacentes.

Technologies d'activation: service workers + API Cache Storage

Les PWA introduisent un niveau d'exigence élevé en termes de rapidité et de résilience. Heureusement, la plate-forme Web propose quelques éléments de base pour faire de ce type de performances une réalité. Je fais référence aux service workers et à l'API Cache Storage.

Vous pouvez créer un service worker qui écoute les requêtes entrantes, en transmettant une partie au réseau et en stockant une copie de la réponse pour une utilisation ultérieure, via l'API Cache Storage.

Service worker utilisant l'API Cache Storage pour enregistrer une copie d'une réponse réseau.

La prochaine fois que l'application Web effectuera la même requête, son service worker pourra vérifier ses caches et simplement renvoyer la réponse précédemment mise en cache.

Un service worker utilisant l'API Cache Storage pour répondre, contournant le réseau.

Éviter le réseau dans la mesure du possible est essentiel pour garantir des performances fiables et rapides.

JavaScript "isomorphique"

Un autre concept que j'aimerais aborder est ce que nous appelons parfois JavaScript"isomorphique" ou "universel". En bref, elle repose sur l'idée que le même code JavaScript peut être partagé entre différents environnements d'exécution. Lorsque j'ai créé ma PWA, je voulais partager du code JavaScript entre mon serveur backend et le service worker.

Il existe de nombreuses approches valides pour partager du code de cette manière, mais mon approche consistait à utiliser des modules ES comme code source définitif. J'ai ensuite transpilé et regroupé ces modules pour le serveur et le service worker en combinant Babel et Rollup. Dans mon projet, les fichiers portant l'extension .mjs sont du code qui se trouve dans un module ES.

Le serveur

En gardant ces concepts et cette terminologie à l'esprit, examinons comment j'ai créé ma PWA Stack Overflow. Je vais commencer par aborder notre serveur backend et expliquer comment il s'intègre dans l'architecture globale.

Je cherchais une combinaison d'un backend dynamique et d'un hébergement statique, et mon approche consistait à utiliser la plate-forme Firebase.

Firebase Cloud Functions lance automatiquement un environnement basé sur des nœuds en cas de requête entrante et intègre le framework HTTP Express très utilisé, que je connaissais déjà. Il propose également un hébergement prêt à l'emploi pour toutes les ressources statiques de mon site. Voyons comment le serveur gère les requêtes.

Lorsqu'un navigateur envoie une requête de navigation à notre serveur, il suit le flux suivant:

Présentation de la génération d'une réponse de navigation côté serveur

Le serveur achemine la requête en fonction de l'URL et utilise une logique de modélisation pour créer un document HTML complet. J'utilise une combinaison de données de l'API Stack Exchange, ainsi que des fragments HTML partiels que le serveur stocke localement. Une fois que notre service worker sait comment répondre, il peut diffuser du code HTML sur notre application Web.

Deux éléments de cette illustration méritent d'être explorés plus en détail: le routage et la création de modèles.

Itinéraires

Pour le routage, mon approche consistait à utiliser la syntaxe de routage native du framework Express. Il est suffisamment flexible pour correspondre aux préfixes d'URL simples, ainsi qu'aux URL qui incluent des paramètres dans le chemin. Ici, je crée un mappage entre les noms d'itinéraires au format Express sous-jacent à mettre en correspondance.

const routes = new Map([
  ['about', '/about'],
  ['questions', '/questions/:questionId'],
  ['index', '/'],
]);

export default routes;

Je peux ensuite référencer ce mappage directement à partir du code du serveur. En cas de correspondance pour un format Express donné, le gestionnaire approprié répond avec une logique de création de modèles spécifique à la route correspondante.

import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
  // Templating logic.
});

Modèles côté serveur

Et à quoi ressemble cette logique de création de modèles ? J'ai adopté une approche consistant à rassembler des fragments HTML partiels l'un après l'autre. Ce modèle se prête bien au traitement par flux.

Le serveur renvoie immédiatement un code récurrent HTML initial, et le navigateur peut afficher immédiatement cette page partielle. Lorsque le serveur rassemble les autres sources de données, il les diffuse dans le navigateur jusqu'à ce que le document soit terminé.

Pour comprendre ce que je veux dire, consultez le code Express de l'un de nos itinéraires:

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

En utilisant la méthode write() de l'objet response et en référençant les modèles partiels stockés localement, je peux démarrer le flux de réponse immédiatement, sans bloquer de source de données externe. Le navigateur affiche immédiatement une interface et un message de chargement pertinents à partir de ce code HTML initial.

La partie suivante de notre page utilise des données provenant de l'API Stack Exchange. Obtenir ces données signifie que notre serveur doit effectuer une requête réseau. L'application Web ne peut rien afficher d'autre tant qu'elle n'a pas reçu une réponse et la traite. Toutefois, au moins les utilisateurs ne regardent pas un écran vide pendant l'attente.

Une fois que l'application Web a reçu la réponse de l'API Stack Exchange, elle appelle une fonction de modélisation personnalisée pour traduire les données de l'API dans le code HTML correspondant.

Langue de création de modèles

La création de modèles peut être un sujet étonnamment controversé, et j'ai choisi cette approche parmi bien d'autres. Vous devez remplacer votre solution, en particulier si vous avez d'anciens liens avec un framework de création de modèles existant.

Dans mon cas d'utilisation, il était logique de n'utiliser que les littéraux de modèle de JavaScript, avec une logique décomposée en fonctions d'assistance. L'un des avantages de la création d'une MPA est que vous n'avez pas besoin de suivre les mises à jour de l'état ni d'afficher à nouveau votre code HTML. Une approche de base qui produit du code HTML statique a donc fonctionné pour moi.

Voici un exemple de modèle de la partie HTML dynamique de l'index de mon application Web. Comme pour mes routes, la logique de création de modèles est stockée dans un module ES qui peut être importée à la fois sur le serveur et sur le service worker.

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
  const form = `<form method="GET">...</form>`;
  const questionCards = items
    .map(item =>
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('');
  const questions = `<div id="questions">${questionCards}</div>`;
  return title + form + questions;
}

Ces fonctions de modèle sont en JavaScript pur. Il est donc utile de diviser la logique en fonctions plus petites et d'assistance, le cas échéant. Ici, je transmets chacun des éléments renvoyés dans la réponse de l'API dans l'une de ces fonctions, qui crée un élément HTML standard avec tous les attributs appropriés définis.

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(id)}">${title}</a>`;
}

Notez en particulier un attribut de données que j'ajoute à chaque lien, data-cache-url, défini sur l'URL de l'API Stack Exchange dont j'ai besoin pour afficher la question correspondante. Gardez cela à l'esprit. Je reviendrai plus tard.

Revenons à mon gestionnaire d'itinéraires. Une fois la création de modèles terminée, je diffuse la dernière partie du code HTML de ma page vers le navigateur et j'arrête le flux. Ce message indique au navigateur que le rendu progressif est terminé.

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

Voilà donc une brève visite de la configuration de mon serveur. Les utilisateurs qui accèdent à mon application Web pour la première fois reçoivent toujours une réponse du serveur, mais lorsqu'un visiteur revient dans mon application Web, mon service worker commence à répondre. Entrons dans le vif du sujet.

Le service worker

Présentation de la génération d&#39;une réponse de navigation dans le service worker.

Ce diagramme devrait vous sembler familier : bon nombre des éléments que j'ai déjà couverts sont ici dans une disposition légèrement différente. Passons en revue le flux de requêtes, en tenant compte du service worker.

Notre service worker gère une requête de navigation entrante pour une URL donnée, et tout comme mon serveur l'a fait, il utilise une combinaison de routage et de logique de modélisation pour déterminer comment répondre.

L'approche est la même qu'avant, mais avec différentes primitives de bas niveau, telles que fetch() et l'API Cache Storage. J'utilise ces sources de données pour construire la réponse HTML, que le service worker transmet à l'application Web.

Workbox

Plutôt que de partir de zéro avec des primitives de bas niveau, je vais compiler mon service worker sur un ensemble de bibliothèques de haut niveau appelé Workbox. Il constitue une base solide pour la logique de mise en cache, de routage et de génération de réponses de tout service worker.

Itinéraires

Tout comme pour mon code côté serveur, mon service worker doit savoir comment faire correspondre une requête entrante à la logique de réponse appropriée.

Mon approche consistait à traduire chaque route Express en une expression régulière correspondante, à l'aide d'une bibliothèque utile appelée regexparam. Une fois cette traduction effectuée, je peux bénéficier de la compatibilité intégrée de Workbox avec le routage d'expressions régulières.

Après avoir importé le module qui contient les expressions régulières, j'enregistre chaque expression régulière auprès du routeur de Workbox. Dans chaque itinéraire, je peux fournir une logique de création de modèles personnalisée pour générer une réponse. La création de modèles dans le service worker est un peu plus complexe que dans mon serveur backend, mais Workbox simplifie une grande partie du travail.

import regExpRoutes from './regexp-routes.mjs';

workbox.routing.registerRoute(
  regExpRoutes.get('index')
  // Templating logic.
);

Mise en cache des éléments statiques

L'un des aspects clés de la création de modèles consiste à s'assurer que mes modèles HTML partiels sont disponibles localement via l'API Cache Storage et qu'ils sont à jour lorsque je déploie des modifications dans l'application Web. La maintenance du cache peut être sujette aux erreurs une fois effectuée manuellement. C'est pourquoi je me tourne vers Workbox pour gérer la mise en cache dans le cadre de mon processus de compilation.

J'indique à Workbox les URL à mettre en cache à l'aide d'un fichier de configuration, en le pointant vers le répertoire qui contient tous mes éléments locaux, ainsi qu'un ensemble de formats à mettre en correspondance. Ce fichier est automatiquement lu par la CLI de Workbox, qui est run chaque fois que je recompile le site.

module.exports = {
  globDirectory: 'build',
  globPatterns: ['**/*.{html,js,svg}'],
  // Other options...
};

Workbox prend un instantané du contenu de chaque fichier et injecte automatiquement cette liste d'URL et de révisions dans mon fichier Service Worker final. Workbox dispose désormais de tout ce dont il a besoin pour rendre les fichiers en pré-cache toujours disponibles et les tenir à jour. Le résultat est un fichier service-worker.js qui contient un élément semblable à celui-ci:

workbox.precaching.precacheAndRoute([
  {
    url: 'partials/about.html',
    revision: '518747aad9d7e',
  },
  {
    url: 'partials/foot.html',
    revision: '69bf746a9ecc6',
  },
  // etc.
]);

Pour les personnes qui utilisent un processus de compilation plus complexe, Workbox propose à la fois un plug-in webpack et un module de nœud générique, en plus de son interface de ligne de commande.

Flux de données

Je veux ensuite que le service worker transmet immédiatement ce code HTML partiel mis en pré-cache à l'application Web. C'est un aspect essentiel pour être "rapide et fiable". Je vois toujours quelque chose d'important tout de suite à l'écran. Heureusement, l'utilisation de l'API Streams au sein de notre service worker rend cela possible.

Vous avez peut-être déjà entendu parler de l'API Streams. Mon collègue Jake Archibald chante ses louanges depuis des années. Il a fait la prédiction audacieuse que 2016 serait l'année des flux Web. L'API Streams est tout aussi performante aujourd'hui qu'il y a deux ans, à quelques différences près.

Alors que seul Chrome acceptait les flux à l'époque, l'API Streams est désormais plus largement prise en charge. La situation globale est positive, et avec un code de remplacement approprié, rien ne vous empêche d'utiliser des flux dans votre service worker aujourd'hui.

Eh bien, il y a peut-être une chose qui vous en empêche. Il expose un ensemble très puissant de primitives, et les développeurs qui ont l'habitude de l'utiliser peuvent créer des flux de données complexes, comme ceux-ci:

const stream = new ReadableStream({
  pull(controller) {
    return sources[0]
      .then(r => r.read())
      .then(result => {
        if (result.done) {
          sources.shift();
          if (sources.length === 0) return controller.close();
          return this.pull(controller);
        } else {
          controller.enqueue(result.value);
        }
      });
  },
});

Toutefois, il est possible que tout le monde ne puisse pas comprendre toutes les implications de ce code. Plutôt que d'examiner en détail cette logique, parlons de mon approche du traitement des flux de service workers.

J'utilise un tout nouveau wrapper de haut niveau, workbox-streams. Avec lui, je peux le transmettre dans une combinaison de sources de flux, à la fois à partir de caches et de données d'exécution qui peuvent provenir du réseau. Workbox se charge de coordonner les différentes sources et de les assembler en une seule réponse en flux continu.

En outre, Workbox détecte automatiquement si l'API Streams est compatible et, dans le cas contraire, crée une réponse équivalente non en flux continu. Cela signifie que vous n'avez pas à vous soucier de l'écriture de créations de remplacement, car les flux se rapprochent de la compatibilité totale du navigateur.

Mise en cache de l'environnement d'exécution

Voyons comment mon service worker traite les données d'exécution provenant de l'API Stack Exchange. J'utilise la compatibilité intégrée de Workbox pour une stratégie de mise en cache de type "obsolète" pendant la revalidation, ainsi que l'expiration pour s'assurer que l'espace de stockage de l'application Web n'est pas illimité.

J'ai configuré deux stratégies dans Workbox pour gérer les différentes sources qui constitueront la réponse de flux. Avec quelques appels de fonction et quelques configurations, Workbox nous permet de faire ce qui aurait dû prendre des centaines de lignes de code manuscrits autrement.

const cacheStrategy = workbox.strategies.cacheFirst({
  cacheName: workbox.core.cacheNames.precache,
});

const apiStrategy = workbox.strategies.staleWhileRevalidate({
  cacheName: API_CACHE_NAME,
  plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});

La première stratégie lit les données qui ont été mises en pré-cache, comme nos modèles HTML partiels.

L'autre stratégie implémente la logique de mise en cache "obsolète" pendant la revalidation, ainsi que l'expiration du cache la moins récemment utilisée une fois que nous avons atteint 50 entrées.

Maintenant que j'ai ces stratégies en place, il ne me reste plus qu'à indiquer à Workbox comment les utiliser pour créer une réponse complète en flux continu. Je transmets un tableau de sources en tant que fonctions, et chacune de ces fonctions s'exécutera immédiatement. Workbox prend le résultat de chaque source et le diffuse dans l'application Web, dans l'ordre, uniquement si la fonction suivante dans le tableau n'est pas encore terminée.

workbox.streams.strategy([
  () => cacheStrategy.makeRequest({request: '/head.html'}),
  () => cacheStrategy.makeRequest({request: '/navbar.html'}),
  async ({event, url}) => {
    const tag = url.searchParams.get('tag') || DEFAULT_TAG;
    const listResponse = await apiStrategy.makeRequest(...);
    const data = await listResponse.json();
    return templates.index(tag, data.items);
  },
  () => cacheStrategy.makeRequest({request: '/foot.html'}),
]);

Les deux premières sources sont des modèles partiels mis en pré-cache qui sont lus directement à partir de l'API Cache Storage. Ils seront donc toujours disponibles immédiatement. Cela garantit que l'implémentation de notre service worker réponde rapidement aux requêtes de manière fiable, tout comme mon code côté serveur.

La fonction source suivante récupère les données de l'API Stack Exchange et traite la réponse dans le code HTML attendu par l'application Web.

La stratégie "stale-while-revalidate" signifie que si j'ai déjà mis en cache une réponse pour cet appel d'API, je pourrai la diffuser immédiatement sur la page, tout en mettant à jour l'entrée de cache "en arrière-plan" pour la prochaine fois qu'elle sera demandée.

Enfin, je diffuse une copie mise en cache de mon pied de page et je ferme les balises HTML finales pour terminer la réponse.

Le partage de code assure la synchronisation

Vous remarquerez que certains bits du code du service worker vous semblent familiers. Le code HTML partiel et la logique de création de modèles utilisés par mon service worker sont identiques à ceux utilisés par mon gestionnaire côté serveur. Ce partage de code garantit une expérience cohérente aux utilisateurs, qu'ils accèdent à mon application Web pour la première fois ou qu'ils reviennent sur une page affichée par le service worker. C'est toute la beauté de JavaScript isomorphe.

Améliorations progressives dynamiques

J'ai examiné à la fois le serveur et le service worker de ma PWA, mais il me reste une dernière logique à aborder: une petite quantité de code JavaScript s'exécute sur chacune de mes pages, après leur diffusion complète.

Ce code améliore progressivement l'expérience utilisateur, mais n'est pas crucial : l'application Web continuera de fonctionner si elle n'est pas exécutée.

Métadonnées de page

Mon application utilise JavaScript côté client pour mettre à jour les métadonnées d'une page en fonction de la réponse de l'API. Comme j'utilise le même bit initial de code HTML mis en cache pour chaque page, l'application Web se retrouve avec des balises génériques dans l'en-tête de mon document. Toutefois, grâce à la coordination entre la conception de modèles et le code côté client, je peux mettre à jour le titre de la fenêtre à l'aide de métadonnées spécifiques à la page.

Dans le cadre du code de création de modèles, mon approche consiste à inclure un tag de script contenant la chaîne correctement échappée.

const metadataScript = `<script>
  self._title = '${escape(item.title)}';
</script>`;

Ensuite, une fois la page chargée, je lis cette chaîne et je modifie le titre du document.

if (self._title) {
  document.title = unescape(self._title);
}

Si vous souhaitez mettre à jour d'autres métadonnées spécifiques à une page dans votre propre application Web, vous pouvez suivre la même approche.

Expérience utilisateur hors connexion

L'autre amélioration progressive que j'ai ajoutée est utilisée pour attirer l'attention sur nos fonctionnalités hors connexion. J'ai créé une PWA fiable et je veux que les utilisateurs sachent que lorsqu'ils sont hors connexion, ils peuvent toujours charger les pages déjà consultées.

Tout d'abord, j'utilise l'API Cache Storage pour obtenir la liste de toutes les requêtes API précédemment mises en cache, que je traduis en une liste d'URL.

Vous souvenez-vous de ces attributs de données spéciaux dont j'ai parlé, chacun contenant l'URL de la requête API nécessaire pour afficher une question ? Je peux croiser ces attributs de données avec la liste des URL mises en cache et créer un tableau de tous les liens des questions qui ne correspondent pas.

Lorsque le navigateur passe en mode hors connexion, je fais défiler la liste des liens non mis en cache et je filtre ceux qui ne fonctionnent pas. Gardez à l'esprit qu'il s'agit simplement d'un indice visuel à l'utilisateur sur ce qu'il peut attendre de ces pages. Je ne désactive pas réellement les liens, ni n'empêche l'utilisateur de naviguer.

const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);

const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

const offlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '0.3';
  }
};

const onlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '1.0';
  }
};

window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);

Écueils les plus courants

Voyons maintenant comment créer une PWA multipage. Il y a de nombreux facteurs que vous devrez prendre en compte lors de l’élaboration de votre propre approche, et vous pourriez finir par faire des choix différents de ceux que j’ai faits. Cette flexibilité est l'un des grands avantages de la conception pour le Web.

Il existe quelques pièges courants que vous pouvez rencontrer lorsque vous prenez vos propres décisions en termes d'architecture, et je veux vous éviter des désagréments.

Ne pas mettre en cache l'intégralité du code HTML

Nous vous déconseillons de stocker des documents HTML complets dans votre cache. D'une part, c'est un gaspillage d'espace. Si votre application Web utilise la même structure HTML de base pour chacune de ses pages, vous finirez par stocker des copies du même balisage encore et encore.

Plus important encore, si vous déployez une modification de la structure HTML partagée de votre site, chacune des pages précédemment mises en cache reste bloquée avec votre ancienne mise en page. Imaginez la frustration que peut avoir un visiteur connu lorsqu'il affiche à la fois des anciennes et des nouvelles pages.

Dérive des serveurs / services de service

L'autre écueil à éviter est la désynchronisation de votre serveur et de votre service worker. Mon approche consistait à utiliser du code JavaScript isomorphe, afin que le même code soit exécuté aux deux emplacements. Ce n'est pas toujours possible en fonction de l'architecture de vos serveurs existants.

Quelles que soient les décisions que vous prenez en matière d'architecture, vous devez avoir une stratégie pour exécuter le code de routage et de modélisation équivalent sur votre serveur et votre service worker.

Dans le pire des cas

Mise en page / Conception incohérente

Que se passe-t-il si vous ignorez ces pièges ? Toutes sortes d'échecs sont possibles, mais le pire des cas est qu'un utilisateur connu consulte une page mise en cache avec une mise en page très obsolète (avec un texte d'en-tête obsolète, par exemple) ou qui utilise des noms de classe CSS qui ne sont plus valides.

Dans le pire des cas: routage interrompu

Un utilisateur peut également rencontrer une URL gérée par votre serveur, mais pas par votre service worker. Un site rempli de mises en page de zombies et d'impasses n'est pas une PWA fiable.

Conseils pour réussir

Mais vous n'êtes pas dans cette situation ! Les conseils suivants peuvent vous aider à éviter ces pièges:

Utiliser des bibliothèques de modèles et de routage avec des implémentations multilingues

Essayez d'utiliser des bibliothèques de modèles et de routage contenant des implémentations JavaScript. Tous les développeurs n'ont pas la possibilité de migrer leur serveur Web et leur langage de création de modèles actuels.

Cependant, un certain nombre de frameworks de modélisation et de routage courants ont des implémentations dans plusieurs langages. Si vous en trouvez un qui fonctionne avec JavaScript ainsi qu'avec le langage de votre serveur actuel, vous avez fait un pas de plus vers la synchronisation de votre service worker et de votre serveur.

Privilégier les modèles séquentiels plutôt que imbriqués

Je vous recommande ensuite d'utiliser une série de modèles séquentiels qui peuvent être diffusés les uns après les autres. Vous pouvez tout à fait utiliser une logique de création de modèles plus complexe pour les parties ultérieures de votre page, à condition que vous puissiez insérer la partie initiale de votre code HTML en flux continu le plus rapidement possible.

Mettre en cache les contenus statiques et dynamiques dans votre service worker

Pour optimiser les performances, vous devez mettre en cache toutes les ressources statiques critiques de votre site. Vous devez également configurer la logique de mise en cache de l'environnement d'exécution pour gérer le contenu dynamique, comme les requêtes API. Workbox vous permet de vous appuyer sur des stratégies bien testées et prêtes pour la production, au lieu de les mettre en œuvre de toutes pièces.

Ne bloquez le réseau sur le réseau qu'en cas d'absolue nécessité

Par ailleurs, vous ne devez bloquer sur le réseau que lorsqu'il n'est pas possible de diffuser une réponse à partir du cache. Afficher immédiatement une réponse d'API mise en cache peut souvent offrir une meilleure expérience utilisateur que d'attendre de nouvelles données.

Ressources