Blog
Les services workers au service des notifications

Partager

Qu'est-ce qu'un service worker ?

Un service worker est un script indépendant qui sera enregistré dans le navigateur et fonctionnant comme un "proxy" entre l'application et le navigateur. Il continue de tourner en arrière-plan lorsque le site qui l'a créé n'est pas ouvert dans le navigateur, ce qui permet de faire de nombreuses choses : mise en cache, pre-loading et pre-rendering, fonctionnement en mode déconnecté.

Mais dans cet article, nous allons plutôt montrer la puissance des services workers dans l'utilisation des notifications.


Note importante : les services workers peuvent être enregistrés que si le HTTPS est présent. Pour le développement, il n'est cependant pas obligatoire de se créer un certificat SSL, car localhost est tout de même autorisé pour l'enregistrement et l'utilisation d'un service worker.

Enregistrement d'un service worker

Pour enregistrer un service worker, il suffit d'appeler la méthode register en lui passant en paramètre le chemin vers le script qui contiendra le service worker. Cette fonction retourne une promesse contenant un objet ServiceWorkerRegistration.

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js')
    .then(registration => console.log('Le service worker est enregistré'))
    .catch(console.error);
} else {
    console.warn('Le navigateur ne prend pas en charge les services worker');
}

Note : Il ne faut pas oublier de vérifier la compatibilité du navigateur avec : if ('serviceWorker' in navigator). À l'heure où ces lignes sont écrites, les services workers ne sont pas compatibles sur tous les navigateurs et sont encore en phase expérimentale. Les dernières versions de Chrome et Firefox sont compatibles, tandis que Safari vient d'ajouter la compatibilité dans sa dernière mise à jour, Edge n’a pas encore sorti sa prise en charge. voir les compatibilités

Maintenant, il faut bien évidemment créer le script du service worker, sinon notre navigateur va gentiment nous dire qu'il ne le trouve pas !

Nous allons donc créer le fichier "service-worker.js" et y mettre ceci :

const version = 1;

self.addEventListener('install', () => console.log(`Installation du service worker v${version}`));
self.addEventListener('activate', () => console.log(`Activation du service worker v${version}`));

Maintenant, examinons ce code. Nous écoutons deux événements : "install" et "activate". Et pour chacun d'eux, nous logguons un message.

Au premier chargement de l'application, nous y retrouverons donc les deux logs. Cependant, si nous rechargeons la page, nous ne verrons plus ces logs. Nous reviendrons sur cette notion un peu plus tard.

Nous avons enregistré notre premier service worker. Nous pouvons maintenant le retrouver dans nos outils de développement !

Sur Google Chrome, il suffit de se rendre dans l'inspecteur, puis dans l'onglet "Application", une rubrique "Service Workers" est disponible. C'est ici que nous retrouvons le service worker que nous venons d'enregistrer, et si nous ouvrons le panel "Service workers from other domains", nous y retrouvons tous les services workers auxquels notre navigateur s'est enregistré.

Service workers enregistrés dans le navigateur

Revenons maintenant sur nos deux événements écoutés par le service worker. L'événement "install", comme son nom l'indique, est lancé lorsque le service worker s'installe. Ce dernier s'installe à chaque mise à jour du service worker. Ainsi, si nous changeons la version du service worker, l'événement va être de nouveau appelé.

Service workers après modification sans activation

Comme nous pouvons le remarquer, l'événement "activate" n'a pas été lancé. Si nous retournons voir notre service worker, nous pouvons apercevoir qu'il y a deux id dont un qui est activé et un second en attente d'activation. En fait, la nouvelle version du service worker s'est installée, mais elle n'est pas activée. Ce qui signifie que le service worker qui est en train de s'exécuter est l'ancien (la version 1). Le navigateur attend que tous les onglets liés à l'application soient fermés pour activer le service worker.

