If you're considering refactoring a web app into a mobile one, there are a couple of things you want to assess before rewriting the entire frontend codebase. You might be able to achieve what you are looking for with a much simpler solution, Progressive Web Apps and service workers!
In this article, we'll be discussing what a Progressive Web Application is, how you could implement it, and the limitations of using a Progressive Web App for your own projects.
There are many technical definitions of Progressive Web Applications out there, but in the simplest of terms, it’s a website that can be downloaded as an app straight from your browser.
A Progressive Web App gives you all the perks of building a regular website with the convenience of native mobile apps. This also means that it allows your application to send notifications and work in offline mode on all supported devices, much like regular apps1. As long as your website is responsive, your app becomes available for all mobile users, which represent most of the online users today.
There has been a lot of transition lately to using Progressive Web Apps by companies like Twitter and AliExpress3.
PWAs have their own advantages as a mobile app. The key benefit is that the application is downloaded but not installed, so there’s no need to submit it to any Mobile Application Store. This makes them available to the user in a significantly faster manner, allowing the client to circumvent many bottlenecks in the lifecycle of the product, such as approval waits on updates3.
A PWA can also be indexed by search engines, which creates an SEO advantage.
All of this is made possible mostly by the browser’s service workers. These scripts run in the background of the user's web browser, enabling key functionalities such as offline access, push notifications, and caching data. For security reasons, service workers only run over HTTPS because they have the ability to intercept network requests and modify responses.
These scripts, much like web workers, allow your browser to run asynchronous, event-based functionalities without interrupting or blocking your application, making extensive use of promises and running in a separate thread from the main4. These workers don’t have any access to the DOM directly, so they’re not able to use the browser’s local storage. For that reason, service workers use the Cache API. The cache storage is what allows you to save relevant files and data and emulate an offline mode.
Before converting your existing site to a PWA, it’s important to ensure you have a strong foundation. If you're starting from scratch, you'll first need to build a basic website that serves as the framework for your progressive web app. This involves creating a functional, responsive site using the basic building blocks like HTML, CSS, and JavaScript, ensuring it's responsive and runs efficiently on different devices.
Once the website is set up, you can then follow these steps to set up a Progressive Web Application:
First you’ll need to set up a ‘.webmanifest’ file on your directory and add a link in your html to access your web app manifest file, similar to what you do with your CSS style sheet, within the ‘head’ tag. The web manifest is a JSON file that will serve as a guide to your browser for the downloaded app. It should contain basic information on your app like name, url, icons, etc;
// manifest.webmanifest
{
"name":"My_app_name",
"short_name":"My_app",
"start_url":"https://myappname.com",
"display":"standalone", //the preferred display mode (look and feel)
"background_color":"#fff", //color of the background until the stylesheet is loaded
"description":"turning my app into a PWA",
"icons":[
{
"src":"./images/icon.png"
}
]
}
// index.html
<link rel="manifest" href="./manifest.webmanifest">
The next step you’ll take is to check if the browser supports a service worker and if it does, then register it. You’ll need to create a service worker JavaScript file (commonly named serviceWorker.js) that will manage background tasks like caching and offline functionality. This JavaScript file should be referenced in your HTML to ensure the service worker is registered properly.
If you're using vanilla JS, you’ll add an event listener on ‘load’ to run the check and then register the service worker. For the case you are using a framework, like React, you’ll add a script that runs a similar logic to your main html file, outside of the ‘body’ tag;
// index.html
<script>
// first check if the browser supports the service worker
if ("serviceWorker" in navigator) {
// navigator.serviceWorker returns a service worker container,
// that has a method to register it to the browser.
// Register method returns a promise, which allows you to
// use the ‘.then’ or ‘.catch’ syntax
navigator.serviceWorker.register("serviceWorker.js")
.catch((err) => {
console.log("Service worker registration failed");
});
}
</script>
It's best practice to update your service worker on every reload of your browser during development mode, so make sure to check that on your browser’s developer tools.
Finally you’ll set up your service worker. Start by creating a ‘serviceWorker.js’ file in your project’s directory. This file is where you’ll add all of the main functionalities to your service worker.
In order to follow the service worker’s lifecycle4, you’ll need three main event listeners. Keep in mind you can always add more to configure your service worker.
The first event your service worker will be listening to is ‘install’, which is the first thing it does after it’s been registered. Here is where you’ll create a cache in the browser and store all the static files, allowing you to run the web app offline.
const cacheName = "myOfflineCache";
// list all static files in your project
const staticAssets = ["./", "./index.html"];
self.addEventListener("install", async (event) => {
// ”caches” is an instance of the CacheStorage
// the method “open” returns a promise that resolves to
// the cache object matching the cache name
const cache = await caches.open(cacheName);
await cache.addAll(staticAssets);
// allow the newly installed service worker to move on to activation
return self.skipWaiting();
});
Keep in mind that the keyword ‘self’ here gives you access to the service worker’s global scope and the methods and objects available to it.
The second event the service worker will be listening to is ‘activate’, which is triggered as soon as installation is complete. Although the it is activated, the application will only use it in the next reload. To make sure the service worker is used in the current session, you’ll have to use the ‘clients’ object’s, which gives you access to methods on the current executable context. By calling the method ‘claim’ the service worker becomes the controller of that context.
self.addEventListener("activate", event => {
self.clients.claim();
});
Finally, you’ll add an event listener for ‘fetch’ requests, which is where your service worker will either direct the request to the network or to your cache, emulating the offline mode.
For this event, break down the traffic by checking where it's requesting information from. If it's data from your own application, like your static files, serve them from the cache you created before. In all other cases, it’s requiring data from the network and that's when you’ll need to add the conditional statement for the offline mode. This should make your service worker fetch data from the cache in case the network fails to respond.
self.addEventListener("fetch", async event => {
const req = event.request;
const url = new URL(req.url);
// check if the request is requiring data from our own application(location)
if (url.origin === location.origin) {
// check our cache
event.respondWith(checkCache(req));
}
// else, fetch from the network and cache that result
else {
event.respondWith(checkNetwork(req));
}
});
async function checkCache(req) {
// open our cache
const cache = await caches.open(cacheName);
// check if there’s data there that match with what the request requires
const cachedData = await cache.match(req);
// if there’s data cached, return it, else fetch from the network
return cachedData || fetch(req);
}
async function checkNetwork(req) {
// open our cache
const cache = await caches.open(cacheName);
// try to fetch data from the network
try {
// save the fetched data
const freshData = await fetch(req);
// save a copy of the response to your cache
await cache.put(req, freshData.clone());
// send the response back (returned the fetched data)
return freshData;
}
// if we are unable to fetch from the network (offline)
catch (err) {
// match the request with data from the cache
const cachedData = await cache.match(req);
// return the cached data
return cachedData;
}
}
It’s important to implement this by checking your browser’s developer tools ‘Application’ tab during all these steps, so you can understand a little better what each of them is actually doing. You’ll be able to see your service worker, the cache storage and the manifest. Once you are done, you can add the application to your home screen by clicking on ‘Add to home screen’ link on your application’s manifest inside the developer tools.
As with any engineering solution, there are drawbacks too. PWAs don’t enable you to take full advantage of all the features you can have in a native mobile environment. This means that it doesn’t provide a user interface with the best possible performance and responsiveness. If that’s what you are looking for, stick to frameworks that are specific to the mobile environment, like React Native.
If you do opt into turning your web application into a PWA, with that simple setup you'll have a downloadable application that can be accessed in any tablet or smartphone and reach all of your mobile users with new amazing features! This setup only allows you to have the offline mode, but remember, there are many other configurations you can add to your service worker to make your app’s user experience even better, like push notifications and periodic background syncs.
References & Useful Links:
Progressive Web Apps are a type of web app that feel more like native mobile apps, offering features like offline use, push notifications, and the ability to be installed on a device's home screen. Regular web apps are accessed through a browser and don’t typically offer these features. PWAs aim to combine the best of both worlds—offering the accessibility of a web app with the smoother experience of a native app.
To turn your existing website into a Progressive Web App, you'll need to add a few key features. First, create a web app manifest, which is a simple JSON file that provides metadata about your app (like its name, icons, and theme color). Next, implement a service worker, which allows your app to work offline and load quickly. Finally, ensure your website is served over HTTPS for security.
To make your app a PWA, you need to add a web app manifest and a service worker. The manifest helps the browser understand how to display your app, while the service worker allows your app to load even when there's no internet connection by caching important files. You should also ensure that your app works well on different devices and loads quickly. Once you've implemented these, your app will meet the basic requirements of a PWA.
PWAs can't typically be installed from the main app stores, like Apple's App Store or Google Play. Instead, users can install PWAs directly from their browser. For example, when visiting a PWA-enabled site on their phone, users will often see an "Add to Home Screen" prompt, which allows them to install the app. There are some ways to package a PWA for app stores, but it's not the standard method of installation.