PWA en SAPUI5 – Tu aplicación en local de manera fácil

En esta entrada vamos a crear una aplicación, pero para ser prácticos no utilizaremos nada extra a nuestra aplicación SAPUI5, esto quiere decir que podremos tener programar ni en Android, ni en iOS ni tan siquiera utilizar cosas como cordova.

Desarrollo de una PWA (Progressive Web App) - Daniel Frías Ruiz

En el título ya ho she hecho spoiler pero… qué es eso de PWA?

Este tipo de aplicaciones, llamadas Aplicaciones Web Progresivas son aplicaciones basadas en tecnologías web que se aprovechan del navegador para que funcione como servidor. Esto nos lleva a que los navegadores modernos incluyen «service workers» y contenedores de memoria que podemos usar como nuestros backends.

Aunque en este post veremos una introducción muy básica, añadir esta capa nos permite por ejemple crear aplicaciones web que funcionen tanto online como offline o evitar la carga de librerias de SAP cada vez que accedemos a las aplicaciones.

Convirtiendo nuestra webapp en app

El primer paso es indicar al navegador que nuestra app tiene «algo especial» es decir, se la puede considerar una aplicación.

Para este ejemplo partiremos de la clásica app creada con el template de SAP.

El primer paso es modificar nuestro manifest.json para añadir la definición de la app. Añadiremos a al incio del fichero los datos que utilizara en navegador:

    "_version": "1.32.0",

    "short_name": "WebAppBeta",
	"name": "Web App Beta",
	"icons": [
		{
		  "src": "/icons/icon-192x192.png",
		  "type": "image/png",
		  "sizes": "192x192"
		},
		{
		  "src": "/icons/icon-512x512.png",
		  "type": "image/png",
		  "sizes": "512x512"
		}
	],
	"start_url": "/index.html",
	"background_color": "#3367D6",
	"theme_color": "#3367D6",
	"display": "standalone",

Lo que hacemos es darle un nombre a la APP, añadir iconos (como cualquier app movil o de escritorio), el fichero de arranque (en nuestro caso el index.html) y la configuración de display, para que se ejecute de manera aislada.

Este será el resultado:

El siguiente paso es indicarle al navegador que existe esta configuración ya que al cargar una aplicación sapui5 el fichero manifest no se carga en el navegador, sino que es la propia aplicación el que lo utiliza (ojo esto no quiere decir que si sacamos la traza no cargue el fichero 😉 ).

En el fichero index.html de la aplicación añadiremos las siguientes cabecera:

        <!-- PWA manifest -->
	<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="OpenUI5 TODO PWA">
	<link rel="apple-touch-icon" href="icons/icon-152x152.png">
	<!-- Windows related -->
	<meta name="msapplication-TileImage" content="icons/icon-144x144.png">
	<meta name="msapplication-TileColor" content="#386CC1">

Este es el resultado:

Una vez añadidos los ficheros podemos arrancar nuestra aplicación con el comando:

npm run start-noflp

Utilizamos el comando noflp ya que no queremos embeber la app en el launchpad. Este será el resultado:

Vaya… no hay nada distinto, bueno lo primero era esta la idea, que la app saliese igual, pero si miramos bien el navegador veremos un cambio:

Con este icono pódemelos instalar nuestra app, al pulsar el botón no instalaría la app, pero antes nos quedan algunos pasos por realizar. Vamos a crear el código necesario para activar el service worker y indicar al navegador como instalar la aplicación.

Añadiendo el «Backend»

Para indicarle al navegador que tenemos un fichero con la lógica del service worker vamos a crear un fichero llamado register-worker.js. A continuation voy a ir por bloques explicado cada parte del fichero:

Primero crearemos un par de variables, una con un id de cache, para indicar al navegador que si hay un cambio de id de cache refresque los ficheros que guardara en la instalación. Esto es, por ejemplo, para actualizar las librerías o los ficheros propios de la app. la segunda variable sera un array de ficheros que la app tendrá que almacenar, es decir, las librerías y ficheros de nuestra app:

var CACHE_NAME = 'my-site-cache-v4';
var urlsToCache = [
  //'./resources/',
  '/resources/sap-ui-core.js',
  '/index.html',
  '/manifest.json',
  '/Component.js',
  '/resources/sap/ui/core/library-preload.js',
  '/resources/sap/ui/core/themes/sap_belize_plus/library.css',
  '/resources/sap/ui/core/themes/base/fonts/SAP-icons.woff2',
  '/resources/sap/m/library-preload.js',
  '/resources/sap/m/themes/sap_belize_plus/library.css'
];

Lo siguiente es añadir los eventos que tendrán lugar en nuestra app. Es decir el evento de instalación, activación y interacción (fetch):

Vamos a por la instalación:

