Mettre en place une Progressive Web App (PWA) est simple comme bonjour grâce à Vue.js, en particulier grâce au CLI Vue v3. Toutefois la mise en place des notifications push peut s’avérer piégeuse.

Vue.js sera utilisé pour la partie frontend, Python/Django et Django Rest Framework pour le backend, et Google Firebase Messaging (FCM) comme intermédiaire de messaging. FCM est nécessaire puisqu’il fera office de tierce partie en charge de pousser les notifications vers l’appareil utilisateur. Je sais, c’est assez décevant de devoir ajouter ce service externe à notre architecture, mais il n’y a malheureusement pas d’autre choix. Bien entendu il existe des alternatives à Firebase, telles que Pusher par exemple.

Firebase devra être intégré à plusieurs endroits dans votre code :

  • côté frontend afin que le navigateur écoute les nouvelles notifications en provenance de Firebase
  • côté frontend aussi sur la page où vous voulez demander à l’utilisateur s’il souhaite autoriser les notifications et, s’il accepte, obtenir un token de notification de la part de Firebase à envoyer à notre backend pour le stocker en base de données. Si un utilisateur utilise plusieurs navigateurs (ex : Chromium mobile sur smartphone, et Firefox desktop sur PC), plusieurs tokens lui seront associés en base de données, et les notifications seront reçues à différents endroits au même moment.
  • côté backend afin d’envoyer les notifications push à l’utilisateur via l’envoi d’un message à l’API Firebase. Firebase se chargera de récupérer votre message et de le router vers les bons navigateurs associés.

Veuillez garder à l’esprit que le standard PWA est encore en pleine évolution et pas encore intégré de la même façon par tous les navigateurs sur toutes les plateformes. Par exemple les notifications push ne sont pas encore acceptées par iOS à l’heure où j’écris cet article !

PWA Vue.js

Installez le CLI Vue.js via la commande npm suivante (installez NPM en premier lieu si nécessaire) :

npm i -g @vue/cli

Créez un nouveau projet PWA :

vue create <My Project Name>

Sélectionnez l’option “Manually select features” puis sélectionnez “Progressive Web App (PWA) support” :

Vue CLI v3

Sélectionnez toutes les autres options dont vous avez besoin et patientez le temps que le CLI Vue crée votre projet. Veuillez noter que le CLI crée automatiquement un registerServiceWorker.js dans le répertoire src et l’importe en haut de votre main.js. Ce fichier prendra en charge la génération automatique d’un fichier service-worker.js à la racine de votre projet au cours du build de production. Ce fichier est nécessaire pour permettre au navigateur de reconnaître votre site comme PWA.

Dans votre répertoire public, créez un fichier manifest.json qui décrit votre PWA : le nom de votre app, les icônes de l’app pour différentes tailles d’écrans, les couleurs… Les éléments importants sont start_url qui est l’url à ouvrir par défaut au lancement de votre PWA par l’utilisateur sur son smartphone, et gcm_sender_id qui est l’ID que toutes les web apps utilisant Firebase doivent utiliser (donc ne le changez pas). Vous pouvez spécifier beaucoup plus d’information dans ce fichier, pour cela jetez un oeil à la doc. Au final votre fichier devrait ressembler à ça :

