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

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

Permiten engagement del usuario

 

Tutorial google

Gracias

 

Progressive Web Apps - Solutions

By Paolo Mosca

Progressive Web Apps - Solutions

Clase dentro de la iniciativa "barris digitals" - Solutions

  • 190