A progressive web application (PWA) is a web application that can be installed on a user’s device and used just like a native app. It gives your website great features that traditional websites cannot do. This increases performance and makes it SEO-friendly and usable across all devices.
In this article, we will build a Progressive Web Application using HTML, CSS, and Vanilla JavaScript.
- What is a progressive Web application
- A brief history of PWA
- Benefits of a Progressive Web app
- Getting Started with PWA
- What is Web App manifest
- Building a JavaScript Calculator
- Transforming our JavaScript Calculator into a PWA Calculator
- Conclusion
What is a progressive Web application?
A progressive web app (PWA) is an application that offers native app features through web technologies (HTML, CSS, and JavaScript). The web application can easily be published online by developers, who can ensure that it satisfies the minimal installation requirements and that users can add it to their home screens.
A brief history of PWA
Progressive web apps, first coined by Frances Berriman and Alex Russell in 2015, gained popularity in 2016 when Google announced it would support PWAs in Chrome. Similar announcements from other major browser organization, such as Mozilla and Microsoft, followed this announcement.
In 2017, Google launched the Progressive Web App Experimentation Platform, which allowed developers to test PWAs in a broader range of devices and browsers. This platform helped to accelerate the adoption of PWAs by making it easier for developers to build and test PWAs.
In 2018, Google announced it would add support for PWAs to Android. This announcement allowed developers to build PWAs that could be installed on Android devices and used like native apps.
Benefits of a Progressive Web app
There are many benefits of building a PWA;
- Installable: PWAs can be installed on the user's device like a native app. This allows users to access the app even when they are offline.
- Offline-capable: PWAs can work offline, even without an internet connection. This is because PWAs cache resources, such as images and JavaScript files. Thus, they can be accessed even when the user is offline.
- App-like: PWAs can look and feel like native apps. PWAs can use the device's native UI elements, such as the notification bar and home screen.
- Fast: PWAs are typically faster than traditional web applications. This is primarily because of their cache resources and the fact that they use the device's native APIs.
- Reliable: PWAs are more reliable than traditional web applications. This is because PWAs can work offline and are not affected by network outages.
- Engaging: PWAs can be more engaging than traditional web applications. This is because PWAs can use push notifications to keep users updated with new content.
Getting Started with PWA
PWA is made possible through the help of service worker and manifest.json. Service worker and manifest.json convert our traditional web app to a progressive one.
What is a Service Worker
A service worker is a script that runs in the background of your browser and manages network requests for your web application.
They can be used to:
- Cache resources so that they can be accessed even when the user is offline.
- Control offline behavior. One of such situation is determining what happens when the user tries to access a page that is not cached.
- Provide push notifications, which can be used to keep users updated with new content.
Benefits of Service Worker
Service worker provides multiple benefits in addition to being a powerful tool for enhancing your online application's functionality, dependability, and user experience.
- Improved performance: They can cache resources like images and JavaScript files. This allows these files to be accessed quickly, even when the user is offline. Thus improving the performance of your web application, especially on mobile devices and regions with slow internet connections.
- Improved reliability: Service worker are capable of handling network errors, such as outages. Thus, your web application can continue to function even when the user is offline. This can improve the reliability of your web application, especially for users with unreliable internet connections.
- Improved engagement: Can be used to provide push notifications, which can be used to keep users updated with new content. This can help improve your users' engagement, especially for users who are interested in your content.
Limitation of Service Worker
Service worker are a powerful tool, but they have some limitations. It is important to understand these limitations before you start using service workers.
Here are some of the things that service workers cannot do:
- Service workers cannot access the DOM (). This means that they cannot directly interact with the user interface of your web application.
- Service workers cannot access the user's data; This means that they cannot access the user's cookies, local storage, or IndexedDB.
What is Web App manifest
Web App manifest which is the manifest.json is a configuration file used in PWAs to provide metadata and settings about the web application. It is a JSON (JavaScript Object Notation) file that contains key-value pairs defining various properties, such as the app's name, icons, colours, and other characteristics.
The manifest.json file contains the following information:
- Name: The name of your PWA.
- Description: A short description of your PWA. What does it do?
- Start_url: The page URL should be opened when the PWA is launched.
- Icons: A list of graphical symbols used to represent your PWA.
- Theme_colour: The colour that should be used for the UI of your PWA.
- Background_color: The background colour that should be used for the UI of your PWA.
- Display: The display mode that should be used for your PWA.
- Scope: The scope of a Progressive Web App (PWA) is the set of resources that are accessible to the PWA when it is running offline.
Building a JavaScript Calculator
For this article, I will transform our calculator application into a PWA. To do this, I need to start with my Index.html, which containers all the code I will use.
Index file
This folder will contain all of our index.html code that will be used in building this project. in our project, we will be building a calculator and making it a progressive web app which means it will have all the benefits of a PWA.
Index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="building a progressive web app" />
<meta name="author" content="Onwumene Joshua" />
<title>Progressive Web App CALCULATOR</title>
<link rel="stylesheet" href="style.css" />
<script src="main.js" defer></script>
<link rel="manifest" href="manifest.json" />
</head>
<body>
<div class="container">
<table>
<tr>
<th colspan="4">
<input
type="text"
class="display"
value=""
readonly
placeholder="0"
/>
</th>
</tr>
<tr>
<td><button class="open-par">(</button></td>
<td><button class="close-par">)</button></td>
<td><button class="delete">Del</button></td>
<td><button class="add">+</button></td>
</tr>
<tr>
<td><button class="seven">7</button></td>
<td><button class="eight">8</button></td>
<td><button class="nine">9</button></td>
<td><button class="minus">-</button></td>
</tr>
<tr>
<td><button class="four">4</button></td>
<td><button class="five">5</button></td>
<td><button class="six">6</button></td>
<td><button class="multiply">x</button></td>
</tr>
<tr>
<td><button class="one">1</button></td>
<td><button class="two">2</button></td>
<td><button class="three">3</button></td>
<td><button class="divide">÷</button></td>
</tr>
<tr>
<td><button class="dot">.</button></td>
<td><button class="zero">0</button></td>
<td colspan="2"><button class="equalTo">=</button></td>
</tr>
</table>
</div>
</body>
</html>
In the code above, the calculator was created using html table property for easy arrangement styling, where each table row contains four table data, except the last table row, which contains three table data
CSS Stylesheet
The CSS stylesheet attributes the design features to our calculator.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: rgb(38, 37, 37);
height: 100vh;
}
table {
margin: 100px auto;
border-radius: 10px;
padding: 25px;
font-family: "Montserrat", sans-serif;
background-color: gray;
}
td {
/* This is about the table data, the space between each data */
padding: 5px;
width: 100px;
height: 100px;
}
.display {
border: none;
background-color: #5e666f;
border-radius: 10px;
padding: 25px;
text-align: right;
font-size: 30px;
font-family: "Roboto Mono", monospace;
color: #800f0f;
margin-bottom: 20px;
}
input {
border: none;
outline: none;
}
button {
width: 100%;
height: 100%;
border: none;
border-radius: 8px;
font-size: 30px;
font-weight: bold;
padding: 10px 8px;
cursor: pointer;
font-family: "Poppins", sans-serif;
}
button:hover {
transition: 1s;
transform: scale(1.1);
}
.multiply,
.divide,
.minus,
.add,
.equalTo {
background-color: #7e90a5;
box-shadow: rgba(50, 50, 93, 0.25) 0px 30px 60px -12px inset,
rgba(0, 0, 0, 0.3) 0px 18px 36px -18px inset;
}
.delete {
background-color: #4e2121;
}
.open-par,
.close-par {
background-color: #65707d;
box-shadow: rgba(50, 50, 93, 0.25) 0px 30px 60px -12px inset,
rgba(0, 0, 0, 0.3) 0px 18px 36px -18px inset;
}
.dot,
.zero,
.one,
.two,
.three,
.four,
.five,
.six,
.seven,
.eight,
.nine {
box-shadow: rgba(0, 0, 0, 0.17) 0px -23px 25px 0px inset,
rgba(0, 0, 0, 0.15) 0px -36px 30px 0px inset,
rgba(0, 0, 0, 0.1) 0px -79px 40px 0px inset, rgba(0, 0, 0, 0.06) 0px 2px 1px,
rgba(0, 0, 0, 0.09) 0px 4px 2px, rgba(0, 0, 0, 0.09) 0px 8px 4px,
rgba(0, 0, 0, 0.09) 0px 16px 8px, rgba(0, 0, 0, 0.09) 0px 32px 16px;
}
We gave our table; margin: 100px auto; this set our top and bottom margin at 100 pixels and horizontally centers the element within its container by evenly distributing the available space on the left and right sides.
All the button was given the same styling for proper arrangement.
Creating the js file
The JavaScript file will help give functionality to the calculator. To do this, we select all our buttons using querySelector and give it the “click” event listener so that when the user clicks on it, it will be able to display something, in our case, we are displaying the value being clicked.
main.js
const open = document.querySelector(".open-par");
const close = document.querySelector(".close-par");
const display = document.querySelector(".display");
const del = document.querySelector(".delete");
const divide = document.querySelector(".divide");
const one = document.querySelector(".one");
const two = document.querySelector(".two");
const three = document.querySelector(".three");
const multiply = document.querySelector(".multiply");
const four = document.querySelector(".four");
const five = document.querySelector(".five");
const six = document.querySelector(".six");
const add = document.querySelector(".add");
const seven = document.querySelector(".seven");
const eight = document.querySelector(".eight");
const nine = document.querySelector(".nine");
const minus = document.querySelector(".minus");
const dot = document.querySelector(".dot");
const zero = document.querySelector(".zero");
const equalTo = document.querySelector(".equalTo");
//display.value tells the web browser to display whatever the value holds while the += means "append". i.e append what we click to our previous value.
open.addEventListener("click", function () {
display.value += "(";
});
close.addEventListener("click", function () {
display.value += ")";
});
one.addEventListener("click", function () {
display.value += "1";
});
two.addEventListener("click", function () {
display.value += "2";
});
three.addEventListener("click", function () {
display.value += "3";
});
four.addEventListener("click", function () {
display.value += "4";
});
five.addEventListener("click", function () {
display.value += "5";
});
six.addEventListener("click", function () {
display.value += "6";
});
seven.addEventListener("click", function () {
display.value += "7";
});
eight.addEventListener("click", function () {
display.value += "8";
});
nine.addEventListener("click", function () {
display.value += "9";
});
zero.addEventListener("click", function () {
display.value += "0";
});
dot.addEventListener("click", function () {
display.value += ".";
});
del.addEventListener("click", function () {
display.value = "";
});
equalTo.addEventListener("click", function () {
//this means, try(run) this code for us, but if you catch(see) any error in the input, then display this error message "WRONG INPUT"
try {
display.value = eval(display.value);
} catch (err) {
display.value = "WRONG INPUT";
}
});
add.addEventListener("click", function () {
display.value += "+";
});
minus.addEventListener("click", function () {
display.value += "-";
});
multiply.addEventListener("click", function () {
display.value += "*";
});
divide.addEventListener("click", function () {
display.value += "/";
});
Transforming our JavaScript Calculator into a PWA Calculator
We’ve successfully built a traditional web app, but not a progressive one. So to convert our traditional web app into a progressive one, we need to add a service worker and manifest.json to our project.
Create the Service Worker
Knowing what service worker is and their benefits, we will use it to create this progressive app. For this project, we will create a service worker from scratch, not using a framework like workbox. Workbox will, however, make the work a lot easier and better.
The service worker has three life cycles or phases
- Registration
- Installation
- Activation
Registration
In this phase, the service worker script is registered with the browser. This is done by calling the navigator.serviceWorker.register()
method. The .register() method takes a service worker registration options object as its argument. The service worker registration options object specifies the scope of the service worker, the name of the service worker, and the list of events that the service worker should listen for.
To register the service worker, you go to your main.js file and register it there;
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("./sw.js")
.then((reg) => console.log("service Worker: registered"))
.catch((err) => console.log(`service worker; Error:${err}`));
});
}
The code above checks to see if the serviceWorker property is present in the navigator object, (that is the browser) If it is, the code registers a service worker with the ./sw.js file. The register() method returns a promise, the .then() means if the registration is successful, console.log(service worker: registered). If the registration fails, the .catch() block is executed, and the error message is printed out.
Installation
Here the browser downloads and installs the service worker script. This is done asynchronously. Once the service worker script has been installed, the browser will call the install event handler of the service worker. The install event handler is responsible for initializing the service worker and caching any resources that the service worker will need to access when it is activated. This will be done in our sw.js file
const cacheName = "calculator v1"; //the name of our cache
const cacheAsset = ["index.html", "style.css", "main.js"]; //this is the asset that we want to cache
self.addEventListener("install", (e) => {
console.log("service worker installed");
e.waitUntil(
caches
.open(cacheName)
.then((cache) => {
console.log("service worker: caching files");
cache.addAll(cacheAsset);
})
.then(() => self.skipWaiting())
);
});
Self refers to the service worker itself.
The code above defines a cache name and a list of assets that should be cached (index.html, CSS stylesheet, and main.js). The code then registers a service worker and uses the install event handler to cache the assets. The install event handler is called when the service worker is installed.
The caches object provides methods for opening and closing caches. The open() method takes a cache name as its argument and returns a promise that resolves with a cache object. The cache object provides methods for adding and removing resources from the cache. The addAll() method takes an array of resource URLs as its argument(our cacheAssets in our example) and adds all of the resources to the cache. The skipWaiting() method tells the browser that the service worker is ready to handle network requests.
The code in the install event handler first opens a cache with the name calculator v1. The code then adds the assets in the cacheAsset array to the cache. Finally, the code calls the skipWaiting() method to tell the browser that the service worker is ready to handle network requests.
Activation
The service worker is activated by the browser. This is done when the browser determines that the service worker can provide a better user experience than the original web page. Once the service worker has been activated, the browser will call the activate event handler of the service worker. The activate event handler is responsible for starting the service worker and beginning to intercept network requests.
self.addEventListener("activate", (e) => {
console.log("service worker activated");
//removing unwanted caches
e.waitUntil(
caches.keys().then((cacheName) => {
return Promise.all(
cacheName.map((cache) => {
if (cache !== cacheName) {
console.log("Service worker: clear old caches");
return caches.delete(cache);
}
})
);
})
);
});
The code above is a service worker that clears old caches when it is activated. The code first gets a list of all of the caches that are associated with the service worker. The code then iterates through the list of caches and deletes any caches that are not the current cache. The code uses the waitUntil method to ensure that all of the caches are deleted before the service worker is activated.
The caches object provides methods for opening and closing caches. The keys() method returns a promise that resolves with an array of all of the cache names. The map() method iterates through an array and calls a function for each element in the array. The Promise.all() method waits for all of the promises in an array to resolve before it resolves (that is, the promise.all method is used to wait for all of the caches that is not in our cacheName to be deleted before the service worker is activated). The waitUntil method tells the browser that the service worker will not be activated until the promise resolves.
Fetch our cache asset
To cache our page, making it available to work offline; we use the fetch event
self.addEventListener("fetch", (e) => {
console.log("service worker: fetching");
//checking if the live site is avaialble and if not, responsd with the cache site
e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
});
The code in the fetch event handler first makes a network request to the live site. If the network request is successful, the code responds with the response from the network request. If the network request is not successful, the code responds with the cached site.
Now our calculator project can be cached, and available offline, and we can now have a push notification, basically, it now has all the benefits of service worker. Next, we create the manifest.json file to provide our web application information when the user wants to install its application.
Creating the Web App manifest file
The manifest.json file is just like the regular JSON (JavaScript Object Notation) file used for storing data in web applications. When building a Progressive Web Application, the manifest.json stores necessary data and information about the PWA.
{
"name": "calculator",
"short_name": "calculator",
"theme_color": "#fff",
"background_color": "#fff",
"display": "standalone",
"orientation": "any",
"scope": "/",
"start_url": "/index.html",
"icons": [
{
"src": "images/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "images/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "images/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "images/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "images/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "images/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
It contains some mandatory properties or optional properties like the name, icon, and scope.
With these, we have transformed our regular calculator web app into a progressive web app (PWA). To confirm if your web app has been transformed into a progressive web app, the Google lighthouse tool in the developer tool will show you the PWA icons. This shows that your application has been transformed.
Conclusion
We’ve now seen how to transform our regular calculator app into a progressive web app with just Service worker and manifest.json. Making your web app a PWA will significantly affect it positively because it contains all the benefits of a traditional app and of a native app.
Also, using a framework like Workbox can help you create a service worker and free you from the stress of writing it from scratch. However, knowing how to create a Service worker using Vanilla JavaScript will help you better understand PWA and appreciate it.
Top comments (3)
Hello Eckehard
You can make your little file a PWA if you want to, is base on choice and what you want to achieve with it.
By the way, I am sorry for replying late, have been chocked with school lately.
I'm curious if you could document ideas on deployment strategies for a pwa, like... vercel? how can one add a pwa to an iOS homescreen?
Hy Joshua,
nice writeup and a good overview of the concept of a PWA.
I was just wondering, if this might bring too much overhead for a small app like a calculator. Here was an example of a small "app", that can be shipped as a single HTML file. This follows a "generative" approach wich allows to write very compact code.
Are there some real advantages of a PWA about just shipping HTML? Or could I make a PWA of my little file?