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,662