Après avoir présenter ce qu’était un service worker et comment en ajouter un dans votre application, nous allons aujourd’hui nous attarder sur la nouvelle toolbox Workbox présentée à Google IO/2017.
On peut se poser la question de pourquoi Google met en place un nouveau projet alors que des solutions comme sw-precache et sw-toolbox existent (voir mon dernier article sur le sujet). En fait il y a eu pas mal de modifications dans le code depuis la mise en place de ces solutions et sw-toolbox n’adresse pour le moment qu’une partie de ce que vous pouvez faire avec des services workers (exclusivement du cache de ressources). Comme beaucoup de personnes utilisent déjà ces projets il était difficile de faire de gros changements sans mettre en péril la compatibilité ascendante.
Workbox a été pensé de manière modulaire pour éviter ces problèmes à l’avenir. Vous pouvez choisir de n’utiliser que les éléments dont vous avez besoin. Quand vous voulez créer des sites performants, il est important de n’embarquer que les ressources vraiment nécéssaires pour limiter un maximum la taille de votre site.
Le but de Workbox est de vous fournir un maximum d’outils pour transformer votre application en progressive webapp. Workbox se base sur différentes API JavaScript
Bien évidemment toutes ces API ne sont pas encore disponibles sous tous les navaigateurs. Workbox vous aide à générer les fichiers de configuration et met à disposition différents scénarios éprouvés.
Utiliser les services workers est assez sensible au niveau sécurité et au niveau de la gestion du cache des ressources. Il est à mon sens important d’utiliser une librairie externe qui évolue sans cesse et où les bug fix sont résolus rapidement.
Comme je le disais plus haut Workbox a vraiment été pensé de manière modulaire. C’est un peu comme un magasin dans lequel vous allez pouvoir faire votre marché, parmi plusieurs librairies ou outils faiblement couplés les uns avec les autres.
Avant de voir en détail les modules bas niveau nous allons regarder comment utiliser ceux de plus haut niveau. Workbox a été créé pour vous faciliter la configuration et peut facilement s’intégrer dans le build de votre application. Il existe différents clients
Client pour webpack : workbox-webpack-plugin
Client pour npm : workbox-cli
Client pour Gulp : workbox-build
Il est intéressant de noter qu’il n’y a pas de client direct pour le moment pour Grunt.
Vous pouvez directement créer votre fichier service worker en vous basant sur workbox-sw mais il est plutôt fortement recommandé de générer votre service worker avec les clients évoqués ci dessus. Voici un exemple de script Gulp pour générer la configuration
gulp.task('bundle-sw', () => {
return wbBuild.generateSW({
cacheId: 'dev-mind',
globDirectory: './build/dist',
swDest: 'build/.tmp/sw.js',
staticFileGlobs: ['**/*.{js,html,css,png,jpg,json,gif,svg,webp,eot,ttf,woff,woff2,gz}']
clientsClaim: true
})
.then(() => {
console.log('Service worker generated.');
})
.catch((err) => {
console.log('[ERROR] This happened: ' + err);
});
});
Si vous ouvrez ce fichier vous allez voir quelque chose de similaire à
importScripts('workbox-sw.prod.v1.0.1.js');
const fileManifest = [
{
"url": "/404.html",
"revision": "529851a7efdb7576b4568154f84f87dd"
},
// ...
];
const workboxSW = new self.WorkboxSW({
"cacheId": "dev-mind",
"clientsClaim": true
});
workboxSW.precache(fileManifest);
Vous pouvez consulter les sources de mon site web pour voir un exemple complet d’utilisation. Nous allons maintenant nous attarder sur les modules bas niveau si vous voulez passer outre la génération automatique
Si vous utilisiez sw-precache et sw-toolbox nous allons tout d’abord regarder les modules qui reproduisent le comportement de ces librairies.
Ce module node s’intègre facilement à votre processus de build Gulp ou Webpack ou autre… Il permet de générer votre fichier service worker ou un fichier manifest.
Le but est de générer la liste des ressources qui peuvent être "précachées" par un service worker. Un hash est associé à chacune des ressources afin de pouvoir mettre à jour intelligemment le cache et supprimer les ressources qui ne seraient plus à jour. Cette librairie permet soit de
générer un service worker avec la liste des ressources à mettre dans le cache
générer un fichier manifest pour ensuite l’injecter dans votre application pour pouvoir accéder aux URL et au détail des modificatons des ressources
injecter un fichier manifest dans un service worker existant. Vous controlez l’écriture de votre service worker tout en bénéficiant du précaching automatique
Le service worker est à l’écoute des requêtes sortantes (fetch event). Nous avons besoin de définir des comportements différents selon les requêtes. Ce module permet d’appliquer différentes stratégies sur des sous ensembles de requêtes. Nous définissons des routes.
Une route met en relation
un matcher : élément permettant de définir un sous ensemble de requêtes.
un handler : définissant la stratégie à appliquer à la réponse
Il existe différents types de routes qui vont vous permettre d’utiliser des matchers différents
La communauté JS aime beaucoup ExpressJS et notamment la manière de définir des URL. ExpressRoute a été créé dans ce sens. Une autre manière de définir des routes est d’utiliser des expressions régulières. Vous pouvez utiliser dans ce cas une route de type RegExpRoute.
const assetRoute = new RegExpRoute({
regExp: /assets/,
handler: new workbox.runtimeCaching.StaleWhileRevalidate(),
});
const imageRoute = new RegExpRoute({
regExp: /images/,
handler: new workbox.runtimeCaching.CacheFirst(),
});
const expressRoute = new workbox.routing.ExpressRoute({
path: 'https://example.com/path/to/:file'
});
const router = new workbox.routing.Router();
router.registerRoutes({routes: [assetRoute, imageRoute, expressRoute]});
router.setDefaultHandler({
handler: new workbox.runtimeCaching.NetworkFirst(),
});
Dans l’exemple ci dessus vous pourriez implémenter vos propres handlers mais il est préférable d’utiliser les handlers Workbox. Nous allons d’ailleurs regarder dès maintenant le module les mettant à disposition.
Cette librairie implémente les différentes stratégies de cache. Comme je vous l’avais indiqué dans l’article précédent vous pouvez lire le offline cookbook de Jake Archibald qui décrit ces différentes stratégies.
networkFirst : essaye de lancer la requête en mode connecté. Si le réseau répond la réponse est stockée dans le cache et servie. Si la réponse dépasse un timeout défini ou si le réseau est inaccessible le SW retourne la ressource si elle est présente dans le cache. Cette stratégie est intéressante quand vous voulez afficher les données les plus récentes.
cacheFirst : si la ressource est dans le cache elle est directment renvoyée. Sinon on charge la ressource. Cette stratégie est utilisée pour des éléments qui ne changent pas (sinon vous devez mettre en place une stratégie pour mettre à jour ces ressources quand elles changent).
cacheOnly : on ne regarde que dans le cache. Si la ressource n’est pas là nous avons une erreur. Intéressant sur mobile par exemple pour préserver la batterie quand elle commence à faiblir.
networkOnly : inverse on interroge toujours le réseau. Cette stratégie est un peu inutile vu qu’il se passe la même chose si vous n’utilisez pas de services workers
staleWhileRevalidate : on lance 2 requêtes en parallèle (une dans le cache une sur le réseau). La version en cache étant plus rapide à répondre, elle est affichée. Mais cette version sera remplacée par le résultat de la requête lancée sur le réseau (si cette dernière s’est bien passée).
Vous pouvez voir des exemples de déclaration dans le paragraphe précédent
Quand vous utilisez des services workers ou plus généralement du cache de ressources dans le navigateur web vous avez toujours la hantise que votre cache soit mal configuré et que les ressources ne soient jamais mise à jour.
Grâce à cette librairie vous pouvez
limiter la taille du cache en limitant le nombre de requêtes pouvant être "cachée"
définir une date d’expiration
const requestWrapper = new workbox.runtimeCaching.RequestWrapper({
cacheName: 'runtime-cache',
plugins: [
// The cache size will be capped at 10 entries.
new workbox.cacheExpiration.Plugin({maxEntries: 10, maxAgeSeconds: 10})
]
});
// ce 'RequestWrapper' peut être ajouté au cache handler d'une route
const route = new workbox.routing.RegExpRoute({
match: ({url}) => url.domain === 'dev-mind.fr',
handler: new workbox.runtimeCaching.StaleWhileRevalidate({requestWrapper})
});
Workbox a l’ambition d’apporter plus que du cache de ressources.
Les services workers vous permettent de servir votre site web si le réseau est défaillant ou absent. Si un utilisateur lance une action et que le réseau n’est pas accessible cette dernière est perdue. Cette librairie va vous aider à empiler les demandes dans une queue et ces demandes seront exécutées quand le réseau sera à nouveau disponible (cette librairie se base sur l’API JavaScript Background Sync).
Le principe est d’instancier une QueuePlugin et de la passer au RequestWrapper
let bgQueue = new workbox.backgroundSync.QueuePlugin({
callbacks: {
onResponse: async(hash, res) => {
// une notification sera affichée quand tout est OK
self.registration.showNotification('Background sync demo', {
body: 'Product has been purchased.',
icon: '/images/shop-icon-384.png',
});
},
onRetryFailure: (hash) => {},
},
});
const requestWrapper = new workbox.runtimeCaching.RequestWrapper({
plugins: [bgQueue],
});
const route = new workbox.routing.RegExpRoute({
regExp: new RegExp('^https://jsonplaceholder.typicode.com'),
handler: new workbox.runtimeCaching.NetworkOnly({requestWrapper}),
});
const router = new workbox.routing.Router();
router.registerRoute({route});
Cette librairie vous permet de paramétrer finement quels objets doivent être mis en cache ou non. Pour celà vous pouvez intercepter le statut de la réponse ou les entêtes de cette réponse.
Un petit exemple dans lequel nous ne voulons mettre en cache que les réponses avec le statut 0 ou 200
const cacheablePlugin = new workbox.cacheableResponse.Plugin({
statuses: [0, 200]
});
const requestWrapper = new workbox.runtimeCaching.RequestWrapper({
cacheName: 'runtime-cache',
plugins: [
cacheablePlugin
]
});
const route = new workbox.routing.RegExpRoute({
match: ({url}) => url.domain === 'example.com',
handler: new workbox.runtimeCaching.StaleWhileRevalidate({requestWrapper})
});
Cet utilitaire utilise l’API JavaScript Broadcast Channel et permet d’effectuer une action quand une entrée dans le cache a été mise à jour.
const requestWrapper = new workbox.runtimeCaching.RequestWrapper({
cacheName: 'text-files',
plugins: [
new workbox.broadcastCacheUpdate.BroadcastCacheUpdatePlugin(
{channelName: 'cache-updates'})
],
});
const route = new workbox.routing.RegExpRoute({
regExp: /.txt$/,
handler: new workbox.runtimeCaching.StaleWhileRevalidate({requestWrapper}),
});
const router = new workbox.routing.Router();
router.registerRoute({route});
Ensuite dans votre code vous pouvez écouter l’événement du même nom
const updateChannel = new BroadcastChannel('cache-updates');
updateChannel.addEventListener('message', event => {
console.log('Cache updated: ${event.data.payload.updatedUrl}');
});
Le but de cet article n’est pas d’être exhaustif. Je vous laisse consulter le site Workbox pour plus d’exemples. Des nouvelles fonctionnalités devraient apparaître prochainement.
Une fois que vous avez mis en ligne votre site, vous pouvez vérifier son comportement et la qualité en utilisant l’outil open source Lighthouse. Il vérifie les aspects liés à la performance, l’accessibilité, le comportement offline, si votre site est responsive… Vous pouvez utiliser soit le client node disponible sous npm, soit le plugin Chrome.
Pour lancer un audit du site dev-mind.fr vous devez aller sur le site et lancer le plugin Chrome qui va générer le rapport suivant. Je vous conseille de désactiver les différentes extensions de votre navigateur avant car certaines ont tendance à fausser les rapports en ajoutant des scripts à votre site.
Le rapport expose différents indicateurs et propose des solutions pour optimiser votre page (lien vers les docs correspondantes).
Ainsi s’achève notre voyage au pays des services workers. Avec ces 3 articles je souhaitais montrer qu’il était simple et rapide d’exposer des fonctionnalités hors ligne ou sur un réseau dégradé. Pour conclure je rappelerai juste quelques conseils
utilisez une librairie pour générer vos services workers
faites du cache busting, en intégrant un numéro de révision dans le nom de vos ressources afin de vous prémunir des problèmes de cache
utiliser un nom unique pour votre cache ou zone de cache. Ce nom est utilisé pour épurer les ressources quand votre service worker est mis à jour
paramétrer toujours une date d’expiration de vos ressources dans le cache
vérifier régulièrement le comportement de votre site sur les différents navigateurs du marché qui n’implémentent pas les normes à la même vitesse.