Programming

Florencia Soldo • 04 JUL 2024

Exploring the potential of Progressive Web Applications

post cover picture

It's a well-known fact that mobile applications dominate the digital landscape, which is why Progressive Web Applications (PWAs) have emerged as a revolutionary alternative to traditional apps. Blending the best of both web and app worlds, they provide users with a seamless interface experience while simplifying development for creators.

Simply put, it's an application that runs directly within a web browser instead of being installed on a mobile device's operating system. At their core, PWAs utilize modern web capabilities to deliver native app-like experiences.

In this blog post, we’ll explore a particular use case where we encountered the need to implement a PWA. 

 

Functionality and benefits of Progressive Web Apps (PWAs)

While native apps and PWAs may look similar in appearance, PWAs differ in that they require manifest files, service workers, and other progressive enhancement techniques to provide features such as offline functionality, push notifications, and seamless performance across devices. 

This JSON document contains details like the app’s name and URL, defining how the app appears to users, including native app-like elements such as icons.


{
  "short_name": "Reading App",
  "name": "Pilio Reading App",
  "id": "/",
  "icons": [
    {
      "src": "pilio.ico",
      "sizes": "144x144",
      "type": "image/png"
    }
  ],
  "start_url": "/mobile/sessions/new",
  "background_color": "#000000",
  "display": "standalone",
  "scope": "/",
  "theme_color": "#000000"
}

Operating independently from the main browser thread responsible for handling the DOM interface, service workers manage network requests and browser cache storage. Essentially, they enable PWAs to provide seamless offline experiences by intercepting requests and enabling background syncing.


// import script into the worker's scope
importScripts("https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js");

const {CacheFirst, NetworkFirst} = workbox.strategies;
const {registerRoute} = workbox.routing;

registerRoute(
  ({url}) => url.pathname.startsWith('/mobile') && !url.pathname.startsWith('/mobile/sessions'),
  new CacheFirst({ cacheName: 'documents' })
)

registerRoute(
  ({url}) => url.pathname.startsWith('/rails/active_storage'),
  new CacheFirst({ cacheName: 'images' })
)

registerRoute(
  ({request}) => request.destination === "script" || request.destination === "style",
  new CacheFirst({ cacheName: 'assets-styles-and-scripts' })
)

registerRoute(
  ({request}) => request.destination === "image",
  new CacheFirst({ cacheName: 'assets-images' })
)

To explore the benefits of implementing PWAs, we'll distinguish between the advantages for users and those for developers.
 

The user experience advantage:

For end-users, the appeal of PWAs lies in their seamless integration into everyday browsing activities. Instead of inconvenient installations and updates from app stores, PWAs can be added to the home screen with a single tap, mirroring native apps in appearance and functionality. Whether online or offline, PWAs deliver consistent performance, ensuring that users can access content and functionality without interruption.
 

Streamlined development process:

From a developer's perspective, PWAs offer advantages too. By leveraging web technologies such as HTML, CSS, and JavaScript, developers can build and deploy PWAs rapidly, with the added benefit of code reuse and cross-platform compatibility. Moreover, the progressive enhancement approach allows developers to cater to a diverse range of devices and network conditions, ensuring a consistent experience for all users.

 

Integrating PWAs: A Step-by-Step Guide

Migrating you web application to a PWA may seem overwhelming at first, but the process is relatively straightforward with the right approach. Here's a step-by-step guide based on our own experience:

1. Understand core concepts:

Familiarize yourself with the core concepts of PWAs, like service workers, manifest files, and offline caching strategies.

2. Project setup:

Create a new project or integrate PWA capabilities into an existing web application. Ensure your project structure aligns with PWA best practices, including HTTPS deployment and responsive design.

3. Service worker integration:

Service workers are the backbone of PWAs, enabling features such as offline caching and background sync. Integrate service workers into your application to provide seamless offline functionality.

4. Manifest file configuration:

Craft a manifest file that defines your PWA's metadata and appearance. This includes specifying icons, display modes, and other essential properties to ensure a native-like experience.

5. Testing:

Thoroughly test your PWA across different devices and network conditions to identify and address any performance issues. Optimize your PWA for speed, accessibility, and user experience.

6. Deployment:

Once your PWA is ready for deployment, ensure that it meets the necessary requirements for installation and distribution. Publish your PWA on the web and promote it to your audience.

 

Case study: Pilio's experience with PWAs

To illustrate the effectiveness of PWAs in real-world scenarios, let's examine the case of Pilio, a web app where users—primarily companies—upload what are known as “readings”. These readings represent the consumption of various energy sources (such as water, gas, and electricity) on the platform. They are associated with a meter that tracks consumption, which in turn belongs to a specific building, whether it's a factory or a business building. Using this data as input, the platform calculates the carbon emissions impact.

The client initially sought an application that embraced the product's main functionalities, essentially a CRUD (Create Read Update Delete) functionality for readings, and that could operate seamlessly both online and offline, for situations when connectivity is not available.

Since we were using Rails, for the PWA we could leverage much of the existing backend codebase and it did not entail any extra cost to develop it. However, we needed to incorporate some logic in JavaScript for the frontend.

After evaluating other alternatives, such as Turbo Native and React Native, we opted for a PWA due to its efficiency and cost-effectiveness.


Pilio as a PWA: Implementation and Deployment

Pilio adopted a PWA approach, which enhanced the user experience by providing seamless offline access to tasks, schedules, and reminders. By integrating service workers and offline caching, Pilio ensured users could remain productive without an internet connection, increasing engagement and satisfaction.