Il est cependant possible de forcer l'activation de la nouvelle version sans avoir à fermer l'onglet et l'ouvrir à nouveau. Pour cela nous pouvons directement cliquer sur "skipWaiting" dans les outils de développements à côté du statut "waiting to activate". Mais cette méthode est manuelle, ce qui veut dire que nous ne pouvons pas forcer tous les utilisateurs à activer la nouvelle version du service worker de cette façon. Il faut donc faire attention avant de publier une nouvelle version du service worker !

Une autre manière de faire est d'y ajouter la fonction skipWaiting() dans l'événement "install". Nous obtenons donc un service worker qui ressemble à :

const version = 3;

self.addEventListener('install', () => {
    console.log(`Installation du service worker v${version}`);
    return self.skipWaiting();
});

self.addEventListener('activate', () => console.log(`Activation du service worker v${version}`));

Cette fois-ci, au rechargement de la page, nous devons avoir les deux événements appelés.

Note : il est également possible de recharger le service worker via le button "update" du service worker dans outils de développement plutôt qu'en rechargeant la page.

Les notifications

Passons maintenant à l'ajout des notifications dans le service worker. Pour cela nous allons utiliser le principe de Push API. Dans le principe, l'utilisateur s'inscrit au push serveur qui retournera une clé publique. (Nous pouvons par exemple utiliser Google Cloud Messaging pour envoyer des notifications push). Il faut garder cette clé du côté applicatif et l'associer à l'utilisateur afin de pouvoir lui envoyer par la suite des notifications. L'envoi de notification se fait via un endpoint auquel il faudra ajouter la clé de l'utilisateur que nous souhaitons notifier. Pour simplifier cela, nous n'allons pas utiliser de serveur push dans cet exemple. Nous allons plutôt simuler les push message directement via la console de développement.

Maintenant, côté service worker, il suffit d'ajouter un listener sur l'événement "push" :

self.addEventListener('push', event => console.log(event.data.text()));

Dans l'exemple nous logguons event.data.text(). La fonction text() permet de récupérer la valeur de data sous la forme d'une string.

Nous verrons un peu plus tard qu'il y a également la méthode json() qui permet de récupérer les données sous la forme d'un JSON. Ce qui sera très pratique pour récupérer les informations dont nous aurons besoin pour créer la notification.

Maintenant, testons ! Pour cela, il suffit de mettre le message que nous souhaitons envoyer dans l'input "push" et de cliquer sur le bouton.

Envoi d'un message push

Et voilà ! Nous avons bien reçu le texte.

Il ne reste plus qu'à ajouter les notifications. Pour cela il faut d'abord penser à demander l'autorisation d'envoyer des notifications à l'utilisateur, voici donc la nouvelle version du fichier app.js :

if ('serviceWorker' in navigator) {
    Notification.requestPermission(permission => {
        if(!('permission' in Notification)) {
            Notification.permission = permission;
        }
        return permission;
    })
    .then(() => navigator.serviceWorker.register('service-worker.js'))
    .catch(console.error);

} else {
    console.warn('Le navigateur ne prend pas en charge les services workers');
}

Ensuite, il faut ajouter le code permettant d'envoyer les notifications depuis le service worker :

const version = 5;

self.addEventListener('install', () => {
    console.log(`Installation du service worker v${version}`);
    return self.skipWaiting();
});

self.addEventListener('activate', () => console.log(`Activation du service worker v${version}`));

self.addEventListener('push', event => {
    const dataJSON = event.data.json();

    const notificationOptions = {
        body: dataJSON.body,
    };

    return self.registration.showNotification(dataJSON.title, notificationOptions);
});

Cette fois-ci, nous utilisons la méthode json() à la place de la méthode text(). Ensuite, nous récupèrons le corps ainsi que le titre envoyés dans les données de l'événement push, puis nous appellons la fonction showNotification() (voir la documentation)

Il ne reste plus qu'à tester notre application. Pour cela, il faut remplacer le texte que nous avions précédemment dans les outils de développement par un JSON adapté. Voici le JSON utilisé pour l'exemple :

{
    "title": "Titre de ma notification",
    "body": "Contenu de ma notification"
}

Et voici le résultat :

Résultat de la réception d'une notification

