DEV Community

Daniel Primo
Daniel Primo

Posted on

Creating a Progressive Web Application (PWA) using HTML and Vanilla JavaScript

Creating a Progressive Web Application (PWA) using HTML and Vanilla JavaScript with an mp3 audio player can be amazing learning.

We will look into a basic example of how service workers can be used for caching in a PWA. Our application is an MP3 Player, but the principles here are applicable to any PWA.

Manifest.json

Let's start with manifest.json:

{
  "short_name": "PWA MP3 Player",
  "name": "Progressive Web Application MP3 Player",
  "description": "An MP3 Player built as a Progressive Web Application",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#3f51b5",
  "icons": [
    {
      "src": "icon.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Manifest.json provides information about an application (such as name, author, icon, and description) in a JSON text file.

The purpose of the manifest.json file is to provide a centralized place to put metadata associated with a web application.

Here's a breakdown of each property:

  • short_name: The short_name is a shorter version of the app's name and is used on the user's home screen and wherever space is limited.

  • name: The full name of the application.

  • description: Description of the application.

  • start_url: This is the URL that the PWA will start on when the user launches the app. In this case, it's set to open the application's home page.

  • display: The display property defines the developer’s preferred display mode for the website. The "standalone" value means that the application will look and feel like a standalone application. This can include the application having a different window, its own icon in the application launcher, etc.

  • background_color: This is the background color of the application, which is used during splash screen display when launching the app from a tile on the home screen.

  • theme_color: Defines the default theme color for the application, which affects the color of the toolbar and the color in the task switcher.

  • icons: This property represents an array of image files that can be used as the application's icon. Each object in the array is an image, and you can specify the path, the sizes, and the image type. The src key is the path to the image file, the sizes key is a space-separated list of image dimensions, and the type key is a MIME type for the image file.

In summary, the manifest.json file is a configuration file for your PWA that provides details about how your app should behave when installed on a device.

Service Worker

Next, let's set up a basic service worker (sw.js):

const cacheName = 'pwa-mp3-player-v1';
const assetsToCache = [
  './',
  'index.html',
  'main.js',
  'styles.css',
  'icon.png'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(cacheName)
      .then((cache) => {
        return cache.addAll(assetsToCache);
      })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        return response || fetch(event.request);
      })
  );
});
Enter fullscreen mode Exit fullscreen mode

When we talk about Progressive Web Apps (PWAs), the term "Service Workers" frequently appears. Service Workers, the true powerhouse behind PWAs, perform various essential functions, one of the most crucial being handling caching strategies.

Let's break down this piece of JavaScript code:

const cacheName = 'pwa-mp3-player-v1';
Enter fullscreen mode Exit fullscreen mode

Here, we are declaring a constant cacheName to denote the version of our cache. It's common practice to version our caches because it makes managing them easier. If we update any files in our project, we can change the cache name, which will then trigger the service worker to cache the new files.

const assetsToCache = [
  './',
  'index.html',
  'main.js',
  'styles.css',
  'icon.png'
];
Enter fullscreen mode Exit fullscreen mode

In assetsToCache, we list the files that we want to cache. This usually includes all of the static files necessary for your app shell (the minimal HTML, CSS, and JavaScript required to power the user interface of a progressive web app).

The self.addEventListener part is where the magic happens. Service Workers can listen to several lifecycle events. The two most commonly used ones are 'install' and 'fetch'.

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(cacheName)
      .then((cache) => {
        return cache.addAll(assetsToCache);
      })
  );
});
Enter fullscreen mode Exit fullscreen mode

The 'install' event fires when the service worker is first installed. Here, we tell the service worker to open the cache using caches.open(), and then cache all necessary assets using cache.addAll(). event.waitUntil() is used to ensure that the service worker doesn’t stop installing until the code inside waitUntil has completed.

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        return response || fetch(event.request);
      })
  );
});
Enter fullscreen mode Exit fullscreen mode

After the installation and caching process, the 'fetch' event fires whenever any resource (specified in assetsToCache) is requested. The event.respondWith() method allows us to intercept the request and provide a response.

Inside respondWith, we're asking the cache if the requested resource is already in there. If yes (caches.match(event.request) returns a response), we respond with the cached version, if not, we fetch it from the network using fetch(event.request).

This strategy is often called Cache Falling Back to Network. The idea is to return cached content when available, otherwise fall back to a network request.

This can ensure speed if the content is cached and availability if it's not.

Great opportunities

This is just the tip of the iceberg when it comes to service workers and their capabilities.

