Estructura de una aplicación web progresiva

This translation is incomplete. Please help translate this article from English

Ahora que conocemos la teoría detrás de las aplicaciones web progresivas (PWAs), veamos la estructura recomendada de una aplicación moderna. Empezaremos a analizar la aplicación  js13kPWA, el porque esta desarrollada de tal forma y que beneficios nos trae crear nuestras aplicaciones de tal manera.

Estructura de una aplicación

Existen dos enfoques principales para representar un sitio web. Desde el lado del servidor o desde el lado del cliente. Ambos tienen sus ventajas y desventajas, y uno puede mezclar los dos enfoques hasta cierto punto.

  • La renderizacion desde el servidor (SSR) significa que el sitio web es, salve la redundancia, representado en el servidor, lo cual nos da una carga mas rápida de la pagina, pero navegar entre varias de ellas requerirá que se descarguen nuevos contenidos en formato HTML. Esto funciona muy bien en los navegadores como Firefox , pero empieza a mostrar sus falencias en cuanto a los tiempos de carga cuando vamos navegando entre las paginas, y por lo tanto el rendimiento general percibido por el usuario se ve reducido. Esto se debe a que cargar una nueva pagina requiere un viaje de ida y vuelta al servidor.

  • La renderizacion desde el lado del cliente (CSR) permite que el sitio web sea actualizado en el navegador de manera casi instantánea mientras estemos navegando por las diferentes paginas, pero esto requiere de una mayor descarga inicial y a su vez de un mayor renderizado desde el lado del cliente al principio. En resumen: el sitio web sera mas lento en una visita inicial, pero sera mucho mas rápido cuando deseemos navegar.

Mezclar SSR con CSR puede llevar a los mejores resultados.Usted puede renderizar un sitio web desde el servidor, el cache que contiene, y entonces actualizar la representación desde el lado del cliente como vayas necesitando. La primer pagina cargara rápidamente debido al SSR, y la navegación entre las paginas sera veloz porque el cliente puede re-renderizar la pagina solo con las partes que han sufrido cambios.

PWAs pueden ser creadas con el enfoque que tu quieras, pero algunos sitios funcionaran mejor que otros. El enfoque mas popular es el concepto de "app shell", el cual mezcla SSR y CSR  de la forma que hemos descripto en el párrafo anterior, y sumado a esto le sigue la metodología del "offline first" la cual vamos a explicar en detalle en próximos artículos y usaremos en nuestra aplicación de ejemplo. También existe un nuevo enfoque que involucra a las Streams API, las cuales mencionaremos brevemente mas adelante.

App shell

El concepto de App shell esta principalmente destinado a cargar una interfaz del usuario mínima lo mas rápido posible y entonces almacenarla en cache para que puedan estar disponibles en estado offline para siguientes visitas y no tener que descargar todo el contenido de la aplicación. De esta manera,la proxima vez que alguien visite la aplicación desde su dispositivo, la UI carga desde el cache inmediatamente y cualquier otro nuevo contenido sera descargado desde el servidor (Si es que no esta disponible desde el cache).

Esta estructura es rápida, y a su vez se siente rápida, ya que el usuario ve "algo" instantáneamente cuando entra al sitio y no una circulo de carga en ua pantalla en blanco sin contenido. Esto también permite que el sitio web sea accesible estando offline si la conexión no se encuentra disponible.

Se puede controlar lo que es requerido desde el servidor y que es  recuperado desde el cache mediante un service worker, concepto que sera explicado en detalle en el aproximó articulo. Por ahora nos enfocaremos en la estructura en si misma.

¿Por que deberia usarlo?

Esta estructura le permite al sitio web beneficiarse de la mayoria des las caracteristicas de las PWAs — it caches the app shell and manages the dynamic content in a way that greatly improves the performance. In addition to the basic shell, you can add other features such as add to home screen or push notifications, safe in the knowledge that the app will still work OK if they are not supported by the user's browser — this is the beauty of progressive enhancement.

The website feels like a native app with instant interaction and solid performance while keeping all the benefits of the web.

Being linkable, progressive and responsive by design

It's important to remember the PWA advantages and keep them in mind when designing the application. The app shell approach allows websites to be:

  • Linkable: Even though it behaves like a native app, it is still a website — you can click on the links within the page and send a URL to someone if you want to share it.
  • Progressive: Start with the "good, old basic website” and progressively add new features while remembering to detect if they are available in the browser and gracefully handle any errors that crop up if support is not available. For example, an offline mode with the help of service workers is just an extra trait that makes the website experience better, but it's still perfectly usable without it.
  • Responsive: Responsive web design also applies to progressive web apps, as both are mainly for mobile devices. There are so many varied devices with browsers — it's important to prepare your website so it works on different screen sizes, viewports or pixel densities, using technologies like viewport meta tag, CSS media queries, Flexbox, and CSS Grid.

