📜 ⬆️ ⬇️

Air Berlin: Progressive Web App Implementation

Hi, Habr! At the anniversary conference on May 18 in California for Google I / O developers there was a lot of things. Serious things for Android, monumental changes and integration in Firebase products, and just a lot of announcements and cool technologies. But something else we have not yet discussed. We are talking about Progressive Web App (modern web applications) - sites that are written as if they were modern mobile applications: convenient, simple, intuitive and comfortable for use on a touch display.

Therefore, in the next two months, we are going to not only publish articles on the topic of PWA, but also to hold a thematic online conference on October 11 - Progressive Web Apps Day . In the meantime, we bring to your attention a real case of using PWA from AirBerlin.



The first airline in the world, airberlin, developed such a website, and Hans Schwager, head of mobile development and innovation, shared his experience with conference visitors: “The popularity and importance of smart mobile Internet services in the airline industry will continue to grow, and we know It is important for our passengers, regardless of where they are located, to easily and quickly verify information and receive boarding passes directly from their mobile devices. That is why we have attracted the prospective development department to create Progressive Web App, a hybrid that combines the best aspects of mobile applications and Internet sites. ”
')
The technology of modern web applications allows airberlin passengers to get access to boarding passes and information about their journey at any time, even if the last time he visited our website via the hotel’s Wi-Fi or at home, and at the airport suddenly remained without communication. This allows us to provide our customers with a very simple and intuitive service, to increase convenience and to bring the future of mobile development to a small step.

What is a “modern web application”?


Simply put, this is a website that looks and feels like a mobile app. After the first login, it is available (partially or completely) offline thanks to caching, supports Chrome and Firefox browsers from version 40, as well as Opera's current builds. Such an “application”, unlike the usual mobile version of the site, works well on a slow Internet connection (for example, in an overloaded free Wi-Fi at the airport), spends a minimum of traffic, can be added to the desktop as an ordinary icon, has access to the notification system of the smartphone and not demanding on the resources of the smartphone.

How we created PWA airberlin


Tell Marian Pöschmann and Axel Michel

Core technology


Under the hood PWA hides simple things, the main task is to collect everything in the right way.

Web Components
The idea is simple: everything except the interface of a progressive web application is a component. We used Polymer 1.0, created separate components for the slider, which include various forms, parts and elements that form the result: a “virtual ticket” that the user will see.

Custom Events
For the interaction between the components, we wrote the main script, which centrally manages asynchronous requests, history, data used in the application, their caching and provision.

HistoryAPI
Our progressive application, in fact, consists of one page. Since the service worker or the caching does not know how to work with the hash in urls, we decided to use GETs to distinguish between the statuses and various “screens” of the application. In principle, the solution is normal, but it has some problems with offline work. For the future, do not use only the values; send immediately the parameter-value pairs if you want your requests to be processed normally. Well, or recreate these requests, based on the information that you encode in the URL (this will be separately discussed a little further in the code examples).

Service worker
As we have already noted, our application, in fact, is a one-page site with one and only use case. To add to such a project a service worker for offline work is easier than ever. The interface itself is cached during the initial “installation”, data and additional files are uploaded on the first request. More problems were the removal of the right data at the right time. Also, through the service worker, we integrated push notifications for the implementation of check-in in two tapas.

WebSQL
In addition to the offline handler, we wanted to improve the user experience of working offline without using the network, using localForage: a technology that immediately includes IndexDB, WebSQL and / or localStorage. All interaction between the server and the client is described by JSON, which greatly simplifies further development.

VanillaJS
Used for everything else. Basic DOM selectors, some asynchronous requests, in general, everything that we did not want to implement through third-party libraries. The only case of connecting ready-made js is to use a moment that fully handles the calculation of different time zones and dates: after all, some flights may send you to the past / future. For the rest. Some basic

Manifest and Meta data
These things are needed so that the user can add the application to his home screen / desktop. Unfortunately, iOS will remain from Android in terms of supporting the offline capabilities of the application (it simply does not exist), we decided to provide users with the correct icon, title and color scheme on Android and iOS.

How we did it


Quick check-in and the ability to get on a plane without installing anything separate on the phone is cool, so we wanted the app to be fast. Therefore, we load everything except the basic css and some placeholders dynamically, without blocking the DOM and not waiting for loading.

Basic HTML structure:

<section class="page" id="dashboard"> <header> <slider-element name="dashboard" display="all"></slider-element> </header> <div class="contents"> <ul class="collection"> <li><a href="#flightdetails">Journey details</a></li> <li><a href="#explore">Explore destination</a></li> <li><checkin-element></checkin-element></li> </ul> </div> </section> <section class="page" id="flightdetails"> <header> <slider-element name="flightdetail" display="activeFlight"></slider-element> </header> <flightdetails-element></flightdetails-element> </section> <section class="page" id="explore"> <place-element></place-element> </section> 

