Book Image

Progressive Web Application Development by Example

By : Chris Love
Book Image

Progressive Web Application Development by Example

By: Chris Love

Overview of this book

Are you a developer that wants to create truly cross-platform user experiences with a minimal footprint, free of store restrictions and features customers want? Then you need to get to grips with Progressive Web Applications (PWAs), a perfect amalgamation of web and mobile applications with a blazing-fast response time. Progressive Web Application Development by Example helps you explore concepts of the PWA development by enabling you to develop three projects, starting with a 2048 game. In this game, you will review parts of a web manifest file and understand how a browser uses properties to define the home screen experience. You will then move on to learning how to develop and use a podcast client and be introduced to service workers. The application will demonstrate how service workers are registered and updated. In addition to this, you will review a caching API so that you have a firm understanding of how to use the cache within a service worker, and you'll discover core caching strategies and how to code them within a service worker. Finally, you will study how to build a tickets application, wherein you’ll apply advanced service worker techniques, such as cache invalidation. Also, you'll learn about tools you can use to validate your applications and scaffold them for quality and consistency. By the end of the book, you will have walked through browser developer tools, node modules, and online tools for creating high-quality PWAs.
Table of Contents (12 chapters)

2048

A few years ago, a popular application was a simple game called 2048. The goal is to combine blocks with numbers to ultimately total 2048. Blocks are numbered in multiples of 2. You can combine adjacent blocks with the same value to create a new block with their combined value.

I wasted more time playing this game than I care to admit. It is easy to play and highly addictive. A well-crafted brew of endorphins and gameplay.

Fortunately, there were numerous open source knock-offs available on GitHub. Several were web applications. I would wager that native versions distributed through app stores were websites wrapped in a native shell, a hybrid application.

I chose a popular repository to fork for the book. The 2048 web application is simple and a perfect candidate to demonstrate how to make an exemplary PWA example:

The source code

