This article is special to me! I always wanted to create mobile applications, but I couldn't decide whether to get started with Android development or iOS development. A few years ago, I found out about Flutter which helps us create platform-independent applications. It is around the same time that I was learning web development and found out about Progressive Web Apps (PWA). PWA provides the capability to use a web application as a full-fledged mobile application.
This is going to be an elaborate article explaining how to create PWA efficiently. Let's get started!
Introduction to PWA
Progressive Web Apps (PWAs) are web apps that use new capabilities to provide a more integrated experience and can be installed as a mobile application. Since it's a web app, it can reach anyone, anywhere, on any device, all with a single codebase. Once installed, a PWA looks like any other app, such as:
- It has an icon on the home screen, app launcher, launchpad, or start menu.
- It appears when you search for apps on the device.
- It opens in a standalone window, separated from a browser's user interface.
- It has access to higher levels of integration with the OS, for example, URL handling or title bar customization.
- It can work offline.
A Progressive Web App includes features from both worlds (web and native apps).
- They are responsive and work with many different screen sizes.
- They function just like normal Native Apps.
- The updates are independent, you don't need to visit the play store for an update.
- They're built with common web technologies.
- They're fast and lightweight.
- They work offline unlike other apps.
- They are discoverable via a search engine.
- They are easily installable.
- Low maintenance cost.
Although Progressive Web Apps are applications that can be installed from the web browser, PWA can also be listed on app stores as an optional distribution channel. For the latter case, we must follow all store rules and requirements, but we will still keep some of the advantages of a PWA.
PWABuilder provides the capability of creating android/iOS build packages for a PWA. These packages could then be published on the app store.
Some good examples of top companies who have their products as PWAs include Twitter, Pinterest, Uber, Tiktok, Spotify, Flipkart, etc.
Get Started with PWA Development
The first characteristic of a progressive web app is that it must work on all devices and must enhance on devices and browsers that allow it. Therefore, you could develop a web app using traditional HTML5 and JavaScript that simulates the retrieval of data from a server API. Alternatively, SPA (Single Page Applications) could be created using Angular, React, Vue, etc.
Now, to convert this simple web application into a PWA, we would need to include the following files.
- Manifest
- Service Worker
I have created a simple weather app in React and converted it into a PWA by adding the above-mentioned files. You can view the code on this repository.
Manifest file
Each PWA should include a single manifest per application, typically hosted in the root folder, and linked on all HTML pages the PWA can be installed from. Its official extension is .webmanifest
, so you could name your manifest something like app.webmanifest
. While the recommended extension is .webmanifest
, any filename will work as long as it's delivered with the application/manifest+json
content type or another JSON-valid content type, such as text/json
. As such, many PWAs, particularly older ones, use manifest.json
instead.
This file contains information about how the PWA should appear and function. It allows us to determine the name, description, icon, colors, and other features of the PWA. With this manifest file, it is possible to run the web app in full-screen mode as a standalone application.
Here's an example of a manifest file:
{
"short_name": "MyPWA",
"name": "My Progressive Web App",
"description": "Creating a PWA that is compatible with all the devices",
"theme_color": "#eb5252",
"background_color": "#e840cf",
"display": "fullscreen",
"start_url": "index.html",
"scope": "/",
"orientation": "portrait",
"icons": [
{
"src": "images/AppIcon-48-48.png",
"type": "image/png",
"sizes": "48x48"
},
{
"src": "images/AppIcon-96-96.png",
"type": "image/png",
"sizes": "96x96"
},
{
"src": "images/AppIcon-192-192.png",
"type": "image/png",
"sizes": "192x192"
}
],
}
Letβs break down and understand what each field depicts within the manifest file:
short_name
is a human-readable name for the application. In Chrome for Android, this is the name accompanying the icon on the home screen.name
is also a human-readable name for the application and defines how the application will be listed.description
provides a general description of the web application.theme_color
is the default theme color for the application. On Android, this is also used to color the status bar.background_color
defines the background color of the web application. In Chrome, it also defines the background color of the splash screen.start_url
is the starting URL of the application.scope
changes the navigation scope of the PWA, allowing you to define what is and isn't displayed within the installed app's window. For example, if you link to a page outside of the scope, it will be rendered in an in-app browser instead of within your PWA window.display
defines the default display mode describing how the OS should draw the PWA window. They can take eitherfullscreen
,standalone
,minimal-ui
orbrowser
as value.orientation
defines the default orientation for the web application: portrait or landscape.icons
define an array of icon objects withsrc
,type
,sizes
, and optionalpurpose
fields, describing what images should represent the PWA. In Chrome for Android, the icon will be used on the splash screen, on the home screen, and in the task switcher.
Linking your manifest
To make the browser aware of your web app manifest, you need to link it to your PWA using <link>
HTML element and the rel
attribute set to manifest
on all of your PWA's HTML pages. This is similar to how you link a CSS stylesheet to a document.
<html lang="en">
<title>This is my first PWA</title>
<link rel="manifest" href="/app.webmanifest" />
</html>
Note: If your PWA includes multiple HTML pages, make sure to link its manifest to all of them.
Once the manifest file is linked to the document, you should be able to verify it on the 'Application' tab of the dev tools.
Service workers
This is one of the key technologies behind PWAs. They help support your app to work offline, and they perform advanced caching and run background tasks. Service workers can complete tasks even when your PWA is not running. Some other functions associated with service workers include:
- Sending push notification
- Badging icons
- Running background fetch tasks etc.
Before the service worker takes control of your page, it must be registered for your PWA. That means the first time a user comes to your PWA, network requests will go directly to your server because the service worker is not yet in control of your pages.
After checking if the browser supports the Service Worker API, your PWA can register a service worker. When loaded, the service worker sets up as a shop between your PWA and the network, intercepting requests and serving the corresponding responses.
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/sw.js")
.then(() => {
console.log("Service worker registered!");
})
.catch((error) => {
console.warn("Error registering service worker: ");
console.warn(error);
});
}
The above code runs on the main thread and does the following:
- Since the user's first visit to a website occurs without a registered service worker, it waits until the page is fully loaded before registering one. This avoids bandwidth contention if the service worker precaches anything. Though the service worker is well-supported, a quick check helps to avoid errors in browsers where it isn't supported.
- When the page is fully loaded, and if a service worker is supported, register
/sw.js
.
Some key things to understand are:
- Service workers are only available over
HTTPS
orlocalhost
. - If a service worker's contents contain syntax errors, registration fails and the service worker is discarded.
- Service workers operate within a scope. Here, the scope is the entire origin, as it was loaded from the root directory.
- When registration begins, the service worker state is set to
installing
.
Once registration finishes, installation begins.
Installation
A service worker fires its install
event after registration. install
is only called once per service worker, and won't fire again until it's updated. A callback for the install event can be registered in the worker's scope with addEventListener
.
// /sw.js
const CACHE_NAME = "MyCacheName_v1";
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
// Add all the assets in the array to the 'MyCacheName_v1'
// `Cache` instance for later use.
return cache.addAll(["/css/style.css", "/js/index.js"]);
})
);
});
This creates a new Cache
instance named MyCacheName_v1
and precaches assets.
After the cache is created, an array of asset URLs are precached using its asynchronous addAll
method.
The other half of a caching strategy is the service worker's fetch
event.
Fetching
self.addEventListener('fetch', async (event) => {
// Open the cache
event.respondWith(caches.open(CACHE_NAME).then((cache) => {
// Respond with the file from the cache or from the network
return cache.match(event.request).then((cachedResponse) => {
return cachedResponse || fetch(event.request.url).then((fetchedResponse) => {
// Add the network response to the cache for future visits.
// Note: we need to make a copy of the response to save it in
// the cache and use the original as the request response.
cache.put(event.request, fetchedResponse.clone());
// Return the network response
return fetchedResponse;
}).catch(() => caches.match("offline.html");
});
}));
});
When a file needs to be loaded, the above code checks if the file is in the service worker cache. If it is available, then the file is served from the cache. If not, it would fetch the file from the network, store the response in the cache, and return the network response.
If the user is offline or facing any network issues, then the file offline.html
would be loaded as a fallback to the user. This file could either have a static template to inform the user that they are offline or it could have some dynamic content such as the dinosaur game on google.
There are various caching strategies available while making this fetch
event call. They are:
- Stale While Revalidate uses a cached response for a request if it's available and updates the cache in the background with a response from the network. Therefore, if the asset isn't cached, it will wait for the network response and use that. It's a fairly safe strategy, as it regularly updates cache entries that rely on it. The downside is that it always requests an asset from the network in the background.
- Network First tries to get a response from the network first. If a response is received, it passes that response to the browser and saves it to a cache. If the network request fails, the last cached response will be used, enabling offline access to the asset.
- Cache First checks the cache for a response first and uses it if available. If the request isn't in the cache, the network is used and any valid response is added to the cache before being passed to the browser.
- Network Only forces the response to come from the network.
- Cache Only forces the response to come from the cache.
Click here to learn more about them.
Apart from the 2 events that we discussed so far, service workers have another event called activate
.
Activating
If registration and installation succeed, the service worker activates, and its state becomes 'activating'. When an updated service worker is installed and the waiting phase ends, it activates, and the old service worker is discarded. A common task to perform in an updated service worker's activate
event is to prune old caches. Remove old caches by getting the keys for all open Cache instances with caches.keys
and deleting caches that aren't in a defined allow list with caches.delete
// Activate the Service Worker
// remove all the previous versions and keep the latest data.
self.addEventListener("activate", (event) => {
// Specify allowed cache keys
const cacheAllowList = ["MyCacheName_v2"];
// Get all the currently active `Cache` instances.
event.waitUntil(
caches.keys().then((keys) => {
// Delete all caches that aren't in the allow list:
return Promise.all(
keys.map((key) => {
if (!cacheAllowList.includes(key)) {
return caches.delete(key);
}
})
);
})
);
});
Old caches don't tidy themselves. We need to do that ourselves or risk exceeding storage quotas. Since 'MyCacheName_v1' from the first service worker is out of date, the cache allow list is updated to specify 'MyCacheName_v2', which deletes caches with a different name.
The activate event will finish after the old cache is removed. At this point, the new service worker will take control of the page, finally replacing the old one!
Well, that was a lot of learning πͺ. Congratulations, you have successfully created your first PWA. If you observe, we have done a lot of manipulations around the service worker. This is when it's good to know about the workbox.
What is Workbox
At this point, service workers may seem tricky. There are lots of complex interactions that are hard to get right. Network requests, Caching strategies, Cache management, and Precaching, It's a lot to remember. This doesn't make service workers an ill-designed technology; it works as intended, and solves hard problems.
Good abstractions make complex APIs easier to use. That's where Workbox comes in. Workbox is a set of modules that simplify common service worker routing and caching. Each module available addresses a specific aspect of service worker development. Workbox aims to make using service workers as easy as possible while allowing the flexibility to accommodate complex application requirements where needed.
We shall discuss more on workbox
in my upcoming blog. However, if you wish to get started, then this is the best place to refer.
Thank you for reading through this article. I hope it provided a good understanding of how to get started with converting your web apps into PWA. Please share your thoughts/queries in the comments section or on Twitter. If this blog is interesting to you, I would appreciate it if you could give this post a like (with your favorite emoji π). Feel free to buy me a coffee!
Peace β