Parlons d'architecture ?
Je vais aborder un sujet important, mais potentiellement mal compris : l'architecture que vous utilisez pour votre application Web, et plus particulièrement la façon dont vos décisions architecturales entrent en jeu lorsque vous créez une progressive web app.
Le terme "architecture" peut sembler vague, et l'importance de ce concept n'est pas forcément évidente. Pour réfléchir à l'architecture, vous pouvez vous poser les questions suivantes : lorsqu'un utilisateur consulte une page de mon site, quel code HTML est chargé ? Et qu'est-ce qui est chargé lorsqu'ils accèdent à une autre page ?
Les réponses à ces questions ne sont pas toujours simples, et une fois que vous commencez à réfléchir 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, je qualifierai les décisions que j'ai prises de "mon approche" pour créer une progressive web app.
Vous êtes libre d'utiliser mon approche lorsque vous créez votre propre PWA, mais il existe toujours d'autres alternatives valides. J'espère que voir comment toutes les pièces s'assemblent vous inspirera et que vous vous sentirez capable de personnaliser ce processus 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 permettrait de parcourir facilement les questions fréquentes sur un sujet donné. Il est basé sur l'API Stack Exchange publique. Il s'agit d'un projet 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 certains éléments de la technologie sous-jacente. Tout d'abord, je vais parler de ce que j'appelle les "applications multipages" ou "MPA".
MPA est un nom sophistiqué pour l'architecture traditionnelle utilisée depuis les débuts 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 préserver l'état de la page ou le contenu entre les navigations. Chaque fois que vous visitez une nouvelle page, vous repartez de zéro.
Cela contraste avec le 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 accède à une nouvelle section. Les SPA et les MPA sont des modèles tout aussi valables, mais pour cet article, je voulais explorer les concepts de PWA dans le contexte d'une application multipage.
Une rapidité fiable
Vous m'avez entendu (ainsi que d'innombrables autres personnes) utiliser l'expression "progressive web app" ou PWA. Vous connaissez peut-être déjà certains des documents de référence, ailleurs sur ce site.
Vous pouvez considérer une PWA comme une application Web qui offre une expérience utilisateur de premier ordre et qui mérite vraiment d'être placée sur l'écran d'accueil de l'utilisateur. L'acronyme FIRE, qui signifie Fast (rapide), Integrated (intégré), Reliable (fiable) et Engaging (engageant), résume tous les attributs à prendre en compte lors de la création d'une PWA.
Dans cet article, je vais me concentrer sur un sous-ensemble de ces attributs : Rapide et Fiable.
Rapide : Bien que le terme "rapide" ait différentes significations selon le contexte, je vais aborder les avantages en termes de vitesse de chargement d'un minimum de données depuis le réseau.
Fiabilité : 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 ne s'agit que d'une page d'erreur personnalisée, quel que soit l'état du réseau.
Rapidité et fiabilité : Enfin, je vais reformuler légèrement la définition de PWA et examiner ce que signifie créer un site rapide et fiable. Il ne suffit pas d'être rapide et fiable uniquement lorsque vous êtes sur un réseau à faible latence. La rapidité fiable signifie que la vitesse de votre application Web est constante, quelles que soient les conditions du réseau sous-jacent.
Technologies d'activation : service workers et API Cache Storage
Les PWA placent la barre très haut en termes de vitesse et de résilience. Heureusement, la plate-forme Web propose des composants permettant d'atteindre ce niveau de performances. 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 transmet certaines au réseau et stocke une copie de la réponse pour une utilisation ultérieure, via l'API Cache Storage.
La prochaine fois que l'application Web effectuera la même requête, son service worker pourra vérifier ses caches et renvoyer simplement la réponse précédemment mise en cache.
Éviter le réseau autant que possible est un élément crucial pour offrir des performances rapides et fiables.
JavaScript "isomorphe"
Un autre concept que je souhaite aborder est ce que l'on appelle parfois le JavaScript "isomorphe" ou "universel". En d'autres termes, il s'agit de l'idée selon laquelle 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 à l'aide d'une combinaison de Babel et de Rollup. Dans mon projet, les fichiers avec une extension de fichier .mjs sont du code qui se trouve dans un module ES.
Le serveur
Maintenant que vous avez ces concepts et cette terminologie en tête, voyons comment j'ai créé ma PWA Stack Overflow. Je vais commencer par notre serveur backend et expliquer comment il s'intègre dans l'architecture globale.
Je cherchais une combinaison de backend dynamique et d'hébergement statique, et mon approche consistait à utiliser la plate-forme Firebase.
Firebase Cloud Functions lance automatiquement un environnement basé sur Node lorsqu'une requête est reçue et s'intègre au framework HTTP Express, que je connaissais déjà. Il offre é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 :
Le serveur achemine la requête en fonction de l'URL et utilise une logique de création de modèles 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 commencer à diffuser du code HTML dans notre application Web.
Deux aspects de cette image méritent d'être examinés plus en détail : le routage et la création de modèles.
Routage
Pour le routage, j'ai choisi d'utiliser la syntaxe de routage native du framework Express. Il est suffisamment flexible pour correspondre à des préfixes d'URL simples, ainsi qu'à des URL incluant des paramètres dans le chemin d'accès. Ici, je crée un mappage entre les noms de routes et le modèle 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. Lorsqu'un modèle Express donné correspond, 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 ? Eh bien, j'ai opté pour une approche qui consiste à assembler des fragments HTML partiels de manière séquentielle, l'un après l'autre. Ce modèle se prête bien au streaming.
Le serveur renvoie immédiatement un code HTML récurrent initial, et le navigateur peut afficher cette page partielle tout de suite. À mesure que le serveur rassemble le reste des sources de données, il les diffuse en streaming vers le navigateur jusqu'à ce que le document soit complet.
Pour comprendre ce que je veux dire, examinez le code Express de l'une de nos routes :
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 des modèles partiels stockés localement, je peux démarrer le flux de réponse immédiatement, sans bloquer aucune source de données externe. Le navigateur prend ce code HTML initial et affiche immédiatement une interface utile et un message de chargement.
La partie suivante de notre page utilise les données de l'API Stack Exchange. Pour obtenir ces données, 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 de réponse et qu'elle ne l'a pas traitée, mais au moins, les utilisateurs ne regardent pas un écran vide en attendant.
Une fois que l'application Web a reçu la réponse de l'API Stack Exchange, elle appelle une fonction de création de modèles personnalisée pour traduire les données de l'API en code HTML correspondant.
Langage de création de modèles
La création de modèles peut être un sujet étonnamment controversé, et l'approche que j'ai choisie n'est qu'une parmi tant d'autres. Vous pouvez utiliser votre propre solution, en particulier si vous avez des liens d'ancienne génération avec un framework de création de modèles existant.
Pour mon cas d'utilisation, il était plus logique de m'appuyer sur les littéraux de modèle de JavaScript, avec une logique répartie dans des 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 d'état et d'effectuer un nouveau rendu de votre code HTML. Une approche de base produisant du code HTML statique a donc fonctionné pour moi.
Voici un exemple de création de modèle pour 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é à la fois dans le serveur et dans 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 du JavaScript pur. Il est utile de diviser la logique en fonctions d'assistance plus petites, le cas échéant. Ici, je transmets chacun des éléments renvoyés dans la réponse de l'API à une telle fonction, 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 l'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 dessus plus tard.
Revenons à mon gestionnaire de routage. Une fois la création du modèle terminée, je diffuse la dernière partie du code HTML de ma page dans le navigateur, puis je mets fin à la diffusion. Cela indique au navigateur que l'affichage 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à pour la présentation rapide de la configuration de mon serveur. Les utilisateurs qui visitent mon application Web pour la première fois recevront toujours une réponse du serveur, mais lorsqu'un visiteur reviendra sur mon application Web, mon service worker commencera à répondre. Voyons cela de plus près.
Le service worker
Ce diagramme devrait vous sembler familier : la plupart des éléments que j'ai déjà abordés sont ici présentés dans un ordre légèrement différent. Examinons 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, il utilise une combinaison de logique de routage et de création de modèles pour déterminer comment répondre.
L'approche est la même qu'auparavant, mais avec des primitives de bas niveau différentes, comme fetch() et l'API Cache Storage. J'utilise ces sources de données pour construire la réponse HTML, que le service worker renvoie à l'application Web.
Workbox
Plutôt que de partir de zéro avec des primitives de bas niveau, je vais créer mon service worker sur un ensemble de bibliothèques de haut niveau appelé Workbox. Il fournit une base solide pour la logique de mise en cache, de routage et de génération de réponses de n'importe quel service worker.
Routage
Tout comme 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, en utilisant une bibliothèque utile appelée regexparam. Une fois cette traduction effectuée, je peux profiter de la compatibilité intégrée de Workbox avec le routage des expressions régulières.
Après avoir importé le module contenant les expressions régulières, j'enregistre chacune d'elles auprès du routeur de Workbox. Dans chaque route, je peux fournir une logique de modèle 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 sur mon serveur backend, mais Workbox facilite grandement les choses.
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templating logic.
);
Mise en cache des éléments statiques
Un aspect essentiel 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 mis à jour lorsque je déploie des modifications dans l'application Web. La maintenance du cache peut être sujette aux erreurs lorsqu'elle est effectuée manuellement. Je me tourne donc vers Workbox pour gérer la mise en cache préalable dans le cadre de mon processus de compilation.
J'indique à Workbox les URL à pré-mettre en cache à l'aide d'un fichier de configuration, en pointant vers le répertoire qui contient tous mes éléments locaux ainsi qu'un ensemble de modèles à mettre en correspondance. Ce fichier est automatiquement lu par la CLI de Workbox, qui est exécutée chaque fois que je recrée 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 de service worker final. Workbox dispose désormais de tout ce dont il a besoin pour que les fichiers pré-mis en cache soient toujours disponibles et à 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 un plug-in webpack et un module de nœud générique, en plus de son interface de ligne de commande.
Streaming
Ensuite, je souhaite que le service worker renvoie immédiatement ce code HTML partiel pré-mis en cache à l'application Web. C'est un aspect crucial de la "vitesse fiable" : j'obtiens toujours quelque chose de pertinent à l'écran immédiatement. Heureusement, l'utilisation de l'API Streams dans notre service worker rend cela possible.
Vous avez peut-être déjà entendu parler de l'API Streams. Mon collègue Jake Archibald en vante les mérites 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 géniale aujourd'hui qu'il y a deux ans, mais avec une différence cruciale.
À l'époque, seul Chrome était compatible avec les flux, mais l'API Streams est plus largement compatible aujourd'hui. Le bilan global est positif et, avec un code de remplacement approprié, rien ne vous empêche d'utiliser des flux dans votre service worker dès aujourd'hui.
Il y a peut-être une chose qui vous empêche de vous lancer : comprendre le fonctionnement de l'API Streams. Il expose un ensemble très puissant de primitives, et les développeurs qui l'utilisent peuvent créer des flux de données complexes, comme celui-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);
}
});
},
});
Cependant, comprendre toutes les implications de ce code n'est pas forcément à la portée de tout le monde. Plutôt que d'analyser cette logique, parlons de mon approche du streaming des service workers.
J'utilise un tout nouveau wrapper de haut niveau, workbox-streams.
Je peux ainsi transmettre un mélange de sources de streaming, provenant à la fois de caches et de données d'exécution pouvant provenir du réseau. Workbox se charge de coordonner les différentes sources et de les assembler en une seule réponse en streaming.
De plus, Workbox détecte automatiquement si l'API Streams est compatible et, si ce n'est pas le cas, crée une réponse équivalente sans streaming. Cela signifie que vous n'avez pas à vous soucier de l'écriture de solutions de remplacement, car les flux se rapprochent d'une compatibilité à 100 % avec les navigateurs.
Mise en cache d'exécution
Voyons comment mon service worker gère les données d'exécution de l'API Stack Exchange. J'utilise la compatibilité intégrée de Workbox avec une stratégie de mise en cache stale-while-revalidate, ainsi que l'expiration pour m'assurer que l'espace de stockage de l'application Web n'augmente pas de manière illimitée.
Je configure deux stratégies dans Workbox pour gérer les différentes sources qui composeront la réponse en streaming. En quelques appels de fonction et configurations, Workbox nous permet de faire ce qui nécessiterait autrement des centaines de lignes de code écrit à la main.
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é pré-mises en cache, comme nos modèles HTML partiels.
L'autre stratégie implémente la logique de mise en cache stale-while-revalidate, ainsi que l'expiration du cache en fonction de l'utilisation la moins récente une fois que nous avons atteint 50 entrées.
Maintenant que j'ai mis en place ces stratégies, il ne me reste plus qu'à indiquer à Workbox comment les utiliser pour construire une réponse complète en streaming. Je transmets un tableau de sources en tant que fonctions, et chacune de ces fonctions sera exécutée immédiatement. Workbox récupère le résultat de chaque source et le diffuse en streaming vers l'application Web, de manière séquentielle, en ne retardant que si la fonction suivante du 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 pré-mis en cache, lus directement à partir de l'API Cache Storage. Ils sont donc toujours disponibles immédiatement. Cela garantit que notre implémentation de service worker répondra rapidement aux requêtes, 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 au format HTML attendu par l'application Web.
La stratégie stale-while-revalidate signifie que si j'ai une réponse précédemment mise en cache 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 permet de synchroniser les éléments
Vous remarquerez que certains éléments du code du service worker vous sont 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 aux utilisateurs une expérience cohérente, qu'ils visitent mon application Web pour la première fois ou qu'ils reviennent sur une page affichée par le service worker. C'est là toute la beauté du JavaScript isomorphe.
Améliorations dynamiques et progressives
J'ai examiné le serveur et le service worker de ma PWA, mais il reste un dernier élément de logique à aborder : une petite quantité de code JavaScript s'exécute sur chacune de mes pages, une fois qu'elles ont été entièrement diffusées.
Ce code améliore progressivement l'expérience utilisateur, mais n'est pas crucial. L'application Web fonctionnera toujours s'il n'est pas exécuté.
Métadonnées de page
Mon application utilise du code 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 code HTML initial 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, en coordonnant mon code de création de modèles et mon 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 code de modèle, mon approche consiste à inclure une balise de script contenant la chaîne correctement échappée.
const metadataScript = `<script>
self._title = '${escape(item.title)}';
</script>`;
Ensuite, une fois ma page chargée, je lis cette chaîne et je mets à jour le titre du document.
if (self._title) {
document.title = unescape(self._title);
}
Si vous souhaitez mettre à jour d'autres métadonnées spécifiques à la 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 permet d'attirer l'attention sur nos fonctionnalités hors connexion. J'ai créé une PWA fiable et je souhaite que les utilisateurs sachent que, lorsqu'ils sont hors connexion, ils peuvent toujours charger les pages qu'ils ont 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, puis je la traduis en une liste d'URL.
Vous vous souvenez des 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 de questions qui ne correspondent pas.
Lorsque le navigateur passe en mode hors connexion, je parcours la liste des liens non mis en cache et j'estompe ceux qui ne fonctionneront pas. N'oubliez pas qu'il s'agit simplement d'une indication visuelle pour l'utilisateur sur ce à quoi il doit s'attendre sur ces pages. Je ne désactive pas réellement les liens et je n'empêche pas 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
Nous avons maintenant fait le tour de mon approche pour créer une PWA multipage. Vous devrez prendre en compte de nombreux facteurs lorsque vous élaborerez votre propre approche, et vous ferez peut-être des choix différents des miens. Cette flexibilité est l'un des grands avantages de la création pour le Web.
Vous pouvez rencontrer quelques pièges courants lorsque vous prenez vos propres décisions architecturales, et je souhaite vous éviter des difficultés.
Ne mettez pas en cache l'intégralité du code HTML
Je vous déconseille de stocker des documents HTML complets dans votre cache. D'une part, c'est une perte 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 de ces pages précédemment mises en cache reste bloquée avec votre ancienne mise en page. Imaginez la frustration d'un visiteur qui revient sur votre site et voit un mélange d'anciennes et de nouvelles pages.
Dérive entre le serveur et le service worker
L'autre piège à éviter est la désynchronisation de votre serveur et de votre service worker. Mon approche consistait à utiliser du JavaScript isomorphe, de sorte que le même code soit exécuté aux deux endroits. Selon l'architecture de votre serveur existant, ce n'est pas toujours possible.
Quelles que soient vos décisions architecturales, vous devez disposer d'une stratégie pour exécuter le code de routage et de création de modèles équivalent sur votre serveur et votre service worker.
Pires scénarios
Mise en page/conception incohérente
Que se passe-t-il lorsque vous ignorez ces pièges ? Toutes sortes d'échecs sont possibles, mais le pire scénario est qu'un utilisateur connu visite une page mise en cache avec une mise en page très obsolète, par exemple une page avec un texte d'en-tête obsolète ou qui utilise des noms de classe CSS qui ne sont plus valides.
Pire scénario : routage défectueux
Un utilisateur peut également tomber sur une URL gérée par votre serveur, mais pas par votre service worker. Un site plein de mises en page zombies et d'impasses n'est pas une PWA fiable.
Conseils pour réussir
Mais vous n'êtes pas seul. Les conseils suivants peuvent vous aider à éviter ces écueils :
Utiliser des bibliothèques de modèles et de routage qui ont des implémentations multilingues
Essayez d'utiliser des bibliothèques de modèles et de routage qui ont des implémentations JavaScript. Je sais que tous les développeurs n'ont pas le luxe de pouvoir migrer depuis leur serveur Web et leur langage de création de modèles actuels.
Toutefois, un certain nombre de frameworks de création de modèles et de routage populaires sont implémentés dans plusieurs langages. Si vous en trouvez un qui fonctionne avec JavaScript ainsi qu'avec le langage de votre serveur actuel, vous êtes sur le point de synchroniser votre service worker et votre serveur.
Privilégiez les modèles séquentiels plutôt qu'imbriqués
Ensuite, je vous recommande d'utiliser une série de modèles séquentiels qui peuvent être diffusés l'un après l'autre. Ce n'est pas un problème si les parties ultérieures de votre page utilisent une logique de création de modèles plus complexe, tant que vous pouvez diffuser la partie initiale de votre code HTML le plus rapidement possible.
Mettre en cache le contenu statique et dynamique dans votre service worker
Pour des performances optimales, vous devez pré-mettre en cache toutes les ressources statiques critiques de votre site. Vous devez également configurer une logique de mise en cache d'exécution pour gérer le contenu dynamique, comme les requêtes API. L'utilisation de Workbox vous permet de vous appuyer sur des stratégies éprouvées et prêtes pour la production, au lieu de tout implémenter de zéro.
Ne bloquez le réseau que lorsque cela est absolument nécessaire
De même, vous ne devez bloquer le réseau que lorsqu'il n'est pas possible de diffuser une réponse à partir du cache. L'affichage immédiat d'une réponse de l'API mise en cache peut souvent améliorer l'expérience utilisateur par rapport à l'attente de nouvelles données.