{
  "name": "My App Name",
  "short_name": "My App Short Name",
  "icons": [{
      "src": "./img/icons/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "./img/icons/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon-60x60.png",
      "sizes": "60x60",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon-76x76.png",
      "sizes": "76x76",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon-120x120.png",
      "sizes": "120x120",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon-180x180.png",
      "sizes": "180x180",
      "type": "image/png"
    },
    {
      "src": "./img/icons/apple-touch-icon.png",
      "sizes": "180x180",
      "type": "image/png"
    },
    {
      "src": "./img/icons/favicon-16x16.png",
      "sizes": "16x16",
      "type": "image/png"
    },
    {
      "src": "./img/icons/favicon-32x32.png",
      "sizes": "32x32",
      "type": "image/png"
    },
    {
      "src": "./img/icons/msapplication-icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "./img/icons/mstile-150x150.png",
      "sizes": "150x150",
      "type": "image/png"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "background_color": "#000000",
  "theme_color": "#210499",
  "gcm_sender_id": "103953800507"
}

Veuillez noter que votre site doit absolument être en HTTPS afin que le navigateur accepte de lire le manifest.json et ainsi se comporter comme une PWA.

Si tout se passe correctement, la PWA est maintenant installable sur votre smartphone. Rendez-vous sur votre site via un navigateur mobile moderne comme Chrome. Si le navigateur détecte le manifest.json il vous propose normalement d’installer la PWA sur votre smartphone comme une application (ce n’est pas encore supporté par tous les navigateurs à l’heure où j’écris cet article).

Mise en place de Firebase

Afin que votre PWA supporte les notifications push, il faut que vous intégriez un service externe comme Firebase Cloud Messaging (FCM). Veuillez noter que FCM n’est qu’une petite partie de Firebase mais vous n’aurez pas besoin des autres fonctionnalités de Firebase (comme les bases de données, l’hébergement, …).

Veuillez donc créer un compte Firebase, rendez-vous sur votre console Firebase, créez un projet pour votre site, et récupérez les informations suivantes depuis les réglages de votre projet (attention, ces infos ne sont pas forcément faciles à identifier; pensez à ouvrir les différents onglets) :

  • Project ID
  • Web API Key
  • Messaging Sender ID
  • Server Key
  • créez un certificat web push et récupérez la Public Vapid Key générée

Backend Django

Je pars du principe ici que vous connaissez Django Rest Framework.

Dans Django, utilisez l’app tierce FCM Django afin de faciliter l’intégration de FCM (cette app prendra soin d’automatiquement sauvegarder et supprimer les tokens de notifications en BDD, et vous fournira des fonctions toutes prêtes pour envoyer les notifications à FCM).

Installez l’application grâce à pip install fcm-django, ajoutez-la à vos apps Django, et configurez-la (n’hésitez pas à adapter les réglages ci-dessous, le seul requis est FCM_SERVER_KEY pour l’authentification auprès de FCM) :

INSTALLED_APPS = (
        ...
        "fcm_django"
)

FCM_DJANGO_SETTINGS = {
        # authentication to Firebase
        "FCM_SERVER_KEY": "<Server Key>",
        # true if you want to have only one active device per registered user at a time
        # default: False
        "ONE_DEVICE_PER_USER": False,
        # devices to which notifications cannot be sent,
        # are deleted upon receiving error response from FCM
        # default: False
        "DELETE_INACTIVE_DEVICES": True,
}

Ajoutez une route dans urls.py vers le point d’accès FCM Django qui se chargera de recevoir le token de notification et de le stocker en BDD.

from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet

urlpatterns = [
  path('register-notif-token/',
    FCMDeviceAuthorizedViewSet.as_view({'post': 'create'}), name='create_fcm_device'),
]

Désormais, lorsque vous souhaitez envoyer une notification push à un utilisateur faites ce qui suit (il y a des chances pour que ce soit dans votre views.py) :

from fcm_django.models import FCMDevice

user = <Retrieve the user>
fcm_devices = FCMDevice.objects.filter(user=user)
fcm_devices.send_message(
  title="<My Title>", body="<My Body>", time_to_live=604800,
  click_action="<URL of the page that opens when clicking the notification>")

C’est à vous d’adapter la requête vers la BDD pour définir précisément à qui vous souhaitez envoyer la notification push. Ici j’envoie des notifications push vers tous les navigateurs d’un utilisateur, mais je pourrais aussi bien décider de n’envoyer les notifications qu’à un navigateur spécifique (appelé “device” dans la terminologie Django FCM).

Il existe d’autres paramètres disponibles à passer à la méthode send_message, n’hésitez pas à jeter un oeil à la doc mais aussi à la doc du projet Python sous-jacent sur lequel cette lib est basée.

Déclarer un time_to_live était nécessaire dans mon cas : Firebase dit qu’il existe un TTL par défaut mais il s’est avéré que ce n’était pas le cas me concernant (bug ?), ce qui fait que les notifications envoyées lorsque le device de l’utilisateur était coupé n’arrivaient jamais lorsque le device était rallumé.

Mettre en place les notifications push dans Vue.js

Créez un fichier firebase-messaging-sw.js dans votre répertoire public et mettez ce qui suit dedans :

importScripts('https://www.gstatic.com/firebasejs/5.5.6/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/5.5.6/firebase-messaging.js');

var config = {
    apiKey: "<Web API Key>",
    authDomain: "<Project ID>.firebaseapp.com",
    databaseURL: "https://<Project ID>.firebaseio.com",
    projectId: "<Project ID>",
    storageBucket: "<Project ID>.appspot.com",
    messagingSenderId: "<Messenging Sender ID>"
};

firebase.initializeApp(config);

const messaging = firebase.messaging();

Vous disposez maintenant d’un service worker valide qui se chargera d’écouter Firebase en arrière plan en attente de nouvelles notifications push.

Il est désormais temps de demander sa permission à l’utilisateur afin de lui envoyer des notifications et, s’il est d’accord, récupérer un token de notification de FCM et le stocker en BDD côté backend. Votre backend utilisera ce token pour envoyer des notifications push via FCM. C’est à vous de décider sur quelle page de votre app vous souhaitez recueillir la permission de l’utilisateur. Par exemple vous pourriez mettre en place cela sur la page d’accueil de votre application une fois l’utilisateur loggué. Vous pourriez faire quelque chose dans ce style :

import firebase from 'firebase/app'
import 'firebase/app'
import 'firebase/messaging'

export default {
  methods: {
    saveNotificationToken(token) {
      const registerNotifTokenURL = '/register-notif-token/'
      const payload = {
        registration_id: token,
        type: 'web'
      }
      axios.post(registerNotifTokenURL, payload)
        .then((response) => {
          console.log('Successfully saved notification token!')
          console.log(response.data)
        })
        .catch((error) => {
          console.log('Error: could not save notification token')
          if (error.response) {
            console.log(error.response.status)
            // Most of the time a "this field must be unique" error will be returned,
            // meaning that the token already exists in db, which is good.
            if (error.response.data.registration_id) {
              for (let err of error.response.data.registration_id) {
                console.log(err)
              }
            } else {
              console.log('No reason returned by backend')
            }
            // If the request could not be sent because of a network error for example
          } else if (error.request) {
            console.log('A network error occurred.')
            // For any other kind of error
          } else {
            console.log(error.message)
          }
        })
      },
    },
  mounted() {
    var config = {
      apiKey: "<Web API Key>",
      authDomain: "<Project ID>.firebaseapp.com",
      databaseURL: "https://<Project ID>.firebaseio.com",
      projectId: "<Project ID>",
      storageBucket: "<Project ID>.appspot.com",
      messagingSenderId: "<Messenging Sender ID>"
    }
    firebase.initializeApp(config)

    const messaging = firebase.messaging()

    messaging.usePublicVapidKey("<Public Vapid Key>")

    messaging.requestPermission().then(() => {
      console.log('Notification permission granted.')
      messaging.getToken().then((token) => {
        console.log('New token created: ', token)
        this.saveNotificationToken(token)
      })
    }).catch((err) => {
      console.log('Unable to get permission to notify.', err)
    })

    messaging.onTokenRefresh(function () {
      messaging.getToken().then(function (newToken) {
        console.log('Token refreshed: ', newToken)
        this.saveNotificationToken(newToken)
      }).catch(function (err) {
        console.log('Unable to retrieve refreshed token ', err)
      })
    })
  }
}

Conclusion

Mettre en place les notifications push dans une PWA n’est clairement PAS évident ! De nombreuses briques de votre application doivent être impliquées et vous devez comprendre comment fonctionne le service tiers que vous avez choisi (ici Firebase).

Veuillez garder en tête que la PWA est une techno encore jeune en pleine évolution. Plus important : ne basez pas la communication d’informations critiques uniquement sur les notifications push car elles sont moins fiables que d’autres systèmes tels que les SMS ou les emails…

Par ailleurs, n’oubliez pas d’utiliser les notifications push avec circonspection car un excès de notifications peut vite devenir fort agaçant pour l’utilisateur !

J’espère que vous avez apprécié ce tuto. N’hésitez pas à me faire un retour ou à partager vos idées via les commentaires !

Also available in English

Rate Limiting d'API avec Traefik, Docker, Go, et la mise en cache

Limiter l'utilisation de l'API en fonction d'une règle avancée de limitation du débit n'est pas si facile. Pour y parvenir derrière l'API NLP Cloud, nous utilisons une combinaison de Docker, Traefik (comme proxy inverse) et la mise en cache locale dans un script Go. Si cela est fait correctement, vous pouvez améliorer considérablement les performances de votre limitation de débit et étrangler correctement les demandes d'API sans sacrifier la vitesse des demandes. Continuer de lire