Progressive

WEB APPS

Text

Emmanuel DEMEY

@EmmanuelDemey

Zenika Lille

 

 

 

PWAs are the missing technology link to bring the best features of the web to solve the issues of native apps and closed marketplaces.


Chris Heilmann

Alibaba
(e-commerce/China)

  • 76% de conversions en plus entre navigateurs;
     
  • 14% en plus d'utilisateurs actifs/mois sous iOS; 30% en plus sous Android;
     
  • 4X plus de taux d'itéraction à partir de "Add to Homescreen".

Source : https://developers.google.com/web/showcase/2016/pdfs/alibaba.pdf

FlipKart
(e-commerce/India)

  • Temps passé sur Flipkart lite vs. expériences mobile précedentes: 3.5 minutes vs 70 seconds.
  • 3x plus de temps passé sur le site;
  • 40% en plus de taux d'engagement
  • 70% en plus de taux de conversion entre ceux arrivant via "Add to Homescreen";
  • 3x mois d'usage de données.

https://developers.google.com/web/showcase/2016/flipkart

Caracteristiques

  • Une seule source
  • "Progressive enhanced"
  • "Offline first"
  • Rapide
  • Engageant
  • Installable

Les requis

  • Design "responsive"
  • Connexion sécurisée
  • Web Manifest
  • Service Workers

Par où commencer? 

Audit

LightHouse

  • Outil permettant d'auditer une application web
  • Disponible via un plugin Chrome ou via un module NPM
  • Retourne un score (sur 100) indiquant le taux de conformité avec les PWA
  • Règles sur différents domaines : Performance, Sécurité, A11Y, RWD, ...
npm i -g lighthouse

lighthouse https://google.fr

LightHouse

LightHouse

Le Web App Manifest

Le Manifest c'est...

  • Méta-données associées à une web app (format json)
     
  • Permet "d'installer" une web app
     
  • Il nous donne un contexte de navigation "top-level"
     
  • Il "fait autorité"

Lien vers le Manifest

<!doctype html>
<html lang="fr">
    <head>
        <meta http-equiv="Content-Type" 
            content="text/html; charset=utf-8" />
	
        <meta content="width=device-width, initial-scale=1.0" 
            name="viewport" />
        
        <title>Ma première PWA</title>
        <link rel="manifest" href="manifest.json">

    </head>

    <body>
    
    </body>
</html>

Conditions d'installation

  • name meta-data
  • Une icône de taille adequate
  • Connexion sécurisée
  • Doit "fonctionner" sans connexion
  • Visites récurrentes
  • Design Responsive

Conditions de validation d'un Manifest

  • Service Worker enregistré
  • name ou short_name
  • L'url de départ : start_url
  • Les icônes : icons (minimum 144px)

Style

Display

Manifest - Validation

https://manifest-validator.appspot.com

Support

Install banner

  • Fichier Manifest défini et valide
     
  • Service Worker enregistré
     
  • Servi via HTTPs
     
  • Visité 3 ou 4 fois, avec au moins 5 minutes entre les visites

 

window.addEventListener(
    "beforeinstallprompt", (e) => { 
      if (isLoggedIn()) {
        e.preventDefault();
      } 
    });

Manifest - Event

HTTP2

  • 15 ans d'existence
  • Applications de plus en plus lourdes
  • Navigateur limitant le nombre de requêtes simultanées
  • Utilisation de  mécanismes permettant de réduire le temps de chargement
    • Minification
    • Concaténation
    • Domain Sharding

HTTP1

  • Compatible avec HTTP1
  • Fonctionnement avec TLS
  • Diminue le nombre d'aller retours
  • Compression des Headers
  • Limite le nombre de connexions
    • Système de multiplexage
  • Server Push

HTTP2

HTTP2

HTTP2 avec Express

const fs = require('fs')

const javascript = fs.readFileSync('./app/assets/script.js');

app.get('/', (req, res) => {
    res.push('/script.js', { 
        response: {'content-type': 'application/javascript'}})
            .end(javascript)
    

    res.sendFile(__dirname + '/app/index.html');
})

const options = {
    key: fs.readFileSync('./server.key'),
    cert: fs.readFileSync('./server.crt')
};

require('spdy').createServer(options, app).listen(3002);

Support

Service Worker

Service Worker

Service Worker

  • Proxy entre votre application et votre serveur
  • Nécessite HTTPs 
  • Pas d'accès au DOM
  • Fonctionnalité de Cache, Push, Geofencing, Background Sync
  • Evenement d'un SW : install, activate, fetch et sync
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('./sw.js')
    .then(req => {
        console.log('Success');
      }).catch(error => {
        console.log('Error');
      });
};

Service Worker

self.addEventListener('fetch', event => {

  event.respondWith(
        fetch(event.request)
  );

});

Service Worker

self.addEventListener('fetch', event => {

  event.respondWith(
        fetch(event.request)
            .then(response => {
                if(response.status === 404) {
                    ...
                }
                return response;
            })
  );

});
interface Request {
    method
    url
    headers
    context
    referrer
    referrerPolicy
    mode
    credentials
    redirect
    integrity
    cache
    bodyUsed
}
self.addEventListener('fetch', event => {
  
  /*
  let options = {
    status: 200, 
    statustext: 'OK',
    headers: { ... }
  };
  */
  event.respondWith(
        
       new Response('Bonjour Devoxx', /*opts*/)
        
  );

});

Support

App Shell

  • Rendre le layout du site accessible offline
  • Afficher la structure du site le plus rapidement possible
  • Utilisation de l'API Cache
  • Precache des ressources statiques durant l'événement install
  • Réutilisation du Cache dans l'événement fetch