D'accord, j'arrive à envoyer une notification, mais je n'ai pas besoin des services workers pour faire cela ?

C'est vrai ! Nous aurions très bien pu générer une notification sans service worker grâce à l'API Notification. Mais l'utilisation de cette API à des limites... Au moment où nous écrivons ces lignes, cette fonctionnalité n'est pas prise en charge par les navigateurs mobiles par exemple. (voir la compatibilité)

De plus, l'utilisation des services workers pour générer une notification permet de le faire même si l'application web n'est pas ouverte dans un onglet.

Testez ! Fermez tous les onglets de votre application de test, rendez-vous dans les outils de développement et cherchez votre service worker dans la liste. Ensuite, envoyez-vous un nouveau message push avec le JSON utilisé plus tôt.

Normalement, vous avez dû recevoir la notification malgré le fait que l'application soit fermée. C'est grâce à votre service worker qui continue de tourner dans votre navigateur indépendamment de votre application.

On peut donc facilement imaginer un site qui envoie sa newsletter par notification plutôt que par mail. Les utilisateurs n'ont plus besoin d'être sur le site pour recevoir les notifications ! De plus, les services workers gèrent très bien les notifications sur smartphone par exemple et il est possible d'y ajouter plein de paramètres pour personnaliser la notification, comme créer une vibration personnalisée à la réception de la notification, changer l'icône de la notification, etc.

Pour aller plus loin

Pour aller plus loin, nous pouvons par exemple ajouter une action sur le clic de la notification. Pour cela, nous avons l'événement notificationclick. Nous pouvons donc ajouter une variable dans notre JSON qui contiendra l'URL du lien vers lequel nous souhaitons être rediriger. Il ne faut pas oublier d'ajouter l'URL aux données de la notification pour pouvoir la récupérer depuis l'événement notificationclick.

const version = 6;

self.addEventListener('install', () => {
    console.log(`Installation du service worker v${version}`);
    return self.skipWaiting();
});

self.addEventListener('activate', () => console.log(`Activation du service worker v${version}`));

self.addEventListener('push', event => {
    const dataJSON = event.data.json();

    const notificationOptions = {
        body: dataJSON.body,
        data: {
            url: dataJSON.url,
        }
    };

    return self.registration.showNotification(dataJSON.title, notificationOptions);
});

self.addEventListener('notificationclick', event => {
    const url = event.notification.data.url;
    event.notification.close();
    event.waitUntil(clients.openWindow(url));
});

Maintenant, si nous testons avec le JSON suivant :

{
    "title": "Titre de ma notification",
    "body": "Contenu de ma notification",
    "url": "https://www.bigint.fr"
}
Lorsque nous cliquerons sur la notification, le navigateur ouvrira la page d'accueil de BigInt.

Encore plus loin ?

Je vous avais dit au début de cet article que nous allions uniquement simuler l'envoi de notification push via les outils de développement ?

