Progressive Web Apps
Me?
Paolo Mosca
tw: @paolomoscaBCN
React-Native Expert
Startup Advisor
Business & Tech
Passionate in Mobile
YOU?
Vuestra formación?
- Que lenguajes / frameworks estais utilizando?
- Cuales son vuestros objectivos profesionales?
Porqué PWA?
- Progresiva
- Adaptable
- Independiente de la conectividad
- Estilo app
- Fresca (service worker)
- Conforme W3C
- Segura (https)
- Push Notifications
- Instalable
- No necesita pasar por Appstore/Gplay
El Proyecto
Servicio meteo
Tutorial oficial de Google
Objectivos del proyecto
Crear una App
- Progresiva
- Adaptable
- Independiente de la conectividad (online/offline)
- Similar a una app tradicional (app shell)
- Actualizada (cache/service worker)
- Segura (https)
- Instalable
Antes de empezar
- Codigo workshop
- Test Server
- Chrome dev tools
Codigo Workshop
Una vez descargado el codigo, lo descomprimimos
y nos ponemos en el directorio "work"
Test Server
Una vez descargada la extensión la configuramos para que apunte a nuestra carpeta "work".
Funciona?
- configurado correctamente?
- el proyecto arranca?
- estamos listos?
Chrome Dev Tools
Estamos listos?
Let's start the war
index.html
<header class="header">
<h1 class="header__title">Weather PWA</h1>
<button id="butRefresh" class="headerButton" aria-label="Refresh"></button>
<button id="butAdd" class="headerButton" aria-label="Add"></button>
</header>
<main class="main">
<div class="card cardTemplate weather-forecast" hidden>
...
</div>
</main>
<div class="dialog-container">
...
</div>
<div class="loader">
<svg viewBox="0 0 32 32" width="32" height="32">
<circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle>
</svg>
</div>
App shell
Ejercicio 1
- Modificar app shell
- Cambiar titulo "Tiempo en Cataluña"
- Cabecera verde (#1C8E48)
Solución
<!-- en index.html -->
<head>
...
<title>Tiempo en Cataluña</title>
...
</head>
...
<header class="header">
<h1 class="header__title">Tiempo en Cataluña</h1>
...
</header>
<!-- en styles/inline.css -->
.header {
...
background: #1c8e48;
...
}
Ejercicio 2
- Arrancar la app
- Activar datos de prueba
Solución
<!-- en index.html -->
<script src="scripts/app.js" async></script>
<!-- en scripts/app.js -->
app.updateForecastCard(initialWeatherForecast);
Ejercicio 3
- Cambiar ciudades (Barcelona, Badalona, Sabadell, Tarragona, Lleida, Girona)
- Encontrar woeid Ciudades
Solución
<!-- en index.html -->
<option value="753692">Barcelona</option>
<option value="753443">Badalona</option>
<option value="765112">Lleida</option>
<option value="775246">Tarragona</option>
<option value="761600">Girona</option>
<option value="772339">Sabadell</option>
Ejercicio 4
- Cambiar unidad de °F a °C
- Cambiar unidad de mph a km/h
Solución
<!-- en scripts/app.js -->
/* en la query YQL */
statement += ` and u="c"`;
Vale... esto era facil
Ejercicio 5
- guardar lista de ciudades en localStorage
- al arranque enseñar ciudades guardadas
- modificar evento click (butAddCity) y guardar ciudades
<!-- en index.html -->
// TODO add saveSelectedCities function here
// TODO add startup code here
// TODO init the app.selectedCities array here
Solución
<!-- en scripts/app.js -->
// TODO add saveSelectedCities function here
app.saveSelectedCities = function() {
var selectedCities = JSON.stringify(app.selectedCities);
localStorage.selectedCities = selectedCities;
};
// TODO add startup code here
app.selectedCities = localStorage.selectedCities;
if (app.selectedCities) {
app.selectedCities = JSON.parse(app.selectedCities);
app.selectedCities.forEach(function(city) {
app.getForecast(city.key, city.label);
});
} else {
/* The user is using the app for the first time, or the user has not
* saved any cities, so show the user some fake data. A real app in this
* scenario could guess the user's location via IP lookup and then inject
* that data into the page.
*/
app.updateForecastCard(initialWeatherForecast);
app.selectedCities = [
{key: initialWeatherForecast.key, label: initialWeatherForecast.label}
];
app.saveSelectedCities();
}
document.getElementById('butAddCity').addEventListener('click', function() {
// Add the newly selected city
var select = document.getElementById('selectCityToAdd');
var selected = select.options[select.selectedIndex];
var key = selected.value;
var label = selected.textContent;
if (!app.selectedCities) {
app.selectedCities = [];
}
app.getForecast(key, label);
app.selectedCities.push({key: key, label: label});
app.saveSelectedCities();
app.toggleAddDialog(false);
});
Service worker
- Un service worker es una secuencia de comandos que tu navegador ejecuta en segundo plano
- puede acceder al DOM directamente
- puede comunicar con nuestra pagina web mediante postMessage
Vida de un Service Worker
- Separado de la web
- Instalación (en segundo plano)
- Activación
- Acceso caché
- Escucha red / mensajes
- Puede terminar / reiniciarse
-
necesita HTTPS
Ejercicio 6
- Creación service worker (caché)
- Instalación service worker
- Activación
<!-- en index.html -->
// TODO add service worker code here
Solución
Creamos service-worker.js al mismo nivel de index.html
var cacheName = 'tiempo-paso-6';
var filesToCache = [];
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
);
});
self.addEventListener('activate', function(e) {
console.log('[ServiceWorker] Activate');
});
<!-- en index.html -->
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('./service-worker.js')
.then(function() { console.log('Service Worker Registered'); });
}
Revision en DevTools
Ejercicio 7
- Actualizar la caché
- Escucha de fetch
Solución
self.addEventListener('activate', function(e) {
console.log('[ServiceWorker] Activate');
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (key !== cacheName) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
return self.clients.claim();
});
var filesToCache = [
... // ponemos los archivos de app shell
];
self.addEventListener('fetch', function(e) {
console.log('[ServiceWorker] Fetch', e.request.url);
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
});
- Cambiamos la estrategia cache-first-then-network
Ejercicio 8
Solución
<!-- en service-worker.js -->
var dataCacheName = 'weatherData-v1';
self.addEventListener('activate', function(e) {
...
if (key !== cacheName && key !== dataCacheName) {
...
});
self.addEventListener('fetch', function(e) {
console.log('[Service Worker] Fetch', e.request.url);
var dataUrl = 'https://query.yahooapis.com/v1/public/yql';
if (e.request.url.indexOf(dataUrl) > -1) {
e.respondWith(
caches.open(dataCacheName).then(function(cache) {
return fetch(e.request).then(function(response){
cache.put(e.request.url, response.clone());
return response;
});
})
);
} else {
... como antes ...
}
});
Conectar nuestra app con la caché
Ejercicio 9
Solución
<!-- en scripts/app.js -->
app.getForecast = function(key, label) {
...
if ('caches' in window) {
caches.match(url).then(function(response) {
if (response) {
response.json().then(function updateFromCache(json) {
var results = json.query.results;
results.key = key;
results.label = label;
results.created = json.query.created;
app.updateForecastCard(results);
});
}
});
}
...
}
Nota
<!-- en scripts/app.js -->
app.updateForecastCard = function(data) {
...
var cardLastUpdatedElem = card.querySelector('.card-last-updated');
var cardLastUpdated = cardLastUpdatedElem.textContent;
if (cardLastUpdated) {
cardLastUpdated = new Date(cardLastUpdated);
/* la tarjeta se actualiza solo si el dato es mas nuevo */
if (dataLastUpdated.getTime() < cardLastUpdated.getTime()) {
return;
}
}
...
}
Offline
Add to Homescreen
- Es posible instalar la app en la homescreen del telefono
- Chrome siguiendo algunos criterios sobre la frecuencia de visitas, propone de instalar la app mediante un banner
Hagamos que la app sea instalable
(manifest.json)
Ejercicio 10
Solución
<!-- en index.html -->
<head>
...
<link rel="manifest" href="/manifest.json">
<!-- Add to home screen for Safari on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Weather PWA">
<link rel="apple-touch-icon" href="images/icons/icon-152x152.png">
<!-- Windows support -->
<meta name="msapplication-TileImage" content="images/icons/icon-144x144.png">
<meta name="msapplication-TileColor" content="#2F3BA2">
...
</head>
<!-- en manifest.json -->
{
"name": "Weather",
"short_name": "Weather",
"icons": [{
"src": "images/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
}, {
"src": "images/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "images/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
}, {
"src": "images/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}, {
"src": "images/icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
}],
"start_url": "/index.html",
"display": "standalone",
"background_color": "#3E4EB8",
"theme_color": "#2F3BA2"
}
Geolocalización
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position) {
console.log("position", position);
}
}
Al arrancar la app pedir permisos al usuario y geolocalizarlo
Encontrar el woeid a partir de las coordinadas geográficas
Cambiar el objecto "city" en localStorage para gestionar las coordinadas actuales
Ejercicio 11
Solución
<!-- en index.html -->
app.selectedCities.forEach(function(city) {
console.log("city", city);
app.getForecast(city.key, city.label, city.coords);
});
...
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position) {
console.log("position", position);
if (position && position.coords) {
app.getForecast("local", "Local", position.coords);
app.selectedCities = [];
app.selectedCities.push({
key: "local",
label: "Local",
coords: {
latitude: position.coords.latitude,
longitude: position.coords.longitude
}
});
console.log("antes de guardar", app.selectedCities);
app.saveSelectedCities();
}
});
}
Push Notification
Gracias
Progressive Web Apps - Solutions
By Paolo Mosca
Progressive Web Apps - Solutions
Clase dentro de la iniciativa "barris digitals" - Solutions
- 185