Starting JavaScript:

 (function() { var raf = window.RequestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; // defer loading of all app relevant javascript // and non criticial CSS function deferLoad() { // JS var element = document.createElement("script"), l = document.createElement('link'); element.src = "javascript/app.js"; document.body.appendChild(element); // CSS l.rel = 'stylesheet'; l.href = 'css/main.css'; document.getElementsByTagName('head')[0].appendChild(l); } if (raf) { raf(deferLoad); } else if (window.addEventListener) { window.addEventListener("load", deferLoad, false); } else if (window.attachEvent) { window.attachEvent("onload", deferLoad); } else { window.onload = deferLoad; } })(); 

How it works?


Initially, the only thing that is sent to the user's device is the skeleton of the application. Basic markup, menu bar, colored plugs on the places where there should be pictures, short text in that part of the page that the user sees.

The main CSS, most of our script and third-party libraries (polymer, moment, local forage) are loaded in the background, after which the main site connects various elements through polymer. Paul Lewis wrote a great article on this topic: aerotwist.com/blog/polymer-for-the-performance-obsessed

Compared with the usual mobile page (m.airberlin), the total download time is about the same - one and a half seconds for our approach and two and a half for the classic one, when using a 3G connection. However, drawing the content and the site itself begins much earlier: half a second after the application opens. The mobile site has a terrible 1.2 seconds before the first elements appear. By the time the PWA is already open and ready to go. We strive to make the download even faster, but the applied technologies have minimized the load time and saved the users from having to watch elements jump across the page in the process of loading styles and images.

Little tricks


Another trick that helps to reduce waiting time and improve user experience, we have peeped from Facebook. The point is this: modern devices differ in display resolution, and a very unremarkable range of indicators - you can find either a six-inch shovel with 720p, and a 5-inch devas with a display of 2560x1440 or even stumble upon 4k2k in a mobile device. For each of the popular solutions, its own background image was prepared, and in order for the user not to look at the single-color substrate, we applied a very small image (60x40 pixels) and a Gaussian blur. As a result, the user sees everything almost as it should, and as soon as the image with the required resolution is loaded in the background, we replace the blurred low-res with the actual image from the cache.

Our first handler consisted of just a few lines of code. After its activation, we simply loaded all the static content, and in a blunt way, we stuffed everything into the cache. This approach suited us while we were working with the prototype of the application, in which there was only one “flight”, with one destination, without history, in general without anything that could become outdated or lose relevance. Of course, in reality, PWA requires a little more: display information about the flight, create a boarding pass. And he, in turn, must be destroyed or rejected after the flight, and for any other flights you need to display any additional information: pictures, text, whatever.


Check-in for the flight has become easier and faster: they added a flight, a reservation number, pressed a button and got a boarding pass. Easier does not happen.

Filling it all up in a cache or storing all the information about all directions at once is somehow not very progressive, so we just destroy everything 48 hours after the landing of the aircraft. Since we use WebSQL and local storage, we have to delete data twice. Now this code is responsible for this:

Code Fragment app.js:

 function _isEmpty = function(obj) { if ('undefined' !== Object.keys) { return (0 === Object.keys(obj).length); } for(var prop in obj) { if(obj.hasOwnProperty(prop)) { return false; } } return true; }; // called whenever a checkin is requested to be displayed function checkCheckinStatus( checkinID ) { var tS = Math.floor(now.getTime() / 1000), removeCheckinFromApp = function(cid) { // remove from data delete app.data[cid]; // update local cache localforage.setItem('flightData',app.data); // trigger event for updating UI elements var event = new CustomEvent( 'updatedData', {detail: {modified: cid}} ); document.dispatchEvent(event); }; // no data or no checkin data? - return if(_isEmpty(app.data) || !app.data[checkinID]) { return false; } // remove only in case arrival time is min. 48 hours in past if((tS - app.data[checkinID].ticket.arrivalTimestamp) < (60 * 60 * 48) ) { return false;} if ('serviceWorker' in navigator) { // delete cache of flight in service worker... app.sendMessage( { command: 'deleteCheckin', keyID: checkinID } ).then(function(data) { // remove the checkin from app data... removeCheckinFromApp(checkinID); }).catch(e) { // could not remove checkin from service worker }; } else { removeCheckinFromApp(checkinID); } } // send data to the service worker app.sendMessage = function(message) { return new Promise(function(resolve, reject) { var messageChannel = new MessageChannel(); // the onmessage handler messageChannel.port1.onmessage = function(event) { if (event.data.error) { reject(event.data.error); } else { resolve(event.data); } }; if(!navigator.serviceWorker.controller){ return; } // This sends the message data and port to the service worker. // The service worker can use the port to reply via postMessage(), which // will he onmessage handler on messageChannel.port1. navigator.serviceWorker .controller.postMessage(message,[messageChannel.port2]); }); } 

Code Fragment service-worker.js:

 var cacheName = 'v1', checkinDataRegex = /applicable\?pnr=([a-zA-Z0-9]+)&lastname=([a-zA-Z]+)/ ticketRegex = /image\/pnr\/([a-zA-Z0-9]+)\/lastname\/([a-zA-Z]+)\/ticket\/([0-9]+)/; self.addEventListener('fetch', function(event) { var request = event.request, matchCheckin = checkinDataRegex.exec(request.url); if (matchCheckin) { // Use regex capturing to grab only the bit of the URL // that we care about (in this case the checkinID) var cacheRequest = new Request(match[1]); event.respondWith( caches.match(cacheRequest).then(function(response) { return response || fetch(request).then(function(response) { caches.open(cacheName).then(function(cache) { cache.put(cacheRequest, response); }) return response; }); }) ); } if (ticketRegex) { // disable the image (by replacing it) [...] } [...] }); // communication between the service worker and the app.js self.addEventListener("message", function(event) { var data = event.data; switch(data.command) { case 'deleteCheckin': // open current cache caches.open(cacheName).then(function(cache) { // remove the flight data (JSON) cache.delete(data.checkinID).then(function(success) { event.ports[0].postMessage({ error: success ? null : 'Item was not found in the cache.' }); )}; }) break; [...] } }); 

Work with cache


Some elements of a polymer can run app.js, inside which a special method checks whether the information stored in the cache is relevant or not. If the data is outdated, the handler receives the “burn” command, erases the internal cache and deletes the data from the local storage, and then informs all interested polymer elements that the data has changed.

The above code also contains a fetch handler. Since the URL the handler interacts with may change (for example, additional GET parameters for firebase analytics will appear), we wrote a regular expression that simply pulls out the required set of parameters and places it in the web application cache. Thus, we are not tied to a URL and can easily get data from it that is easier to process and store.

Something else


The most effective way to drastically reduce the load time on mobile devices was to reduce the number of individual files and items and use lazy loading through the handler for everything else. The first step is to pack all the web components and send them to the mobile device, and the handler waits for everything you need to be loaded. At the same time, we load in the background CSS, scripts, images and put them in the cache. Further, the country is assembled from the “basic” design and returns with details and elaborate design as resources are loaded onto the user device.


For example, the background is loaded with a cute background or additional information about the destination. And the main functions will work without these beauties, even if you are connected via a barely live edge connection.

Unfortunately, the "make your own hurt" magic button has not yet been invented, so just taking a window with the button "add our website to your home screen" will not work through any API. We'll have to use ingenuity and a set of crutches: that is, write your message and a dialog box in which we will tell the user how to add PWA to the desktop. In our case, a check was added to the invention of the bicycle for when the dialogue was highlighted. All information becomes available offline only after check-in, so it is after it that we suggest the user create a shortcut. Actually, everything is simple:

 var deferredPromptEvent; window.addEventListener('beforeinstallprompt', function(e) { e.preventDefault(); deferredPromptEvent = e; return false; }); // and in the moment your condition is fulfilled // check if the prompt had been triggered if(deferredPromptEvent !== undefined && deferredPromptEvent) { // show message deferredPromptEvent.prompt(); // do something on the user choice deferredPromptEvent.userChoice.then(function(choiceResult) { if(choiceResult.outcome != 'dismissed') { } // finally remove it deferredPromptEvent = null; }); } 

In our case, the message appears after the user closes the pop-up with a notification that his ticket has been saved.

Polymer elements are atomic (now atomicity continues to be understood as indivisibility, although we know that atoms are still dividing). So, from atomicity it follows that each element of the polymer will carry inline CSS and javascript. Of course, you can add external styles / scripts, but the inline implementation loads faster and works more reliably. Of course, it has its drawback - it is more difficult to maintain such code, especially CSS. Our solution is to use Grunt and inline inline it, well, and use SCSS as a precompiler for CSS. Each element along with normalized CSS gets its own SCSS file with basic parameters (functions and variables). Grunt takes both the generated CSS and injects it as an inline style, and then binds it to the element. Of course, the same will work with gulp or LESS.

What we learned in the process of creating PWA


It is possible to facilitate the work and speed up the loading in different ways, and one of the most interesting and effective is to use an array of objects as a database for polymer objects, but there is one small difficulty, especially if you work with nested elements. In our case, the application had a slider that contains boarding passes. Since we transferred all the drawing of coupons from the server to the client, the drawing data that the client receives from the server weigh very little: a little JSON, a couple of binaries ... All this improves the load time, makes the server easier, especially when it comes to modern gadgets. On older devices, everything is not so clear. Sometimes it is easier to give pre-rendered content than the data to build it, however, the first launch of PWA will take longer. All of this is the object of studying specific cases in specific applications, the goal of A / B testing and evaluating the convenience of a particular approach to solving a fixed problem.

The second thing, which greatly simplifies life, but can spoil your blood - handler. Yes, it increases the speed of loading and rendering, simplifies offline work, removes a large number of requests to the server, which even more strongly affects the feeling of working with an unstable mobile connection. At the same time, this approach raises the question “what to cache” and “how to cache”. If everything is more or less clear with Android, there will be no problems on modern devices, but iOS still has problems due to ... let's say, features of a closed architecture and platform. You can add a shortcut to the desktop, and (due to certain browser caching mechanics) it may even work offline or with a very bad connection ... but at the same time it can always show a dinosaur.



This is all for today, but we will return to the PWA topic in the near future.

Source: https://habr.com/ru/post/308498/


All Articles