Different concept: streams

An entirely different approach to server- or client-side Rendering can be achieved with the Streams API. With a little help from service workers, streams can greatly improve the way we parse content.

The app shell model requires all the resources to be available before the website can start rendering. It's different with HTML, as the browser is actually streaming the data already and you can see when the elements are loaded and rendered on the website. To have the JavaScript "operational", however, it has to be downloaded in its entirety.

The Streams API allows developers to have direct access to data streaming from the server — if you want to perform an operation on the data (for example, adding a filter to a video), you no longer need to wait for all of it to be downloaded and converted to a blob (or whatever) — you can start right away. It provides fine-grained control — the stream can be started, chained with another stream, cancelled, checked for errors, and more.

In theory, streaming is a better model, but it's also more complex, and at the time of writing (March 2018) the Streams API is still a work-in-progress and not yet fully available in any of the major browsers. When it is available, it will be the fastest way of serving the content — the benefits are going to be huge in terms of performance.

For working examples and more information, see the Streams API documentation.

Structure of our example application

The js13kPWA website structure is quite simple: it consists of a single HTML file (index.html) with basic CSS styling (style.css), and a few images, scripts, and fonts. The folder structure looks like this:

Folder structure of js13kPWA.

The HTML

From the HTML point of view, the app shell is everything outside the content section:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<title>js13kGames A-Frame entries</title>
	<meta name="description" content="A list of A-Frame entries submitted to the js13kGames 2017 competition, used as an example for the MDN articles about Progressive Web Apps.">
	<meta name="author" content="end3r">
	<meta name="theme-color" content="#B12A34">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<meta property="og:image" content="icons/icon-512.png">
	<link rel="shortcut icon" href="favicon.ico">
	<link rel="stylesheet" href="style.css">
	<link rel="manifest" href="js13kpwa.webmanifest">
	<script src="data/games.js" defer></script>
	<script src="app.js" defer></script>
</head>
<body>
<header>
	<p><a class="logo" href="http://js13kgames.com"><img src="img/js13kgames.png" alt="js13kGames"></a></p>
</header>
<main>
	<h1>js13kGames A-Frame entries</h1>
	<p class="description">List of games submitted to the <a href="http://js13kgames.com/aframe">A-Frame category</a> in the <a href="http://2017.js13kgames.com">js13kGames 2017</a> competition. You can <a href="https://github.com/mdn/pwa-examples/blob/master/js13kpwa">fork js13kPWA on GitHub</a> to check its source code.</p>
	<button id="notifications">Request dummy notifications</button>
	<section id="content">
		// Content inserted in here
	</section>
</main>
<footer>
	<p>© js13kGames 2012-2018, created and maintained by <a href="http://end3r.com">Andrzej Mazur</a> from <a href="http://enclavegames.com">Enclave Games</a>.</p>
</footer>
</body>
</html>

The <head> section contains some basic info like title, description and links to CSS, web manifest, games content JS file, and app.js — that's where our JavaScript application is initialized. The <body> is split into the <header> (containing linked image), <main> page (with title, description and place for a content), and <footer> (copy and links).

The app's only job is to list all the A-Frame entries from the js13kGames 2017 competition. As you can see it is a very ordinary, one page website — the point is to have something simple so we can focus on the implementation of the actual PWA features.

The CSS

The CSS is also as plain as possible: it uses @font-face to load and use a custom font, and it applies some simple styling of the HTML elements. The overall approach is to have the design look good on both mobile (with a responsive web design approach) and desktop devices.

The main app JavaScript

The app.js file does a few things we will look into closely in the next articles. First of all it generates the content based on this template:

var template = "<article>\n\
    <img src='data/img/SLUG.jpg' alt='NAME'>\n\
    <h3>#POS. NAME</h3>\n\
    <ul>\n\
    <li><span>Author:</span> <strong>AUTHOR</strong></li>\n\
    <li><span>Twitter:</span> <a href='https://twitter.com/TWITTER'>@TWITTER</a></li>\n\
    <li><span>Website:</span> <a href='http://WEBSITE/'>WEBSITE</a></li>\n\
    <li><span>GitHub:</span> <a href='https://GITHUB'>GITHUB</a></li>\n\
    <li><span>More:</span> <a href='http://js13kgames.com/entries/SLUG'>js13kgames.com/entries/SLUG</a></li>\n\
    </ul>\n\
</article>";
var content = '';
for(var i=0; i<games.length; i++) {
    var entry = template.replace(/POS/g,(i+1))
        .replace(/SLUG/g,games[i].slug)
        .replace(/NAME/g,games[i].name)
        .replace(/AUTHOR/g,games[i].author)
        .replace(/TWITTER/g,games[i].twitter)
        .replace(/WEBSITE/g,games[i].website)
        .replace(/GITHUB/g,games[i].github);
    entry = entry.replace('<a href=\'http:///\'></a>','-');
    content += entry;
};
document.getElementById('content').innerHTML = content;