The original application source code is available on GitHub (https://github.com/gabrielecirulli/2048). You can clone the repository and open it in a browser to play the game. Just be forewarned that it is addictive and could distract you from learning how to create PWAs.

I forked the repository in my GitHub profile (https://github.com/docluv/2048). My version adds the manifest, icons, service workers, and applies some code upgrades to make the application perform better and take advantage of newer APIs and browser features. The original code was written very well, but this was for browser capabilities of 3 years ago.

Feel free to star, clone, and fork my GitHub repository to customize it to your liking. A working version of the final application created in this book is available online (https://2048.love2dev.com/).

The application's code structure

Let's review how the game's source code is structured. I like this project because the code is simple and demonstrates how much can be accomplished with a small amount of code in the browser.

There are three asset folders: js, meta, and style. They contain JavaScript files, images, and style sheets that are needed to render and execute the game.

You will also notice a node_modules folder. I added a local web server using grunt connect, which is a node module. The original game works just fine if you load the index.html file directly in the browser. However, due to security constraints, a service worker does not function without a web server. I will cover this in more detail shortly.

At the root-level, there are only handful of web application files:

  • index.html
  • manifest.json
  • sw.js
  • favicon.ico

The nice thing about the 2048 code is that it only requires a single HTML file. The manifest.json and sw.js files add the PWA functionality we are after. The favicon.ico file is the icon loaded by the browser for the address bar:

Adding node modules to the project

The original repository is a stand-alone game, meaning it does not require a web server to execute, just a browser. You can right-click the index.html file and choose to open it in your favorite browser. You can still do this after registering a service worker and may not notice any differences. But if you open the browser console (F12 Developer Tools), you will most likely see an error.

This error can be attributed to service worker requirements. Service workers, like most new APIs supported by browsers, require HTTPS protocol. This requirement raises the default security level and gives the browsers a minimum level of trust in your site ownership.

The service worker specification relaxes this requirement for localhost addresses. Localhost is a common way to reference your local machine, which is typically a development environment. Because it is unlikely you are going to hack yourself, browsers tend to let you do what you want—except when you open files directly from the file system.

When localhost is used to load an asset, the browser is making a traditional network request, which requires a web server to respond. This means you, the user of the local machine, has gone through the effort of launching a local web server. This is not something the average consumer knows how to do.

A file, opened from the file system, is different. Anyone can send you an index.html file that loads scary code, designed to steal your identity or worse show endless loops of cat videos! By not honoring the direct file system, access browsers are protecting you from registering a malicious service worker script. Trusting a localhost web server makes development easier by avoiding the messy process of registering a localhost SSL certificate.

There are a variety of local web servers you can run. In recent years, my preference is node connect, which I execute as a Grunt task (https://love2dev.com/blog/make-a-local-web-server-with-grunt-connect/). Because connect is a node module, you can launch it directly from the command line or a custom script. There are modules for your favorite task runner, such as Gulp and so on. Besides, node is cross-platform, so everyone can use connect.

If you are familiar with installing node modules, you can skip ahead. If node and connect are new to you, this section will serve as a simple primer to get you up and running to run all the samples applications in this book on your local machine.

The first step to loading a node module is to install them from https://www.npmjs.com/ or one of the emerging package manager sites. You can manage this from the command line if you like, or you can define the modules needed in a package.json file.

You can read more about the package.json format here (https://docs.npmjs.com/files/package.json). For our purposes, grunt and the grunt-contrib-connect module are devDependencies. You could also define a dependencies section if this were a node application.

Grunt is a task runner that gained popularity several years ago and is still my preferred task runner. Task runners, and there seems to be a new one every week, help you organize repeatable tasks into repeatable recipes. I use Grunt and custom node scripts to build and deploy my PWAs. Think about your task runner as a command-line control panel to manage your application:

{
"name": "2048",
"version": "1.0.0",
"description": "2048 Progressive Web App",
"author": "Chris Love",
"private": true,
"devDependencies": {
"grunt": "*",
"grunt-contrib-connect": "*"
}
}

Both Grunt and the Grunt connect module are node packages and must be downloaded in order to execute. The package.json file gives npm a configuration so it can manage your packages. This way, you can quickly set up your project on any machine without having to maintain your node dependencies as part of the source code.

If you have cloned the sample repository, you will note that the node modules were excluded from the source code. That's because they are not part of the application itself. They are a dependency and npm helps you recreate the desired environment.

To install the packages, you need to open a command line and change to your source code's folder. Next, you must execute the following command:

>npm install

This kicks off the npm installation process, which downloads your modules and their dependency chain. When completed, you have everything you need to run or build your application.

Next, you will need to create a gruntfile.js. This is where you tell Grunt what tasks you want to run and how you want them to run. If you want to know the details of using Grunt, visit their website (https://gruntjs.com/):

module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-connect');
// Project configuration.
grunt.initConfig({
connect: {
localhost: {
options: {
port: 15000,
keepalive: true
}
}
}
});
};

Since we are only using the connect module, the 2048 gruntfile is very simple. You need to tell Grunt to load the connect module, then register the task to run in the initConfig function.

2048 is a very simple application, which keeps our customization to a minimum. I arbitrarily chose port 15000 to serve the application and chose to have keepalive open. There are many options you can define. More details are available on the grunt-contrib-connect npm page (https://www.npmjs.com/package/grunt-contrib-connect).

The only task left to do is start the connect web server. This is done from the command line. If you still have the command line open from when you performed the npm install, you can reuse it. If not, repeat the process of opening a command line and changing to the project's folder:

>grunt connect
Running "connect:localhost" (connect) task
Waiting forever...
Started connect web server on http://localhost:15000

Execute grunt connect and you should see the preceding example output. Note that the command continues to execute. This is because it is a server, listening to requests on port 15000. You cannot execute additional commands at this prompt.

You can now load the 2048 game in your browser by entering http://localhost:15000 in the address bar.

Adding a manifest

Adding a web manifest file should be the first step in upgrading an existing website. You can create your site's manifest file in a matter of minutes. In the tooling chapter, I will review a few online resources that can help automate the process.

Registering a PWA's web manifest requires a special link element in the HTML head element. The following code shows how the 2048 manifest file is registered:

<head>
….
<link rel="manifest" href="manifest.json">
</head>

If you are familiar with referencing a style sheet, this syntax should look familiar. The difference is the rel attribute value being manifest. The href value points to the manifest file. You are free to name it anything, but manifest is the most common name.

The next chapter will go into more manifest file details. You can reference the project's manifest.json file to see how the 2048 game is configured. It contains the application's name, default URL, primary colors, and an array of icon image references.

Adding a service worker

Next, you need to register a service worker. This is done in what I call the client-side code, which is the JavaScript you are accustomed to writing. Service workers execute in a separate thread from the UI. I think about it as a background process. You still need to register the service worker for the site.

For simple registrations, like this example, my preferred method is a script block at the bottom of my site's markup. First, detect if service workers are supported. If they are, then attempt to register the site's service work. If the browser does not support service workers, skip the registration code so no exceptions occur.

Registration is done by calling the navigator.serviceWorker.register function. It accepts a single argument, which is a path to the service worker file. I will review more rules around this in later chapters.

The register function returns a promise. You can add code to log successful registration as follows:

      <script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(function
(registration) { // Registration was successful
console.log('ServiceWorker registration successful with
scope: ', registration.scope);
}).catch(function (err) { // registration failed :(
console.log('ServiceWorker registration failed: ',
err);
});
}
</script>

We will start diving into details about service workers in Chapter 5, The Service Worker Life Cycle. To help you understand the example code, let me introduce some service worker fundamentals. Service workers are completely asynchronous. They enter an idle or sleep state if they are not needed. They wake up or spin up completely in response to the operating system or browser firing and events.

All logic execution is a product of events. You must register event handlers to execute your service worker logic. The 2048 service worker registers event handlers for the install, activate, and fetch events.

The 2048 game service worker pre-caches the entire application in the install event. You will learn more about caching strategies in Chapter 6, Master the Cache API – Manage Web Assets in a Podcast Application. For now, we will cache the application so it is available all the time, without any network chatter:

self.addEventListener("install", function (event) {
console.log("Installing the service worker!");
caches.open("PRECACHE")
.then(function (cache) {
cache.addAll(cacheList);
});
});

The 2048 service worker caches assets in the install event. The application assets are defined in an array in the server worker code. The cache API provides an interface to a special storage designed specifically to persist response objects. I will defer the details to later chapters:

var cacheList = [
"index.html",
"style/main.css",
"js/keyboard_input_manager.js",
"js/html_actuator.js",
"js/grid.js",
"js/tile.js",
"js/local_storage_manager.js",
"js/game_manager.js",
"js/application.js"
];

The service worker also has an activate and a fetch event handler. A fetch event handler must be registered before the add to homescreen feature can be triggered.

The fetch event fires when the browser requests an asset from the network. This could be an image, stylesheet, script, AJAX call, and so on. The event parameter contains the request object and can be used to check your cache to see if the asset is available:

self.addEventListener("fetch", function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});

Without a fetch event handler, your application cannot work offline. There is no requirement that the handler catch any requests, just that it is registered. It is a minimal check for offline capability.

In the example fetch event handler, all caches are interrogated to see if there is an existing match to the request. If so, the locally cached version is returned. If not, the request is passed to the network.

That's it; congratulations! Your website is now a PWA, at least on your local machine:

At this point, loading the 2048 localhost site in Chrome should cause an add to homescreen prompt being displayed. If not, reload the page once or twice and apply focus to the browser tab. If you are still not seeing the prompt, check the console for any error messages and debug them accordingly.