Understanding Progressive Web Apps: Delivering a Mobile Experience - HTML5 and JavaScript Service Workers in 2021
TechSnack - Technology Tutorials
Posted on August 15, 2021
What is a PWA?
A PWA (Progressive Web Application) is a type of app software delivered through the web. PWAs are built using common technologies such as HTML, CSS and JavaScript. They can be installed and function on any platform which uses a W3C compliant web browser on either desktop or mobile devices.
It is important to note that the browser will allow your web application to become a PWA only over asecure connection
(using SSL encryption technology) or onlocalhost
.
Who Can Install PWAs?
Support for PWAs are mostly focused around mobile browsers but there is limited support for some desktop browsers as well.
Support as of August 2021:
Mobile:
Nearly every mobile browser supports PWAs, with the exception of KaiOS
Desktop:
Chrome 39 & UP
Edge 79 & UP
IOS Safari/Chrome 11.3 & UP - Partial Support
Firefox - Deprecated as of January 2021
Screenshot from caniuse.com
Why Should I Develop PWAs?
Purpose:
PWAs allow us to offer web applications that can be installed onto any device and act indistinguishably from a native app.
By meeting certain criteria, your website or web application can easily be turned into a PWA as well.Required Technologies:
- HTML
- CSS
- JavaScript
- NodeJS (optional package that we will be using here)
Benefits:
- Application Store Registration
- Offline Fallbacks
- Network or Cache First Resource Fetching
- Push Notifications
- Background Sync
- And More
It doesn't take much to get started!
Our File Structure
-root/
-index.html
-manifest.json
-service-worker.js
-logo.[png, jpg, etc...]
index.html
Development can start with a basic HTML5 boilerplate.
HTML5 Boilerplate
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TechSnack Simple PWA</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
<html>
manifest.json
In order for the user's device to know what to do with our web application, we are going to need to provide it some details. These details are fairly self explanatory.
We will leave the
icons
array empty for now. More on this shortly.
{
"name": "TechSnack Simple PWA",
"short_name": "TechSnack",
"start_url": "/?home=true",
"icons": [],
"theme_color": "#000000",
"background_color": "#FFFFFF",
"display": "fullscreen",
"orientation": "portrait"
}
With this information the user's device can:
- Install our application
- Apply a custom icon for launching the app
- Display a custom splash screen on launch
- Allow customization of the application window and behaviour
- Mimic native application behaviour
- Allow access to native features such as GPS and push notifications
- Register our application with popular app stores
Linking manifest.json
Use the link
tag to connect manifest.json
to our app
<head>
...
<link rel="manifest" href="manifest.json">
</head>
Node Packages (1 - optional)
As previously mentioned, the user's device will apply the custom icon for us. For it to do that we will need to supply at least one image for the device to reference.
What about multiple screen sizes or resolutions?
There are countless different mobile devices being used around the world today. To optimize the display of visual assets, each device prefers logos of a certain dimension.
pwa-asset-generator
You will need at least one image file of the following MIME types:
- PNG
- JPEG/JPG
- SVG
- WebP
Install
$ npm install --global pwa-asset-generator
We will now want to run the package in our webroot
directory.
The following snippet will do for our purposes.
npx pwa-asset-generator [path/to/logo] [path/to/output/dir] -i [path/to/index.html] -m [path/to/manifest.json] -f
The-f
flag generates favicon image/meta tagExecution - From
webroot
directory$ npx pwa-asset-generator logo.jpg logos -i index.html -m manifest.json -f
It is worth noting that if you execute
pwa-asset-generator
without using the-i
,-m
and-f
flags the results will be output to your console instead.Copy and paste the results into the
icons
array withinmanifest.json
Copy and paste the output into the
head
tag ofindex.html
New icons/
directory
Contains all of the generated images.
Updated index.html
Our index.html
file should look like this now:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#000000">
<link rel="apple-touch-icon" href="icons/apple-icon-180.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2732-2048.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2388-1668.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2048-1536.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1668-2224.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2224-1668.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1620-2160.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2160-1620.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1284-2778.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2778-1284.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1170-2532.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2532-1170.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2436-1125.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1242-2688.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2688-1242.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-828-1792.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1792-828.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1242-2208.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2208-1242.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-750-1334.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1334-750.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-640-1136.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1136-640.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<title>TechSnack | Simple PWA</title>
<link id="favicon" rel="sortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="manifest" href="manifest.json">
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
Final manifest.json
Our manifest.json
file should look like this now:
{
"name": "TechSnack Simple PWA",
"short_name": "TechSnack",
"start_url": "/?home=true",
"icons": [
{
"src": "icons/manifest-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/manifest-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"theme_color": "#000000",
"background_color": "#FFFFFF",
"display": "fullscreen",
"orientation": "portrait"
}
Service Worker
Before our PWA can do all of the fancy things previously mentioned we must first create a service worker.
A service worker is a listener script the browser runs in the background. The service worker runs separately from the webpage allowing for the implementation of features that do not require interaction or calls from the webpage or user.
Service Workers may support features such asperiodic sync
orgeofencing
in the future.
NOTE: Although service workers are a JavaScript file, there are additional limitations imposed when coding. You may not have access to the DOM through a service worker.
Lifecycle of a Service Worker
There are multiple functions/features that you would normally have to build out around A Service Workers Lifecycle.
We will be using an API calledworkbox
to avoid worrying about configuring what is under the hood.
workbox
API
For our service worker, we are going to employ the use of an API called workbox
. This API offers baked-in features that would require multiple articles to describe on its own.
If you are interested in digging into the nitty gritty you can read about A Service Workers Lifecycle.
ImportScript
Inside of
service-worker.js
we willimport
theworkbox API
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.0.2/workbox-sw.js');
Registering Routes
A service worker can intercept network requests from a page. It may respond to the page with cached content
or generated content
Screenshot from Google Dev
Note: (From Above)
The
method
isGET
by default.
To change this it must be specified.The order of route registration is important when multiple workers are available to handle a request.
Whichever worker has been createdfirst
will take priority in handling a given request.
service-worker.js
We can now add the following code within service-worker.js
:
...
workbox.routing.registerRoute(
({request}) => request.destination === 'image',
new workbox.strategies.CacheFirst() //to search cache first
//new workbox.strategies.NetworkFirst() //to search server first
);
That's it! The above code will:
RegisterRoute
withworkbox
- Intercept all
'image'
files at the page'srequest
Here we choose our strategies
. Would we like to serve to our page from CacheFirst
or NetworkFirst
? This we will decide by whether the specific resource(s) we are interested in is static
or dynamic
.
If they are generally
static
to the page our user lands on then we will want to serve them fromcache
.However, if they are
dynamically generated
by some sort ofback-end
then we would want to largely get that file from thenetwork
.
Final service-worker.js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.0.2/workbox-sw.js');
workbox.routing.registerRoute(
({request}) => request.destination === 'image',
new workbox.strategies.CacheFirst() //to search cache first
//new workbox.strategies.NetworkFirst() //to search server first
);
Linking service-worker.js
Now that we have our service worker in place and intercepting requests for image files, we can link our script within index.html
<body>
...
<script>
if('serviceWorker' in navigator){
navigator.serviceWorker.register('/service-worker.js');
}
</script>
</body>
Its that simple to register our service worker!
Putting it all together
We can finally take a look at the final code base for our PWA project.
Our File Structure:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#000000">
<link rel="apple-touch-icon" href="icons/apple-icon-180.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2732-2048.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2388-1668.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2048-1536.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1668-2224.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2224-1668.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1620-2160.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2160-1620.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1284-2778.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2778-1284.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1170-2532.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2532-1170.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2436-1125.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1242-2688.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2688-1242.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-828-1792.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1792-828.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1242-2208.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-2208-1242.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-750-1334.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1334-750.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-640-1136.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="icons/apple-splash-1136-640.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<title>TechSnack | Simple PWA</title>
<link id="favicon" rel="sortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="manifest" href="manifest.json">
</head>
<body>
<h1>Hello World!</h1>
<script>
if('serviceWorker' in navigator){
navigator.serviceWorker.register('/service-worker.js');
}
</script>
</body>
</html>
manifest.json
{
"name": "TechSnack Simple PWA",
"short_name": "TechSnack",
"start_url": "/?home=true",
"icons": [
{
"src": "icons/manifest-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/manifest-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"theme_color": "#000000",
"background_color": "#FFFFFF",
"display": "fullscreen",
"orientation": "portrait"
}
service-worker.js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.0.2/workbox-sw.js');
workbox.routing.registerRoute(
({request}) => request.destination === 'image',
new workbox.strategies.CacheFirst() //to search cache first
);
Serving Our Page
We can now view our page in the browser.
For
localhost
runnpx serve
in thewebroot
directory.
Then you can visit your securely servedremote ip
ordomain
Please keep in mind the browser support for your device
There is a new icon in our address bar!
The browser requires a
user action
to install the PWA
The User can click this icon to see theprompt
Now you can click on the icon installed on the device's homepage
Summarization of PWA Development
As you can see it is super easy to create a web application that can mimic the same functions as a native app. In future articles we will delve into each feature that we will now have access to with this powerful piece of technology.
Help TechSnack Write Concise Content:
Leave us a comment with your thoughts on the article below. Whether you liked or disliked the article, all feedback will help me to know how to better create content that meets your needs, goals and aspirations.
Sharing the article on your social platforms would also be a great help!
Join the conversation at r/TechSnack
Posted on August 15, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.