You can set up more complex caching strategies, handle post requests, background syncs, and much more. Service workers are a powerful tool in modern web development, enabling app-like features in the browser.

Powered by JavaScript

Now, let's create a simple audio player (main.js):

class AudioPlayer {
  constructor(audioElement) {
    this.audioElement = document.querySelector(audioElement);
  }

  play() {
    this.audioElement.play();
  }

  pause() {
    this.audioElement.pause();
  }

  togglePlay() {
    this.audioElement.paused ? this.play() : this.pause();
  }
}

document.addEventListener('DOMContentLoaded', () => {
  const player = new AudioPlayer('#audioElement');

  document.querySelector('#playButton').addEventListener('click', () => {
    player.togglePlay();
  });
});
Enter fullscreen mode Exit fullscreen mode

Let's dissect it and understand how we can harness the power of object-oriented programming in JavaScript to create interactive web components.

class AudioPlayer {
  constructor(audioElement) {
    this.audioElement = document.querySelector(audioElement);
  }
Enter fullscreen mode Exit fullscreen mode

At the start, we define a class named AudioPlayer. Classes are a blueprint for creating objects with specific methods and properties in JavaScript. Our AudioPlayer class takes a parameter audioElement in its constructor. This parameter should be a selector for the HTML audio element you want the player to control. It uses document.querySelector to get the first HTML element that matches this selector and assigns it to this.audioElement.

  play() {
    this.audioElement.play();
  }

  pause() {
    this.audioElement.pause();
  }
Enter fullscreen mode Exit fullscreen mode

We then define two methods play and pause within the class. These methods, when called, use the built-in play and pause methods on the HTMLAudioElement to control playback.

  togglePlay() {
    this.audioElement.paused ? this.play() : this.pause();
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, we have the togglePlay method. This checks whether the audio is currently paused (the paused property is true when audio is not playing). If it is paused, it calls the play method, and if it's playing, it calls the pause method. It's a simple way to toggle between play and pause with a single function.

document.addEventListener('DOMContentLoaded', () => {
  const player = new AudioPlayer('#audioElement');

  document.querySelector('#playButton').addEventListener('click', () => {
    player.togglePlay();
  });
});
Enter fullscreen mode Exit fullscreen mode

Finally, outside the class, we have some code that uses our AudioPlayer. First, we wait for the DOMContentLoaded event to ensure that our HTML has fully loaded before we try to interact with it.

Then, we create a new instance of our AudioPlayer class, passing the selector for our audio element.

We also add an event listener to the play button (#playButton). When the button is clicked, the togglePlay method is invoked on our audio player instance.

This enables us to start or pause the audio playback by clicking the button.

Good practices

This example is a perfect illustration of how we can create reusable, organized code in JavaScript using classes. The AudioPlayer class abstracts away the details of how to play, pause, and toggle audio, providing an easy-to-use interface that we can leverage across our application.

It allows us to make our code more modular, maintainable, and manageable.

Important note

Please note that service workers and the Cache API work only over HTTPS or localhost for security reasons. If you're developing locally, it should work, but once you're ready to deploy, you'll need to make sure that your server uses HTTPS.

HTML loads all

Here's a basic index.html file that works with the JavaScript code you provided:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PWA MP3 Player</title>
    <link rel="manifest" href="manifest.json">
    <style>
        /* Add your CSS styles here */
    </style>
</head>
<body>
    <div class="player">
        <audio id="audioElement" src="song.mp3" controls></audio>
        <button id="playButton">Play/Pause</button>
    </div>

    <script src="main.js"></script>
    <script>
        if ('serviceWorker' in navigator) {
            window.addEventListener('load', function() {
                navigator.serviceWorker.register('/sw.js').then(function(registration) {
                    console.log('ServiceWorker registration successful with scope: ', registration.scope);
                }, function(err) {
                    console.log('ServiceWorker registration failed: ', err);
                });
            });
        }
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

In this HTML file:

  • We have an <audio> tag with id="audioElement". This is the audio player that the AudioPlayer class will control. The src attribute should be the path to your mp3 file.

  • We have a <button> with id="playButton". When this button is clicked, it will call the togglePlay method on the AudioPlayer instance, starting or pausing the audio.

  • We're including the main.js file that contains the AudioPlayer class.

  • We also have a script to register our service worker (sw.js) if the browser supports it. The service worker will control network requests to help provide a seamless offline experience.

Thanks to the malandriner community for always contributing value, and to this article that gave me the courage to create this post: Web Reactiva 27: Convierte tu web en PWA (Progressive Web App)

Happy coding!

Top comments (0)