Et bien, je vous ai menti, si vous le voulez bien, nous allons aller un peu plus loin (mais pas de trop quand même 😉). Nous allons utiliser Push Companion qui est une interface web pour nous permettre de s'inscrire à un serveur push facilement afin de tester notre application. Si vous vous rendez dessus, vous y trouverez une public api key ainsi qu'une private api key (que nous n'utiliserons pas ici).

Gardez le site ouvert dans un coin, nous en aurons besoin plus tard. Et pour un résultat plus sympathique, vous pouvez ouvrir le site sur un autre navigateur, voir même un autre appareil connecté à un autre réseau ! Bien que la dernière solution soit un peu moins pratique, car il faudra pouvoir vous passer un fichier JSON d'un appareil à l'autre pour effectuer notre test. Mais avec un outil comme Slack par exemple, rien de plus simple !

Nous allons d'abord adapter notre code de la page web. L'avantage est que le code du service worker n'a plus besoin de changer actuellement.

Pour commencer, ajoutez dans app.js la fonction suivante :

function urlB64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

Je ne vous demande pas de comprendre cette fonction. Elle va juste nous permettre de convertir notre clé API en un tableau d'entier qui sera interprété par le pushManager.

Ensuite, nous allons ajouter ces quelques lignes :

const apiKey = '<-- Votre public api key -->';
const applicationServerKey = urlB64ToUint8Array(apiKey);
let swRegistration;
Il vous faudra mettre la clé API publique fournie par le Push Companion. La variable swRegistration sera utilisée un peu plus tard.

Ensuite, nous y ajoutons le code permettant l'inscription au serveur push. Voici le fichier app.js complet :

function urlB64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

const apiKey = '<-- Votre public api key -->';
const applicationServerKey = urlB64ToUint8Array(apiKey);
let swRegistration;

if ('serviceWorker' in navigator && 'PushManager' in window) {
    Notification.requestPermission(permission => {
        if(!('permission' in Notification)) {
            Notification.permission = permission;
        }
        return permission;
    })
    .then(() => navigator.serviceWorker.register('service-worker.js'))
    .then(registration => {
        //On garde l'enregistrement du service worker pour s'en resservir plus tard
        swRegistration = registration;

        //On attend que le service worker soit activé
        return navigator.serviceWorker.ready;
    })
    .then(() => swRegistration.pushManager.subscribe({applicationServerKey: applicationServerKey, userVisibleOnly: true}))
    .then(subscription => console.log(JSON.stringify(subscription)))
    .catch(console.error);

} else {
    console.warn('Le navigateur ne prend pas en charge les services workers ou les push manager');
}

Nous y avons donc ajouté une condition dans le if permettant de vérifier que PushManager est disponible. Puis après avoir enregistré notre service worker, nous enregistrons dans la variable swRegistration qui est l'objet ServiceWorkerRegistration. Nous attendons que le service worker soit bien activé. Car si nous lançons ce script alors que le service worker n'a jamais été enregistré, une erreur surviendra par la suite. Bien que nous ayons self.skipWaiting() dans notre service worker, cette étape est asynchrone. Donc si nous n'attendons pas l'activation, nous ne pouvons pas entrer dans le prochain then() sans avoir activé notre service worker !

Une fois le service worker prêt, nous l'abonnons à notre push server via la fonction subscribe() du pushManager. Ensuite, nous récupèrons la réponse sous forme d'un objet JSON, et nous la logguons sans oublier de la transformer en string avant.

Maintenant il ne reste plus qu'à recharger notre page, récupérer le JSON que nous venons de logguer et aller le mettre dans l'input "Subscription to Send To" du push companion !

Ensuite, il suffit de remettre notre JSON de paramètre de notre notification dans "Text to send" et d'envoyer le message.

screenshot push companion

Et voilà, nous recevons bien notre notification ! Vous pouvez essayer d'envoyer la notification depuis un autre appareil, de fermer tous les onglets du navigateur qui reçoit les notifications, la notification devrait quand même être présente.

Il ne vous reste plus qu'à utiliser une API Push depuis votre propre serveur et vous pourrez notifier tous vos utilisateurs !

Conclusion

Les services workers nous permettent de faire des choses très puissantes. Les notifications ne sont qu'une partie des possibilités qui nous sont offertes. D'autres événements sont disponibles comme l'événement "fetch" qui est lancé à chaque appel réseau. Nous pouvons donc imaginer de la mise en cache avec mise à jour du cache sans que l'utilisateur ne soit sur l'application web.

Cependant, c'est encore une technologie considérée comme expérimentale, et les navigateurs ne la prennent pas encore tous en charge. Ce qui rend leurs utilisations en production plutôt compliquées pour le moment. Bien qu'il soit possible de les utiliser pour ajouter des fonctionnalités assez puissantes, il n'est pas encore envisageable d'utiliser réellement les services workers pour une fonctionnalité indispensable au fonctionnement de l'application. Sauf si nous utilisons une autre façon de développer cette fonctionnalité pour les utilisateurs qui n'utilisent pas de navigateurs compatibles.

Les services workers sont de très bons outils pour nous aider à créer de véritables PWA. Ils sont peut-être l'avenir des applications mobiles, qu'en pensez-vous ?

comments powered by Disqus