self.addEventListener('install', function(event) {

    const cdnBase = 'https://openui5.hana.ondemand.com/resources/';
	urlsToCache = urlsToCache.concat([
		`${cdnBase}sap-ui-core.js`,
		`${cdnBase}sap/ui/core/library-preload.js`,
		`${cdnBase}sap/ui/core/themes/sap_belize_plus/library.css`,
		`${cdnBase}sap/ui/core/themes/base/fonts/SAP-icons.woff2`,
		`${cdnBase}sap/m/library-preload.js`,
		`${cdnBase}sap/m/themes/sap_belize_plus/library.css`
	]);




  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        return cache.addAll(urlsToCache);
      })
  );
});

Este primer bloque tiene también un approach alternativo par ala carga de librerías sapui5, lo que hacemos es indicar que ficheros del cache deberemos guardar en la memoria reservado a nuestra aplicación mediante la instrucción cache.addAll.

El siguiente bloque es la activación de la APP, es decir, se limpiará la cache de la app en caso que se detecte una cache nueva, es decir, esto se activara cada vez que accedamos a la url:

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

El último bloque se encarga de la recuperación de información en el caso que estemos offline:

  // During runtime, get files from cache or -> fetch, then save to cache
self.addEventListener('fetch', function (event) {
    
	// only process GET requests
	if (event.request.method === 'GET') {
		event.respondWith(
			caches.match(event.request).then(function (response) {
				if (response) {
					return response; // There is a cached version of the resource already
				}
	
				let requestCopy = event.request.clone();
				return fetch(requestCopy).then(function (response) {
					// opaque responses cannot be examined, they will just error
					if (response.type === 'opaque') {
						// don't cache opaque response, you cannot validate it's status/success
						return response;
					// response.ok => response.status == 2xx ? true : false;
					} else if (!response.ok) {
						console.error(response.statusText);
					} else {
						return caches.open(CACHE_NAME).then(function(cache) {
							cache.put(event.request, response.clone());
							return response;
						// if the response fails to cache, catch the error
						}).catch(function(error) {
							console.error(error);
							return error;
						});
					}
				}).catch(function(error) {
					// fetch will fail if server cannot be reached,
					// this means that either the client or server is offline
					console.error(error);
					return caches.match('offline-404.html');
				});
			})
		);
	}

En este caso lo limitaremos solo a métodos GET, aunque podemos hacer también post y encaso de estar sin conexión mantener los datos hasta que volvamos a tener conexión.

Ya tenemos nuestro fichero de backend apunto, este es el resultado final:

var CACHE_NAME = 'my-site-cache-v4';
var urlsToCache = [
  //'./resources/',
  '/resources/sap-ui-core.js',
  '/index.html',
  '/manifest.json',
  '/Component.js',
  '/resources/sap/ui/core/library-preload.js',
  '/resources/sap/ui/core/themes/sap_belize_plus/library.css',
  '/resources/sap/ui/core/themes/base/fonts/SAP-icons.woff2',
  '/resources/sap/m/library-preload.js',
  '/resources/sap/m/themes/sap_belize_plus/library.css'
];
var resourcesToCache = [];

self.addEventListener('install', function(event) {

    const cdnBase = 'https://openui5.hana.ondemand.com/resources/';
	urlsToCache = urlsToCache.concat([
		`${cdnBase}sap-ui-core.js`,
		`${cdnBase}sap/ui/core/library-preload.js`,
		`${cdnBase}sap/ui/core/themes/sap_belize_plus/library.css`,
		`${cdnBase}sap/ui/core/themes/base/fonts/SAP-icons.woff2`,
		`${cdnBase}sap/m/library-preload.js`,
		`${cdnBase}sap/m/themes/sap_belize_plus/library.css`
	]);

    console.log(urlsToCache);
    console.log(resourcesToCache);

  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

// Delete obsolete caches during activate
self.addEventListener('activate', function (event) {
	event.waitUntil(
		caches.keys().then(function (keyList) {
			return Promise.all(keyList.map(function (key) {
				if (key !== CACHE_NAME) {
					return caches.delete(key);
				}
			}));
		})
	);
});

  // During runtime, get files from cache or -> fetch, then save to cache
self.addEventListener('fetch', function (event) {
    
	// only process GET requests
	if (event.request.method === 'GET') {
		event.respondWith(
			caches.match(event.request).then(function (response) {
				if (response) {
					return response; // There is a cached version of the resource already
				}
	
				let requestCopy = event.request.clone();
				return fetch(requestCopy).then(function (response) {
					// opaque responses cannot be examined, they will just error
					if (response.type === 'opaque') {
						// don't cache opaque response, you cannot validate it's status/success
						return response;
					// response.ok => response.status == 2xx ? true : false;
					} else if (!response.ok) {
						console.error(response.statusText);
					} else {
						return caches.open(CACHE_NAME).then(function(cache) {
							cache.put(event.request, response.clone());
							return response;
						// if the response fails to cache, catch the error
						}).catch(function(error) {
							console.error(error);
							return error;
						});
					}
				}).catch(function(error) {
					// fetch will fail if server cannot be reached,
					// this means that either the client or server is offline
					console.error(error);
					return caches.match('offline-404.html');
				});
			})
		);
	}
    
});

Por último añadiremos el chivato en el fichero «index.html», el código lo añadiremos después de los scrips de carga de SAPUI5:

<script src="register-worker.js"></script>    
<script>
        if ('serviceWorker' in navigator) {
            window.addEventListener('load', function () {
                navigator.serviceWorker.register('register-worker.js').then(function (registration) {
                    // Registration was successful
                    console.log('Registered!');
                }, function (err) {
                    // registration failed :(
                    console.log('ServiceWorker registration failed: ', err);
                }).catch(function (err) {
                    console.log(err);
                });
            });
        } else {
            console.log('service worker is not supported');
        }
    </script>

Este será el resultado final del fichero index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <!-- PWA manifest -->
    <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="OpenUI5 TODO PWA">
    <link rel="apple-touch-icon" href="icons/icon-152x152.png">
    <!-- Windows related -->
    <meta name="msapplication-TileImage" content="icons/icon-144x144.png">
    <meta name="msapplication-TileColor" content="#386CC1">

    <title>App Title</title>
    <style>
        html,
        body,
        body>div,
        #container,
        #container-uiarea {
            height: 100%;
        }
    </style>
    <script id="sap-ui-bootstrap" src="resources/sap-ui-core.js" data-sap-ui-theme="sap_fiori_3"
        data-sap-ui-resourceroots='{
            "ecastella.pwa.beta.pwabeta": "./"
        }' data-sap-ui-oninit="module:sap/ui/core/ComponentSupport" data-sap-ui-compatVersion="edge"
        data-sap-ui-async="true" data-sap-ui-frameOptions="trusted"></script>

    <script src="register-worker.js"></script>

    <script>
        if ('serviceWorker' in navigator) {
            window.addEventListener('load', function () {
                navigator.serviceWorker.register('register-worker.js').then(function (registration) {
                    // Registration was successful
                    console.log('Registered!');
                }, function (err) {
                    // registration failed :(
                    console.log('ServiceWorker registration failed: ', err);
                }).catch(function (err) {
                    console.log(err);
                });
            });
        } else {
            console.log('service worker is not supported');
        }
    </script>

</head>

<body class="sapUiBody sapUiSizeCompact" id="content">
    <div data-sap-ui-component data-name="ecastella.pwa.beta.pwabeta" data-id="container"
        data-settings='{"id" : "ecastella.pwa.beta.pwabeta"}' data-handle-validation="true"></div>
</body>

</html>

El resultado

Pues bien, ahora que tenemos nuestro proyecto apunto, volvemos a arrancar la aplicación con el comando «npm run start-noflp».

En la consola veremos que la aplicación ha sigo reconocida:

Con el botón que aparece en la barra del navegador, la podemos instalar:

Una vez instalado la podemos abrir directamente en una ventada aislada:

Tambien veremos, por ejemplo en windows nuestro proyecto convertido en «aplicación»

Si simulamos modo offline, por ejemplo parando el servició de nuestro BAS, veremos que la aplicación sigue activa:

Y hasta aquí el tutorial. Para esta entrada me he inspirado en este repositorio oficial, aunque como veréis este repositorio no incluye toda la parte de despliegue ni la estructura estándar de una aplicación SAPUI5:

https://github.com/SAP-samples/openui5-pwa-sample


En esta entrada hemos visto una nueva manera de de crear una aplicación híbrida para que nos de capacidades extra como offline o notificaciones push de una manera fácil.

Como siempre suscribete, dale a la campanita de notificaciones y comparte en redes para estar a la última. Vota Like / Dislike para aportar feedback.

4 respuestas a «PWA en SAPUI5 – Tu aplicación en local de manera fácil»

  1. Cuando escribí el artículo medite sobre lo mismo, porque no hacer un WebView en Android/iOS?

    Pero luego le di una vuelta y por ejemplo una utilidad es economizar la carga de librerias o datos maestros. O por si hay riesgo de pérdida de conexión

    También puede ser útil si el presupuesto, las ganas o el tiempo no permite hacer las 3 apps, me refiero a la app en SAPUI5 + android y iOS

    Gracias por el comentario Antonio!

  2. Hi, the button to install the app is not showing in my case. I’m using Google Chrome. Would you mind sharing your app?

Responder a Enric Castella Cancelar la respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.