Here’s how we helped Pilio implement its PWA:
 

Workbox integration:

We utilized Workbox, a Google library, to simplify routing and cache management within the service worker. This allowed us to specify which files should be stored in the cache, ensuring essential resources were available offline.

In the Service Worker code, we can see a clear example of how we register specific routes in the service worker and configure how we want the caching to behave independently. For instance, if not cached, routes starting with rails/active_storage are created with the parameter name, and the corresponding URL is saved.

In a web application, we constantly fetch resources from the server via requests. In a PWA, the service worker intercepts these requests, allowing us to handle them under certain conditions, like internet availability or specific request characteristics. For example, we can respond to a request with a cached image when there's no internet connection. However, to achieve this, we should have previously cached the image while online.

Up to this point, we could say we have an application that can function like a native app as we handle offline capabilities. However, we need to provide user interaction for managing records while offline.
 

Offline Data Management:

That’s when we introduced Pilio to IndexedDB, a JavaScript API that allows the manipulation of JSON objects in the browser. Unlike ActiveRecord, which runs on the server, IndexedDB runs in the browser. We chose IndexedDB because it's widely used in similar cases and has good documentation for manipulation. LocalStorage was also considered, but it was limiting for the type of data and records we needed to store.

In a web application, manipulating data involves interacting with a database hosted somewhere. When we create a record in Ruby, that object is saved using ActiveRecord. When offline, we need to store this same data locally so we can later synchronize it with the server and save it to the database. This is exactly what IndexedDB enables us to do.

To summarize the steps to follow:

  1. Check connection.

  2. If there is no connection, do not submit the form and store data in IndexedDB.

  3. Regain connection and persist data in the database.



checkOnlineStatus(event) {
    event.preventDefault();
    event.target.classList.add('disabled');
    let db;
    const DBOpenRequest = window.indexedDB.open("ReadingsAppDatabase", 1);

    DBOpenRequest.onsuccess = (event) => {
      db = DBOpenRequest.result;

      getData();
    }
}

 

Here, I want to show how to interact with IndexedDB briefly. We connect to the database and call the getData() function to fetch the information we need. This function handles what we call object stores, which are like different tables, transactions, and search indexes. 

 


function getData() {
  const transaction = db.transaction(["readings"], "readwrite");
  const transactionMeters = db.transaction(["meters"], "readwrite");
  const objectStore = transaction.objectStore("readings");
  const objectStoreMeters = transactionMeters.objectStore("meters");
  const meterToTupdate = objectStoreMeters.get(meterId);
  const keyRange = IDBKeyRange.bound(parseInt(meterId), parseInt(meterId), false, false);
  const readingsIndex = objectStore.index("meter_id");
  const result = [];
}

 

In the result variable, we store the query we previously made that interests us. We'll use put on the object store to save the new record, which will later be synchronized and persisted in the database.
 


readingsIndex.openCursor(keyRange).onsuccess = function (event) {
  const cursor = event.target.result;
  if (cursor) {
    result.push(cursor.value);
    cursor.continue();
  } else {
    if (result.length == 0) {
      if (window.navigator.onLine) {
        //submit the form
        document.getElementById("new_reading").submit();
      } else {
        //persist in IDB
        objectStore.put({ reading });
      }
      const data = meterToTupdate.result;
      const transactionMeters = db.transaction(["meters"], "readwrite");
      const objectStoreMeters = transactionMeters.objectStore("meters");
      objectStoreMeters.put(data);
    }
  }
}

 

Once we have the data persisted in IndexedDB, we persist it in the database:

 


rsendData(e) {
    e.preventDefault();
    let db;
    const url = e.target.href;
    const DBOpenRequest = window.indexedDB.open("ReadingsAppDatabase", 1);

    DBOpenRequest.onsuccess = (event) => {
      db = DBOpenRequest.result;

      syncNow()
    };

    function syncNow() {
      const meterId = parseInt(document.getElementById("meter_id").value);
      const transaction = db.transaction(["readings"], "readwrite");
      const objectStore = transaction.objectStore("readings");
      const keyRange = IDBKeyRange.bound(meterId, meterId, false, false);
      const readingsIndex = objectStore.index("meter_id");
      const result = [];
      const body = [];

      readingsIndex.openCursor(keyRange).onsuccess = function (event) {
        const cursor = event.target.result;
        if (cursor) {
          result.push(cursor.value);
          cursor.continue();
        } else {
          for (let index = 0; index < result.length; index++) {
            const reading = result[index];
            if (!reading.synched){
              body.push({
                meter_id: meterId, taken_at: reading.taken_at, amount: reading.amount
              })

              objectStore.put({ id: reading.id, meter_id: reading.meter_id, taken_at: reading.taken_at, amount: reading.amount, comment: reading.comment, reading_id: reading.id, synched: true });
            }
          }

          fetch(url, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({ reading: body}),
          });
        }
      };
    };
  };


PWAs impact: scalability and performance

Progressive Web Applications represent a groundbreaking approach to app development, offering a blend of web accessibility and app-like functionality. By embracing PWAs, developers can unlock new user engagement, performance, and scalability possibilities. Whether you're building a new application or enhancing an existing one, PWAs hold the key to delivering compelling experiences in the digital age.

If you want to dive deeper into web and app development and stay up to date with the latest software trends, visit our blog and read our latest articles.

 

Stay updated!

project background