Next, it registers a service worker:

if('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/pwa-examples/js13kpwa/sw.js');
};

The next code block requests permission for notifications when a button is clicked:

var button = document.getElementById("notifications");
button.addEventListener('click', function(e) {
    Notification.requestPermission().then(function(result) {
        if(result === 'granted') {
            randomNotification();
        }
    });
});

The last block creates notifications that display a randomly-selected item from the games list:

function randomNotification() {
    var randomItem = Math.floor(Math.random()*games.length);
    var notifTitle = games[randomItem].name;
    var notifBody = 'Created by '+games[randomItem].author+'.';
    var notifImg = 'data/img/'+games[randomItem].slug+'.jpg';
    var options = {
        body: notifBody,
        icon: notifImg
    }
    var notif = new Notification(notifTitle, options);
    setTimeout(randomNotification, 30000);
}

The service worker

The last file we will quickly look at is the service worker: sw.js — it first imports data from the games.js file:

self.importScripts('data/games.js');

Next, it creates a list of all the files to be cached, both from the app shell and the content:

var cacheName = 'js13kPWA-v1';
var appShellFiles = [
  '/pwa-examples/js13kpwa/',
  '/pwa-examples/js13kpwa/index.html',
  '/pwa-examples/js13kpwa/app.js',
  '/pwa-examples/js13kpwa/style.css',
  '/pwa-examples/js13kpwa/fonts/graduate.eot',
  '/pwa-examples/js13kpwa/fonts/graduate.ttf',
  '/pwa-examples/js13kpwa/fonts/graduate.woff',
  '/pwa-examples/js13kpwa/favicon.ico',
  '/pwa-examples/js13kpwa/img/js13kgames.png',
  '/pwa-examples/js13kpwa/img/bg.png',
  '/pwa-examples/js13kpwa/icons/icon-32.png',
  '/pwa-examples/js13kpwa/icons/icon-64.png',
  '/pwa-examples/js13kpwa/icons/icon-96.png',
  '/pwa-examples/js13kpwa/icons/icon-128.png',
  '/pwa-examples/js13kpwa/icons/icon-168.png',
  '/pwa-examples/js13kpwa/icons/icon-192.png',
  '/pwa-examples/js13kpwa/icons/icon-256.png',
  '/pwa-examples/js13kpwa/icons/icon-512.png'
];
var gamesImages = [];
for(var i=0; i<games.length; i++) {
  gamesImages.push('data/img/'+games[i].slug+'.jpg');
}
var contentToCache = appShellFiles.concat(gamesImages);

The next block installs the service worker, which then actually caches all the files contained in the above list:

self.addEventListener('install', function(e) {
  console.log('[Service Worker] Install');
  e.waitUntil(
    caches.open(cacheName).then(function(cache) {
      console.log('[Service Worker] Caching all: app shell and content');
      return cache.addAll(contentToCache);
    })
  );
});

Last of all, the service worker fetches content from the cache if it is available there, providing offline functionality:

self.addEventListener('fetch', function(e) {
  e.respondWith(
    caches.match(e.request).then(function(r) {
      console.log('[Service Worker] Fetching resource: '+e.request.url);
      return r || fetch(e.request).then(function(response) {
        return caches.open(cacheName).then(function(cache) {
          console.log('[Service Worker] Caching new resource: '+e.request.url);
          cache.put(e.request, response.clone());
          return response;
        });
      });
    })
  );
});

The JavaScript data

The games data is present in the data folder in a form of a JavaScript object (games.js):

var games = [
    {
        slug: 'lost-in-cyberspace',
        name: 'Lost in Cyberspace',
        author: 'Zosia and Bartek',
        twitter: 'bartaz',
        website: '',
        github: 'github.com/bartaz/lost-in-cyberspace'
    },
    {
        slug: 'vernissage',
        name: 'Vernissage',
        author: 'Platane',
        twitter: 'platane_',
        website: 'github.com/Platane',
        github: 'github.com/Platane/js13k-2017'
    },
// ...
    {
        slug: 'emma-3d',
        name: 'Emma-3D',
        author: 'Prateek Roushan',
        twitter: '',
        website: '',
        github: 'github.com/coderprateek/Emma-3D'
    }
];

Every entry has its own image in the data/img folder. This is our content, loaded into the content section with JavaScript.

Next up

In the next article we will look in more detail at how the app shell and the content are cached for offline use with the help from the service worker.