The Progressive Web App Module

PWA header image
Share
by Alex Borsody|Senior Developer

 

The PWA module is an out of the box solution that provides a service worker for caching and offline capabilities. Once the service worker is active, the page loading is faster. It also serves as a knowledge repository for PWA best practices to provide needed meta tags for a perfect Lightbox score.

There is functionality in the module’s service worker that provides unique solutions to Drupal specific behavior, some of these solutions can be applied to apps outside of the Drupal world as well and we will discuss this below. When developing the D8 module, it was decided to mirror the existing functionality of the D7 version as a starting point. In turn, we were able to use the same service worker for D8 and now the two modules can be developed in parallel, with patches for one module easily rolled back into the other. Below, we’ll also discuss some of the functionality provided by the PWA module.

 

Offline Caching

In Workbox, a precache manifest is generated by a command-line tool that scans your directory and adds assets to the precache manifest file (not to be confused with manifest.json). This is impossible in Drupal because the CSS/JS filenames change after compression. Théodore "nod_" Biadala provided the solution in D7 to prepopulate the service worker with known assets, by internally requesting the URLs set in the admin panel and extracting assets out of the DOM. This allows the install event to fetch all CSS/JS and images from these pages to store in the browser Cache API for offline rendering, the complete pages will then be viewable offline even if we never visit them first.

Image
First PWA image

Below we fetch all the assets from the URLs set in the admin panel to inject later into the service worker precache assets array.  In D8 we change our request to use Drupal::httpClient(), which is the updated version of drupal_http_request() in D7 and is basically a wrapper for the PHP Guzzle library.

 

  foreach ($pages as $page) {
      try {
        // URL is validated as internal in ConfigurationForm.php.
        $url = Url::fromUserInput($page, ['absolute' => TRUE])->toString(TRUE);
        $url_string = $url->getGeneratedUrl();
        $response = \Drupal::httpClient()->get($url_string, array('headers' => array('Accept' => 'text/plain')));

 

 We match all assets we need.

 

 // Get all DOM data.
      $dom = new \DOMDocument();
      @$dom->loadHTML($data);

      $xpath = new \DOMXPath($dom);
      foreach ($xpath->query('//script[@src]') as $script) {
        $resources[] = $script->getAttribute('src');
      }
      foreach ($xpath->query('//link[@rel="stylesheet"][@href]') as $stylesheet) {
        $resources[] = $stylesheet->getAttribute('href');
      }
      foreach ($xpath->query('//style[@media="all" or @media="screen"]') as $stylesheets) {
        preg_match_all(
          "#(/(\S*?\.\S*?))(\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)#ie",
          ' ' . $stylesheets->textContent,
          $matches
        );
        $resources = array_merge($resources, $matches[0]);
      }
      foreach ($xpath->query('//img[@src]') as $image) {
        $resources[] = $image->getAttribute('src');
      }
    }

 

Below, you can see the final result in the processed serviceworker.js file that is output in the browser. The variables in the service worker are replaced when they are processed by the Drupal backend, the assets are then output in the file.

 

Image
Second PWA image

 

Phone Home uninstall

Another clever piece of functionality that the module provides is responsible cleanup when uninstalled. The module sends a request back to a URL created by the module, if the URL does not exist it means the module has been uninstalled. The service worker then unregisters itself and deletes all related caches left on the user's browser.

 

// Fetch phone-home URL and process response.
  let phoneHomeUrl = fetch(PWA_PHONE_HOME_URL)
  .then(function (response) {
    // if no network, don't try to phone-home.
    if (!navigator.onLine) {
      console.debug('PWA: Phone-home - Network not detected.');
    }

    // if network + 200, do nothing
    if (response.status === 200) {
      console.debug('PWA: Phone-home - Network detected, module detected.');
    }


    // if network + 404, uninstall
    if (response.status === 404) {
      console.debug('PWA: Phone-home - Network detected, module NOT detected. UNINSTALLING.');
// Let SW attempt to unregister itself.
      Promise.resolve(pwaUninstallServiceWorker());
    }

    return Promise.resolve();
  })
  .catch(function(error) {
    console.error('PWA: Phone-home - ', error);
  });
};

Credit to co-maintainer Chris Rupl for this solution. 

 

Workbox Broadcast Update

Beyond this core functionality of the module, there are endless possibilities with service workers. One branch currently in local testing is Broadcast Update.

According to the Workbox docs, Broadcast Update is “A helper library that uses the Broadcast Channel API to announce when a caches entry is updated with a new response, allowing your web app to listen for these updates and react to them.” We utilize it in our module as follows; first instantly display the cached page, this page may be stale, so in the background we compare headers with the version on the server and if there is a newer version available we render a button with help text giving the user the option to refresh the page for newer content.

 

Image
Third PWA image

 

We can also enable this functionality only on specific routes.

 

Image
Fourth PWA image

 

Broadcast update works by default by checking headers Content-Length, ETag, and Last-Modified you can set it to compare your own custom headers as well, but make sure you have these headers enabled on your server if using the default implementation. 

Here is a view of our working Broadcast Update implementation in the unprocessed serviceworker.js

 

const REGULAR_EXPRESSION = [/*regular_expression*/];; // This will get replaced by the values set in the Drupal admin when the file is processed by the module.

  REGULAR_EXPRESSION.forEach(function(url) {
  console.log(url);
  // Register on route input on Drupal config.   workbox.routing.registerRoute(
      new RegExp(url + '$'),
      new workbox.strategies.StaleWhileRevalidate({
        plugins: [
          new workbox.broadcastUpdate.Plugin({
            channelName: 'dashboard-updates',
          })
        ]
      })
 );
  });

 

We display the help message and button with this added javascript which is only included on the page if Broadcast Update is enabled in the admin config.

 

(function ($, Drupal) {
  Drupal.behaviors.pwa = {
    attach: function (context, settings) {

      var displayMessageOnce = true;
      var alertText = drupalSettings.pwa.alert_text;

      var message = "<div class='broadcastcache''>"+ alertText +"</br><button ' class='reload-page'>Reload</button></div>";

      const updatesChannel = new BroadcastChannel('dashboard-updates');
      updatesChannel.addEventListener('message', async (event) => {
        const {cacheName, updatedUrl} = event.data.payload;

        // Do something with cacheName and updatedUrl...
        // Get the cached content and update the content on the page.       

        if (displayMessageOnce) {
          displayMessageOnce = false;

          $( "body" ).after( $(message) );

          $('.reload-page').click(function () {
            location.reload();
          });
        }

      });

    }
  }
})(jQuery, Drupal);

 

Though still a bleeding-edge technology, Progressive Web Apps have support from some of the biggest tech companies including strong support from Google and Microsoft, with iOS trailing behind, yet catching up. The PWA improves website efficiency, development time, SEO, and it’s extremely developer friendly . 

For more background on the PWA module, Drupal and service workers visit https://www.drupalcampatlanta.com/sites/default/files/slides/Meet%20the%20PWA%20Module%20%281%29.pdf by Christoph Weber.

 

 

Related News & Events