Creating a Progressive Web Application (PWA) using HTML and Vanilla JavaScript
Daniel Primo
Posted on May 29, 2023
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"
}
]
}
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. Thesrc
key is the path to the image file, thesizes
key is a space-separated list of image dimensions, and thetype
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);
})
);
});
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';
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'
];
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);
})
);
});
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);
})
);
});
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();
});
});
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);
}
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();
}
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();
}
}
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();
});
});
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>
In this HTML file:
We have an
<audio>
tag withid="audioElement"
. This is the audio player that theAudioPlayer
class will control. Thesrc
attribute should be the path to your mp3 file.We have a
<button>
withid="playButton"
. When this button is clicked, it will call thetogglePlay
method on theAudioPlayer
instance, starting or pausing the audio.We're including the
main.js
file that contains theAudioPlayer
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!
Posted on May 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 26, 2024