App Shell

  • Interface permettant de stocker des paires Request / Response
  • API se basant sur les Promise
  • Plusieur cache pour une même application
  • Aucun cache implémenté par défaut
  • Ajout, mise à jour et suppression à la charge du développeur

App Shell - Cache

App Shell - Cache

CacheStorage.open(cacheName)

Cache.match(request, options)

Cache.matchAll(request, options)

Cache.add(request)

Cache.addAll(requests)

Cache.put(request, response)

Cache.delete(request, options)

App Shell - install

const cacheName = 'devoxx';

const filesToCache = [
  '/',
  '/script.js',
  '/css/style.css'
];

self.addEventListener('install', e => {
  e.waitUntil(
    caches.open(cacheName).then(cache => {
      return cache.addAll(filesToCache);
    })
  );
});

App Shell - fetch

self.addEventListener('fetch', e => {
  e.respondWith(
      caches.match(e.request)
        .then(response => {
            return response || fetch(e.request);
        })
  );
});

App Shell - activate

self.addEventListener('activate', e => {
  e.waitUntil(
    caches.keys().then(keyList => {
      return Promise.all(keyList.map(key => {
        if (key !== cacheName) {
          return caches.delete(key);
        }
      }));
    })
  );
});

Prévenir vos utilisateurs

online / offline

if(!navigator.onLine) {
    ...
}

window.addEventListener("offline", () => {
    ...
}, false);

window.addEventListener("online", () => {
    ...
}, false);

IndexedDB

  • API permettant de stocker des données côté client (clé => valeur)
  • La donnée stockée peut être un objet complexe
  • Base de données non relationnelle
  • API asynchrone, utilisant des callbacks :(
  • Syntaxe assez verbeuse
    • Vendor Prefixes
    • Cursor
    • Indexes
    • Transaction
  • Utilisation de librairies externes pour simplifier l'utilisation

IndexedDB

const indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB;

// Open (or create) the database
const open = indexedDB.open("Devoxx", 1);

// Create the schema
open.onupgradeneeded = function() {
    let db = open.result;
    let store = db.createObjectStore("Conference", {keyPath: "id"});
    let index = store.createIndex("SpeakerIndex", ["speaker"]);
};

open.onsuccess = function() {
    // Start a new transaction
    let db = open.result;
    let tx = db.transaction("Conference", "readwrite");
    let store = tx.objectStore("Conference");
    let index = store.index("SpeakerIndex");

    // Add some data
    store.put({id: 12345, speaker: "Manu", "title": "PWA");
    store.put({id: 67890, speaker: "Aurélien", "title": "VueJS");
    
    // Query the data
    let getPWA = store.get(12345);
    let getVueJS = index.get(["Aurélien"]);

    getPWA.onsuccess = function() {
        console.log(getPWA.result);
    };

    getVueJS.onsuccess = function() {
        console.log(getBob.result);
    };

    // Close the db when the transaction is done
    tx.oncomplete = function() {
        db.close();
    };
}
  • Librairie plus simple à utiliser
  • Fallback vers WebSQL et localStorage
  • Support des callbacks et des promises
  • API similaire à localStorage

LocalForage

localforage.setItem('somekey', {}).then(value => {
    console.log(value);
})
localforage.getItem('somekey').then(value => {
    console.log(value);
});
localforage.removeItem('somekey').then(() => {
    console.log('Key is cleared!');
});

IndexedDB

Support

Background Sync

  • Ajout d'un événement Sync aux Service Workers
     
  • Assure l'envoie d'une requête lorsque l'utilisateur est offline
     
  • Traitement exécutée tant que la Promise est rejetée

Background Sync

Background Sync

self.addEventListener('sync', event => {
  if (event.tag == 'SYNC_SETTINGS') {
    event.waitUntil(updateSettings());
  }
});
navigator.serviceWorker.getRegistration()
    .then(registration => {
        registration.sync.register(
            'SYNC_SETTINGS').then(() => {
                console.log('Sync registered');
            });
    })  

sw-precache

  • Module NPM permettant d'automatiser le precache de votre AppShell
  • Génére un Service Worker avec la gestion de install, activate et fetch
  • Intégrable avec un SW existant
  • Intégrable dans votre outil de build : NPM, gulp, Grunt, WebPack, ...

sw-precache

sw-precache

const swPrecache = require('sw-precache');
const rootDir = 'dist/my-folder';

swPrecache.write(`${rootDir}/sw.js`, {
    staticFileGlobs: [
        'app/index.html',
        `${rootDir}/**/*.css`,
        `${rootDir}/**/*.js`,
        `${rootDir}/**/*.svg`,
    ],
    stripPrefixMulti: {
        'dist/': '/'
    },
    importScripts: [
        '/sw-dynamic.js'
    ]
});

sw-toolbox

  • Librairie permettant de gérer le cache au runtime
  • Nécessité d'importer la librairie dans votre Service Worker
  • Plusieurs stratégies de cache disponible
  • Cache configurable : maxEntries, maxAgeSeconds
  • Gestion des URLs via la syntaxe similaire à Express

sw-toolbox

sw-toolbox

importScripts('/sw-toolbox.js')

toolbox.router.get(urlPattern, handler, options)
toolbox.router.post(urlPattern, handler, options)
toolbox.router.put(urlPattern, handler, options)
toolbox.router.delete(urlPattern, handler, options)
toolbox.router.head(urlPattern, handler, options)
toolbox.router.any(urlPattern, handler, options)

sw-toolbox

toolbox.networkFirst

toolbox.cacheFirst

toolbox.fastest

toolbox.cacheOnly

toolbox.networkOnly

Emmanuel DEMEY

@EmmanuelDemey

Zenika Lille

 

 

 

PWA

By Emmanuel Demey

PWA

  • 2,677