Skip to content

Latest commit

 

History

History
1380 lines (1041 loc) · 55.1 KB

ch3.asciidoc

File metadata and controls

1380 lines (1041 loc) · 55.1 KB

Grow with Google Challenge Notes: Chapter 3

Introducing the Service Worker Notes
To go back to the README where all the chapters are: click here.

Table of Contents

3.1: An Overview of Service Worker

All service worker really is is a single javascript file that sits between the website and the network. SW is a web worker which means it runs separately from the website. The user cannot see it. The SW cannot access the DOM.
It does control web pages, and by control, it means it intercepts requests and sends it to wherever you tell it to.

Registering the SW

The first step you need to do is register the service worker by giving the location of the service worker. Inside the () is the location of the service worker.

navigator.serviceWorker.register('/sw.js')

After registering it, it will then return a promise so you can get call backs for success or failure.

Note
But wait, what does a promise really mean. I had to go look it up here.
"Essentially, a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function."
    navigator.serviceWorker.register('/sw.js').then(function(reg) {
    console.log('yay');
    }).catch(function(err) {
    console.log('Boo!');
    });
Note
If by any chance you accidentally registerred it when there’s already a SW running, it will just return a promise from the existing registration. Not a big deal.

Registration’s scope

You can also provide a scope for the SW during registration. A scope are the directory/files that SW can control. It will ignore anything else.

navigator.serviceWorker.register('/sw.js', {
    scope: '/my-app/'
})

If given a scope /my-app/, scope will only go to these:
/my-app/
/my-app/hello/world/

It cannot go to these:
/
/another-app/
/my-app (same as '/' because it doesn’t have the trailing slash)

Note
You do not need scope. The default scope is where the path sits in. So be sure to put it in the correct place
example: /foo/sw.js will affect /foo/
example 2: /foo/bar/sw.js will affect /foo/bar/

What goes on in the SW?

SW listens for events. Just like regular Javascript, you can react to them or even prevent the default and do your own thing. Here’s a sample code he featured for listening events that he will elaborate later.

self.addEventListener('install', function(event) {});
self.addEventListener('activate', function(event) {});
self.addEventListener('fetch', function(event) {});

What browsers support service worker?

Here’s the link to see which browsers support Service Worker. By the looks of it, all of the major ones do, but only chrome supports Background sync.

Jake mentions that SW is progressive enhancement ready. Which means it won’t hurt those browsers that doesn’t support it. They just won’t get the benefits. To do so, wrap your registration in an if feature detect.

if (navigator.serviceWorker) {
    navigator.serviceWorker.register('/sw.js');
}

3.2: Quiz: Scoping Quiz

Mike wants to remind us the importance of where the scope affects. So if the scope is /foo/, which of these would it affect?

a: /
b: /sw.js
c: /foo
d: /foo.html
e: /foo/
f: /foo/bar/index.html
g: /foo/bar

The answer is /foo/ or anything after /foo/ which would be E, F, and G.

3.3: Adding a Service Worker to the Project

We are finally diving into the code.

Testing

Step 1: The first thing we need to do is head over to our index.js file.
public> js > sw > index.js
Step 2: Currently, the file is empty. Jake wants us to add a simple console.log("hello") so the build system picks it up and shoots it to the root of the server in sw.js which is located in
build>public> sw.js
Step 3: After adding the console.log() into the index, you will see that it was also inserted into sw.js. The extra code in sw.js is from the output of Babel which the script runs through.

addEventListener fetch

We are going to work on fetch.

Step 1: Head back over to index.js in public>js>sw>
Step 2: delete the previous test console.log and add in a listener.

self.addEventListener('fetch', function() {
}

Once you have a service worker, and a user navigates to the page within the SW’s scope, it controls the web page. The website goes to the SW and triggers a fetch event. It will also retrieve every request event triggered by that page such as css, js, images. You get a fetch event for each, even if the requests were to another origin. We can inspect the requests with Javascript and give it a fetch like so…​

self.addEventListener('fetch', function(event) {
    console.log(event.request);
});

3.4: Quiz: Registering a Service Worker

This quiz session wants you to register the service worker so it can run as soon as we start our app. We reviewed it in the previous lesson and now we just have to put it in practice. First we have to set up the template by getting Jake’s git branch for this lesson.

Step 1: If you already have the server running, open up another console and navigate to the wittr folder. Type in:

git reset --hard
git checkout task-register-sw

Step 2: Open public> js> main> IndexController.js and find:

IndexController.prototype._registerServiceWorker = function() {
// Todo: register service worker
};
Note
The IndexController.js file takes care of the setup of the app. That’s where we can setup a web socket for live updates. Javascript does not have private methods. It’s good to start methods with an underscore if they will only ever be called by other methods of this object.

Step 3: Mike wants you to register the SW where he says to "register service worker" inside the code.
He also wants the scope to be the whole origin, so you can leave scope out and it will default.

Fortunately, Jake had already given us the necessary code to register SW. All we have to do is combine them into line 15:
Here’s a refresher of the code:

Here’s the if statement to check if the browser supports service worker. If it doesn’t, it outputs nothing.

  if (navigator.serviceWorker) {
    navigator.serviceWorker.register('/sw.js');

A normal registration returns a promise so you can use it to get call backs if it was a success or failure:

    navigator.serviceWorker.register('/sw.js').then(function(reg) {
    console.log('yay');
    }).catch(function(err) {
    console.log('Boo!');
    });

Step 4: To see if there are any errors, it’s best to get used to pulling out the devtools in chrome.
To find dev tools, go to the 3 dots on the far right side next to all the extensions > more tools > Developer Tools.
There, you can find your hotkey to open up dev tools. Mine is Ctrl + Shift + I
devtools
Step 5: Once registered, test to see if it was successful by going over to localhost:8889 and enter registered in the test ID.
Step 6: You should see "Service worker successfully registered"

The way Jake registers SW

Jake decided not to wrap the code in a browser support check and just put the check in a single line.
With the registration returning a promise, he wants it to spit out a message to see if it failed or succeeded.

if (!navigator.serviceWorker) return;

navigator.serviceWorker.register('/sw.js').then(function(reg) {
    console.log('Registration worked!');
}).catch(function() {
    console.log('Registration failed');
});

If it succeeded, refresh again and in the console in the devtools should spit out: "Registration worked!"
Over at Test ID in localhost:8889, you should get the message: "Service worker successfully registered!" when you type in: registered.

Warning
I’ve noticed on the forums there were some issues getting this to be registered even when the code was entered correctly. Try the following:
1st: Check your bash. Restart if you see this error:
consoleERR1
Ctrl C and then turn it back on with npm run serve.
2nd: If your bash isn’t spitting errors, try to do a hard refresh of chrome.
Shift + refresh.
3rd: Try clearing the cache if the other two doesn’t work.

Dev tools Console Requests logs

After the success of Jake’s code, you will notice in the devtools console after a refresh it shows all the requests logs.
webconsole1

The scope restricts the pages it controls, but it will intercept any request made by these controlled pages regardless of the URL. You can change these requests and respond to it with something entirely different.
Service Worker is limited to HTTPS, because if it wasn’t encrypted, any user could intercept it and add/remove/modify the content.

3.5: The Service Worker Lifecycle

Jake let’s us know that we have to do 2 refreshes to see the results. When we made changes to the service worker, SW didn’t pick up that change. The steps that SW took when we registered it and why it took 2 refreshes.

Why it takes 2 refreshes to see Service Worker after registration.

  • website is open. We’ll call it Website_1

  • We register SW.

  • We hit refresh on the website_1 to send out requests and get a response.

  • new window client gets made. We’ll call it Website_2

  • Website_2 made a request off to the network and back.

  • Website_1 went away and Website_2.0 stays.
    Though if the response came back that the browser should save the resource to disk via download dialog, website_1 would have stayed. Since the response we got was just a page, the website_1 is gone.

  • The response was a page and website_1 is gone. The request went out for css, images, and also the new javascript. The registered Service Worker.

Important
Q: How come we didn’t see the request log after one refresh?
A: Because Service Worker only control pages when they’re completely loaded, and the page was loaded before the service worker existed.
  • any request by website_2 will bypass the service worker script.

  • When we refresh again, a new website client was made. We call it website_3 and website_2 is now gone.

  • Since Service Worker was up with website_2 (but not running), it is now running with website_3.

  • Any new requests will go through Service Worker.

Why changes to Service Worker doesn’t change until after you reopen the page.

Making changes to the service worker script is different. Jake shows us that when you made changes to the script, nothing happened after a refresh. The new version of the service worker won’t make any changes until all pages using the current version are gone. because it only wants one version of the website running at a given time. Such as native apps.

Q What does Native App mean?
A:

According to: http://searchsoftwarequality.techtarget.com/definition/native-application-native-app
A native application (native app) is an application program that has been developed for use on a particular platform or device.

Because native apps are written for a specific platform, they can interact with and take advantage of operating system features and other software that is typically installed on that platform. Because a native app is built for a particular device and its operating system, it has the ability to use device-specific hardware and software, meaning that native apps can take advantage of the latest technology available on mobile devices such as a global positioning system (GPS) and camera. This can be construed as an advantage for native apps over Web apps or mobile cloud apps. The term "native app" is often mentioned in the context of mobile computing because mobile applications have traditionally been written to work on a specific device platform. A native app is installed directly on a mobile device and developers create a separate app version for each mobile device. The native app may be stored on the mobile device out of the box, or it can be downloaded from a public or private app store and installed on the mobile device. Data associated with the native app is also stored on the device, although data can be stored remotely and accessed by the native app. Depending on the nature of the native app, Internet connectivity may not be required.

Here is what’s going on with Service Worker.

  • Service_Worker 1 looks for changes in resources and byte identical.

  • If yes, it becomes the new version. Service_Worker 2.

  • Service_Worker 2 doesn’t take control yet because Service_Worker 1 is still running with the website.

  • Service_Worker 2 is waiting for all pages using Service Worker 1 to close to ensure only one version of the website is running.

  • Once you close that page, the new website 3 will use Service Worker 2.

Testing changes to Service Worker

  • Head over to public>js>sw>Index.js

  • Modify the code to console.log("Hello World");.

  • Go over to the wittr app localhost:8888

  • One refresh you shouldn’t see any changes and only see the normal requests.

  • Close the page and pull up console. You will see "hello world" instead of the normal requests.

Note
SW uses the same update process as browsers such as chrome. Chrome downloads updates in the background but won’t take over until the browser closes and reopens again. chrome will let us know there’s an update ready when the icon on the top right changes color.

Intro to an upcoming lesson with cache.

Jake lets us know that in an upcoming lesson we will learn how to use the service worker to look for updates and then notify the user that there is an update available. The Service Worker will go through the browser’s cache just like all requests do. Jake recommends keeping the cache time on the service worker short. Jake recommends keeping the cache time zero on all service worker projects.

Caution
Jake lets us know that if you set the service Worker script to cache for more than a day, the browser will ignore it and set the cache to 24 hours. Does that mean the service worker not work after 24 hours? No, the update checks will bypass the browser cache if the service worker it has is over a day old.

3.6: Quiz: Enabling Service Worker Dev Tools

This lesson Jake wants us to install Chrome Canary. This course was taped in 2015 so most of the features are actually in the normal chrome. I’m going to skip this install.

3.7: Quiz: Service Worker Dev Tools

Here, Jake is giving us an overview of the Dev tools.

Dev Tools Console

  • First is the console. Any code that is run here will be run against the documents. Though, the Service Worker lives outside the documents. To get to the Service Worker script in console, there is a drop down menu, you can select service worker and type anything in console for that script.
    webconsole2

Dev Tools Sources

  • To find the debugging menu that Jake was playing around with go to sources tab. The UI is actually the same, but the navigator is hidden by default. Just press the arrow and the navigator will pop up.

  • Navigate to sw.js>localhost:8888>public/js/sw>index.js

  • While in index.js, Jake put in a breakpoint in our fetch event by pressing 2 on the side.

  • Refresh the page and notice that the script gets paused.

  • To unpause, just unclick the number and press the play button.

Dev Tools Service Worker

Service Worker has its very own panel. It’s not in the Resources like Jake has it, but it’s actually in the Application panel.
SW1

  • There you will see the link Unregister that will refetch the Service Worker from scratch.

  • Instead of tabs, we just get status information. Right now the status should be green. It should have the message "activated and is running"

Note
If you have a service worker waiting underneath it, it probably just means you made some changes. More on that on the quiz.

The Quiz to give you the waiting status

Step 1: Mike wants you to get the waiting status. First you need to ready the template if you want to.

git reset --hard
git checkout log-requests

Step 2: Make any changes to the sw.js file. A different console log. anything.
Step 3: Go over to the dev tools> application> service workers
Step 4: You should now see your green status for an active one and an orange status that says it’s waiting.
SW2
Step 5: After that, head over to the settings page: localhost:8889 and type in the Test-ID: sw-waiting
Step 6: You should see the message "Yey! There’s a service worker waiting!".

3.8: Quiz: Service Worker Dev Tools 2

This lesson, Mike wants us to get that new service worker active.

  • Mike reminds us that what we need to do is close the current pages that are using the old Service Worker.

  • When you reopen wittr, and reopen the Dev Tools>Application>Service Workers you will now notice that there is no more service worker waiting and there’s only an active one.

  • To confirm, go to settings page (localhost:8889) and type into the Test ID: sw-active

  • You should see the message "No service worker waiting! Yay!".

3.9: Service Worker Dev Tools Continued

Jake explains that having to reopen the page during development can be annoying.

You can do a hard refresh with Shift + refresh.
It will reload the page but bypasses the Service Worker. It will set the waiting as the active, but there’s actually an easier way.

Instead of doing Shift + refresh, you can just check the update on reload option in devtools.

SW3

The option will change the Service Worker life cycle to be developer friendly. Now that when you hit refresh, rather than just refreshing the page, it fetches a service worker and treats it as a new version whether it was changed or not and it will become active straight away. After that, the page refreshes.

Warning
This is only for developers. The user will be stuck with the old way of having to close the page and reopening it to get the new service worker.

3.10: Hijacking Requests

So far we’ve only seen requests go from page to SW, and then from SW’s fetch event to the internet through the HTTP cache.
Here, we’re going to catch the request when it accesses the Service Worker and respond ourselves without it going to the internet. This is actually an important step to going offline first.

Introducing hijacking with event.respondWith()

  • To get started, Jake wants you to go to the service worker script at public>js>sw>index.js.

  • in the fetch script, replace what’s inside with event.respondWith()

Note
event.respondWith tells the browser that we’re going to handle this request ourselves. Here’s more information on it here.
  • event.respondWith() takes in a response object or a promise that resolves with a response.

  • To create a response you just type in new response().

    • The first parameter is the body of the response. which can be a blob, a buffer, a string, or some other thing. Here’s a documentation of how we can use new Response()

  • In Jake’s example, he’s going to play with a string.

event.respondWith(
    new Response("Hello World")
    );
  • Once the hello world is entered, go over to the wittr page and refresh it. You will notice that it has been completely hijacked with the simple message: "Hello World".

Adding html to the hijacker

Jake edited the new Response string to have HTML, but it passed as plain text instead.
webconsole3

You can easily change it to HTML by setting the header as part of the new Response.

  • The second parameter of new Response is an object.

  • the header’s property takes an object of headers and values.

  • set the foo header to be bar like this: 'foo': 'bar'

new Response('Hello <b>World</b>', {
    headers: {'foo': 'bar'}
}
);

3.11: Quiz: Hijacking Requests 1 Quiz

Here we will be hijacking the requests the way Mike wants us to.

Hijack the website with a string and changing it to read in HTML.

Step 1: Mike wants us to prepare our template.

git reset --hard
git checkout task-custom-response

Step 2: Over at public>js>sw>Index.js Mike will have a todo in the fetch event.
Step 3: Mike wants us to make the event listener be able to read HTML. The HTML element can be anything as long as the class name is "a-winner-is-me".


code refresher:
the new Response

event.respondWith(
    new Response(
    // The new Response takes in two parameters.
    // The first parameter is the body so it can be a string
    // The second parameter can be an object)
    )

code refresher:
The basic bold html element string with a fubar class. This will be put into parameter # 1. Mike says it can be any HTML element as long as it has the class "a-winner-is-me".

<b class="fubar"> Hello World </b>

code refresher:

adding a new header object. This would go into parameter # 2

{ headers: {'foo': 'bar'} }

Note
We need to overwrite the old header name: Content-Type to read HTML.
The default is text/plain. We need to change it to text/HTML.

Step 4: Once you’re done with the code, Mike wants you to go to the webpage and refresh.
Step 5: The HTML of that page should be applied and see if the header changed to read HTML by going to devtools>network>response headers.
Step 6: Once you see the result, confirm it by going to the settings page (localhost:8889) and put in the test ID: html-response
Step 7: You should see the result: "Custom HTML response found! Yay!"

How Jake hijacked the request to read html

event.respondWith(
    new Response('Hello <b class="a-winner-is-me">world</b>', {
        headers: {'Content-Type': 'text/html'}}
    ))

With the new hijacking, you can still see hello world offline and every other network condition.

3.12: Hijacking Requests 2

Introducing fetch

Here we will go to the network for the response, but give something else that was requested using
fetch(url)
fetch let’s you make network requests and let’s you read the response. Here is how you write a fetch request:

fetch('/foo').then(function(response) {
    return response.json();
}).then(function(data) {
    console.log(data);
}).catch(function() {
    console.log('It failed');
})

Describing what’s going on with the fetch code:

fetch('foo') - it will return a promise
.then(function(response) - which will resolve to a response.
return response.json(); - then we will read the response’s JSON.
.then(function(data) { console.log(data) - Here is the results.
.catch(function() { console.log("It failed") - You can catch errors from either the request or reading the response.

back to our code event.respondWith()

Jake reminds us that event.respondWith takes either a response or a promise that resolves to a response.
Fetch returns a promise that resolves to a response. So fetch and event.respondWith() work together very well.
Here, we’re going to respond with a fetch for a gif image.

event.respondWith(
    fetch('/imgs/dr-evil.gif)

The fetch API performs a normal browser fetch. When this is inserted to our current SW script, it gave the gif response to everything.

3.13: Quiz: Hijacking Requests 2 Quiz

Mike wants us to give this gif response to only a particular request.

Step 1: To ready the template:

git reset --hard
git checkout gif-response

Step 2: Head over to public>js>sw>index.js and read the TODO:
Step 3: The task s to respond with a gif if the request URL ends with .jpg.

We will definitely need a fetch method to take a full request object as well as a URL

event.respondWith(
    fetch(event.request).then(function(response) {
    if () {
    return;
    }
    else {
    return;
    }
}).catch(function() {
    return;
    })
);
Note
The main thing we had to modify is the if statement and what it returns.
.then is for success and .catch is for failure

code refresher:
We will have to check for an url that ends with .jpg.

if (event.request.url.endsWith('.jpg'))
or
if (/.jpg$/.test(event.request.url))
Note
To learn more about RegExr, here’s a great link.

code refresher:
Here’s how to fetch the image to replace with:

fetch('url')

Step 4: Once you have it working, go over to the settings page (localhost:8889) and type in the test ID: gif-response
Step 5: You should see the message "Images are being intercepted!"

Here’s how it worked

Well unfortunately Jake didn’t give us the answer of how he did this, but the forums and slack helped me get it working.

  event.respondWith(
    fetch(event.request).then(function(response) {
      if (event.request.url.endsWith('.jpg')) {
        return fetch('/imgs/dr-evil.gif');
      }
    return response;
    }).catch(function() {
      return new Response("Nope! You broke something, idiot!");
    })
  );

My struggle on this

I struggled with this for a few hours trying to get it to work. My main issue was what to return.
* turns out return response fetch(url) didn’t work.
* return new Response(fetch(url)) didn’t work.
* and new Response 'url' didn’t work.

Boy did it get very frustrating. Turns out I didn’t really understand fetch(). I can’t find any good documentations about new Response() and fetch(), but to quote someone who understands it better:

Mario Ruiz on Slack:
this line return new Response(fetch('/imgs/dr-evil.gif')); should be something like

return fetch('/imgs/dr-evil.gif');

fetch already returning a response promise, is not necesarry to wrap it again into a Response object.

Then I remembered on my notes when I wrote:

event.respondWith takes either a response or a promise that resolves to a response. Fetch returns a promise that resolves to a response. So fetch and event.respondWith() work together very well.

event.respondWith(
    fetch('/imgs/dr-evil.gif)

So if I understand this correctly:
.fetch() is a response. And having them together in return response fetch() is repetitive. Okay, I think I can finally move on now..

3.14: Hijacking Requests 3

Instead of th image gif hijacking any jpg url like the previous lesson, Jake wants to give us a 404 page response. Also, we can give a message if the fetch fails or our network is offline. A bit of a refresher: The page can send a request then we can intercept and send to the network. Instead of sending the response back, we can look at it and send back something else.
=== The network fetch() for a 404 page.

self.addEventListener('fetch, function(event) {
    event.respondWith(
        fetch(event.request).then(function(response) {
            if (response.status === 404) {
                return new Response("Whoops, not found!");
            }
                return response;
        }).catch(function() {
                return new Response("Something went wrong");
    })
    );
});

What the code in the 404 page means

Step 1:
event.respondWith() We will respond with a network fetch for the request. This is what the browser would do anyway.
Step 2:
fetch(event.request) the fetch method will take a full request object as well as a URL. The fetch will return a promise.
Step 3:
.then(function(response) {} ) Since fetch returns a promise, we can attach a .then to get the result if it was successful.
Step 4:
Whatever we put inside .then(function(response) {here} is the callback. It will become the value for the promise.
Step 5:
if (response.status === 404) Here we can look at the response ourselves, and if the response is a 404 Not Found…​
Step 6:
return new Response We can respond with our own message.
Step 7:
return response Otherwise we can just return the response that was received.
Step 8:
.catch(function(response) {} ) .catch will be the fallback if there was a failure in the fetch or if the network is offline
Step 9:
Again with the .then, you can give it a return new Response("message");

3.15: Quiz: Hijacking Requests 3 Quiz

In this lesson, they us to chain two fetch requests together.

Step 1: First we have to ready our template.

git reset --hard
git checkout error-handling

Step 2: Instead of the new response being a custom text, Mike wants us to respond with the dr-evil.gif.
Step 3: We have to fetch from the network.

code refresher:

fetch("url")

Step 4: Then we have to navigate to a page that doesn’t exist within wittr and see if the image shows.
Step 5: Once you see it working, confirm at the settings page (localhost:8889) and enter the Test ID: gif-404
Step 6: You should see the message "Yay! 404 pages get gifs!"

The answer to giving the gif image to the 404 request

This one was easy if we reflect back to my revelation and rant in lesson 3.13
All we needed to do was add the fetch inside the if response.

return fetch('/imgs/dr-evil.gif');

3.16: Caching and Serving Assets

We have been doing simple texts and images, but it’s about time to actually respond with something useful. To do this we need to store the HTML, CSS, etc. There’s a special API for this called the cache API.

Special cache API

  • to Open a cache, type the following with the name of the cache. If we’ve never opened that cache name before, it will create one and return it.

caches.open('my-stuff').then(function(cache) { // } )
Note
A cache box contains requests and response pairs from any secure origin. We can use it to store fonts, scripts, images, etc from our own origin as well as elsewhere on the web.
  • to add cache items, type the following and pass in a request/URL and a response.

cache.put(request, response);

or

cache.addAll ([
'/foo',
'/bar'
])
Warning
the addAll with the arrays way is atomic. Which means if any of those in the arrays fail to cache, none of them will be added.
Note
addAll uses fetch under the hood, so requests will go via the browser cache.
  • to retrieve something out of the cache, we type in the following. It will only pass a request/URL. It will then return a promise for a matching response if one is found or null.

cache.match(request);

or

caches.match(request);
//will search in any cache starting with the oldest.

The install event

Now that we have somewhere to store our stuff, but now when do we store it? Jake shows that there’s another service worker event that will install the cache.

There are steps that the service worker install has to do

Step 1: We run the service worker the first time.
Step 2: The browser won’t let the new service worker take control pages until it’s install phase has been completed. We are in control of what that involves.
Step 3: We use the opportunity to get everything we need from the network and create a cache for the content.

Step 4: To create an install event, we make a new self.addEventListener.
Step 4: inside the install eventListener, we added in an event.waitUntil(). It let’s us signal the progress of the install.
Step 5: We then pass it a promise. If and when the promise resolves, the browser knows the install is complete. If the promise rejects, it knows the install failed, and should be discarded.

self.addEventListener('install', function(event) {
    event.waitUntil()

})

3.17: Quiz: Install and Cache Quiz

Here we are going to try and install and cache.

Step 1: We have to ready the template.

git reset --hard
git checkout task-install

Step 2: What Mike wants us to do is open a cache name 'wittr-static-v1' and cache the urls from urlsToCache.

code refresher:

#open a cache
caches.open('my-stuff').then(function(cache) { // }

#to add cache
cache.put(request, response);
or
cache.addALL (['x', 'y', 'z'])

Step 3: To verify if we’ve made the cache, head over to Dev Tools > application> Cache Storage> 'wittr-static-v1'
devtools2
Step 4: To confirm that it was a success, head over to the settings page (localhost:8889) and enter in the Test ID: install-cached
Step 5: You should see the message: "Yay! The cache is ready to go!"

How Jake installed the cache

This is how Jake created the caches.

 event.waitUntil(
    caches.open('wittr-static-v1').then(function(cache) {
    return cache.addAll([
    '/',
    'js/main.js',
    'css/main.css',
    'imgs/icon.png',
    'https://fonts.gstatic.com/s/roboto/v15/2UX7WLTfW3W8TclTUvlFyQ.woff',
    'https://fonts.gstatic.com/s/roboto/v15/d-6IYplOFocCacKzxwXSOD8E0i7KZn-EPnyo3HZu7kw.woff'
  ]);
    })

 )

an alternative way to use the array is just to call the variable *urlsToCache* like this:

return cache.allAll(urlsToCache)

3.18: Quiz: Cache Response Quiz

Now that we have created the caches, we should be able to use it now.
Mike tells us that they haven’t taught us the code on how to respond with a cache entry, but he did remind us that they did show us how to get things out of the cache.match and event.respondwith().

code refresher:

cache.match(request);

or

caches.match(request);
//will search in any cache starting with the oldest.

In this lesson, we are going to put these together to try and use a cache. Here’s a handy website talking about the Cache API and more information on this subject: caching files with service worker

Step 1: Ready the template

git reset --hard
git checkout task-cache-response

Step 2: All the work is going to be at the usual service worker script in public>js>sw>index.js
Step 3: The TODO is to respond with an entry from the cache if there is one, if there isn’t, we will have to fetch it from the network.

Mike’s hint: We have to call with event.respondWith() synchronously. We cannot call it with a promise handler because it’s already too late to do that.

code refresher:

    event.respondWith(
        x(event.request).then(function(response) {
            if
            else
        })

code refresher:

event.request is the original request
fetch(event.request)

Step 4: Once you have the code going, test to see if it works by going to the settings page (localhost:8889) and try offline mode to see if the website is still up.
Step 5: While still in the setting’s page, select back to online and confirm that it’s working by typing in the Test ID: cache-served.
Step 6: You should see the message "Yay! Cached responses are being returned!"

How Jake created a cache and used it

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

or

event.respondWith(
    caches.match(event.request).then(function(response) {
        return response || fetch(event.request)
    }))

With this code, we still have content during offline mode. Most of the site is still up, but not the images aren’t in the cache.

Jake gives us a new TO-DO list from here to a full offline first app.

  • Unobtrusive app updates

  • Get the user to use the latest version

  • Continually update cache of posts

  • Cache photos

  • Cache avatars

3.19: Updating the Static Cache

Jake wants us to disable the force update on reload option in dev tools so we can troubleshoot our app the way a user would experience it.
When Jake made changes to the css, the website did not update when he refreshed the page. Only when he did a Shift + refresh it worked because it bypassed the service worker.
When Jake went over to the servie worker panel, the changes in the css didn’t show as a new service worker update.
Here we will try to get the service worker to pick up any changes we’ve made so the user can receive the changes as soon as possible without any obstruction.

To get the CSS to change

Step 1: We have to change the service worker, and any change to the service worker is a differnt service worker version. Service Worker 2+ Step 2: Service Worker 2 will get its own install event. It will fetch the javascript, css, etc and put it cache 1.
Step 3: It’s recommended to make a new cache so we won’t disrupt the old cache. To create a new cache, we just have to change the name of the cache entry.
Step 4: Once Service Worker 1 gets released, we delete the old cache and the new page load will use the new cache. Step 5: That’s it. Thew new website will get the new css. Though, all we have to do is edit the service worker to trigger any change in the css.

To delete the old cache

Jake introduces us to a new event listener.

self.addEventListener('activate', function(event) {
//
});

The activate event runs when the new Service Worker becomes active and when it is ready to control pages, and the previous service worker is gone.
Inside the activate event is the best time to delete the old cache. Just like the install event, you can use the waituntil() to signal the length of the process. While activating, the browser will queue of a service worker events such as fetch.
By the time your service worker receives its first fetch, you know you have the caches how you want them.

To delete a cache…​

caches.delete(cacheName);

To get the names of all caches, use

caches.keys();

Both of these methods return promises

3.20: Quiz: Update Your CSS Quiz

Mike wants us to edit the CSS without it disrupting the currently running version of the site.

Step 1: Ready the template

git reset --hard
git checkout task-handling-updates

Step 2: Make sure the force update on reload in dev tools is disabled.
Step 3: The TODO wants us to change the theme of the website in public>scss>theme.scss . You can change $primary-color: to a different color or switch theme with the commented out code.
Step 4: Once the SCSS has been changed, we need to update the Service Worker’s cache version. Head on over to the service worker script public>js>sw>index.js
Step 5: While still in the index.js, we need to remove the old cache. Step 6: To check if it’s working, refresh the wittr page and in dev tools should see a new service worker waiting, but do not activate it yet.
Step 7: Go over to the settings page (localhost:8889) and type in the Test ID: new-cache-ready
Step 8: You should see the message "Yay! The new cache is ready, but isn’t disrupting current pages"

3.21: Quiz: Update Your CSS 2

To finish this off…​

With the changes in the previous lesson, we should head back to the wittr’s dev tools and activate the service worker.

  • To activate it, we just need to do Shift - Refresh or reopen the page.
    We should see the primary color has been changed.

  • To confirm, go to the settings page and enter the Test ID: new-cache-used

  • You should see the message "Yay! You safely updated the CSS!"

What changes did Jake do to his Service Worker

from caches.open('wittr-static-v1') to caches.open('wittr-static-v2')

That’s the easy one, but how did he delete the old cache?

in the activate eventListener he puts in

event.waitUntil(
    caches.delete('wittr-static-v1')
  );

Deleting the cacheName manually is right, but there’s a better way to do this.
Jake suggests maintaining a safe list of cache names to keep and remove the others. Here’s how he did it…​

Step 1: store the name as staticCacheName = 'wittr-static-v2. Step 2: Head over to caches.open and replace 'wittr-static-v2' to the variable staticCacheName. It will look like this:

caches.open(staticCacheName)

Step 3: Head over to the waitUntil() in activate event listener.
Step 4: We’re going to get all the cache names that exist with caches.keys().then(function(cacheNames){} It will return a promise.
Step 5 We will filter the cache names with cacheNames.filter(function(cacheName) {}
Step 6 Inside that we are interested in the cache name that begins with "wittr-" and it must not the name of the static cache.

Note
It’s better to search for caches with wittr-. That way we don’t accidentally delete caches from other apps that might be running on the same origin
return cacheName.startsWith('wittr-') &&
    cacheName != staticCacheName

That wil give us the list of all the caches that we don’t need anymore.

Step 7: Now we will map the collected cacheNames, and then delete them.

.map(function(cacheName) {
    return cache.delete(cacheName}

Step 8: Then wraps the check and delete into a Promise.all() so we wait for the completion of all the promises.

Here’s how they all look together.

var staticCacheName = 'wittr-static-v2'

self.addEventListener('activate', function(event) {
    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(
                cachesNames.filter(function(cacheName) {
                    return cacheName.startsWith('wittr-') &&
                        cache != staticCacheName;
                    }).map(function(cacheName) {
                        return cache.delete(cacheName);
                    })
                );
            })
        );
    });

Okay, so I had to go look up .map.
What map does is create a new array with the results.


Warning
Okay I am absolutely lost at this point. Okay here on Jake talks about cache time which I have absolutely no idea what he’s talking about. I tried to find some information on cache age but the nearest thing I found is this. Hopefully I can find more information and understand what Jake is talking about here. I’m also confused by version number being generated. A build script will automatically update the service worker’s URL to the new version? I don’t know. Maybe I’ll update this with more information with what Jake is talking about. I’m just going to continue on with the lesson anyway.

Here we’ve checked off one item in the TO-DO list

✔ Unobtrusive app updates
□ Get the user to use the latest version
□ Continually update cache of posts
□ Cache photos
□ Cache avatars

3.22: Adding UX to the Update Process

Jake reminds us that we want our users to get the latest update as soon as possible. This lesson we will give the user a button to either ignore the update or refresh the page with the new version.

Now in order to do that, we have to use an API that gives us insight into the service worker life cycle.

The service Worker properties and methods

When we register a service worker, it returns a promise. That promise fulfills with a service worker registration object.
The object has properties and methods relating to the service worker registration.

We get 2 methods:

  • reg.unregister(); - unregister the service worker

  • reg.update(); - programatically trigger an update.

We also get 3 properties:

  • reg.installing; - Means it’s installing but it may be thrown away if the install fails.

  • reg.waiting; - we know there’s an updated serviceWorker ready and waiting to take over.

  • reg.active;

These will point to a serviceWorker object or be null. They will give you insight of the ServiceWorker lifecycle and they also map directly to the dev tools view we’ve been working with so far.

The dev tools view is actually just looking at these registration objects.

The registration object will give us an event when a new update is found.

reg.addEventListener('updatefound', function() {
// reg.installing has changed
}

When updatefound is running, .installing becomes a new worker.


In the ServiceWorker object itself, you can look at their state.

var sw = reg.installing;
console.log(sw.state);  // logs "installing" but not yet completed.

The state can also be:

  • installed - installation is completed successfully but not yet activated.

  • activating - The activate event has been fired, but not yet completed.

  • activated - The service worker is ready to receive fetch events.

  • redundant - The service worker has been thrown away. This happens if it failed to install or has been taken over by a newer service worker.

The SW fires a new event 'statechange' whenever the value of the state property changes.

sw.addEventListener('statechange', function() {
// sw.state has changed
});

The service Worker that controls the page

navigator.serviceWorker.controller

To tell the user when there’s an update is ready, but since the serviceWorker update happens in the background, we need a way to look at the state of things when the page loads. We should actually always be listening for future changes.

We need to look at the state of things when the page loads.

if (!navigator.serviceWorker.controller) {
// this page didn't load using a service worker so instead they loaded the content from the network.
}

otherwise we need to look at the registration.

if (reg.waiting) {
// there's an update ready and we tell the user.
}

otherwise if there isn’t an installing worker, there’s an update in progress:

if (reg.installing) {
// there's an update in progress... buuuuut the update might fail so we have to listen to the state change with:
reg.installing.addEventListener('statechange', function() {
// and track it with this till it reaches an installed state:
    if (this.state == 'installed') {
    // Here we would tell the user when it reaches the installed state.
    }
})
}

otherwise we listen to the updatefound event to track the state of the installing worker until it reaches the installed state so we can tell the user.

reg.addEventListener('updatefound', function() {
    reg.installing.addEventListener('statechange', function() {
        if (this.state == 'installed') {
        // we tell the user about the update.
        }
    });

});

All of these is how we tell the user about the update whether they’re already there, in progress, or start some time later.

3.23: Quiz: Adding UX Quiz

Oh boy, the previous lesson was a huge one. Mike wants us to tell the user that there’s an update available.

Step 1: Ready the template:

git reset --hard
git checkout task-update-notify

Step 2: We will be editing a different file this time. Head to public>js>main>IndexController.js
Step 3: There we will see the current To Do. Jake had already written us a method updateReady.

IndexController.prottype._updateReady = function() {
    var toast = this.toastsView.show("New Version available", {
    buttons: ['whatever']
    });
};

We need to call updateReady to show a notification to the user. Our job is to call it at the correct time.

Step 4: After we finished coding, we have to unregister our SW in the Dev Tools and refresh the page. That will reCache the javascript.
Step 5: After you’ve unregistered, go over to ServiceWorker public>js>sw>index.js and make a simple change such as adding comment.
Step 6: Then head over to the wittr app and refresh the page. You should see the notification of an updated version.
Step 7: To confirm that it was completed, go over to the settings page (localhost:8889) and type in the Test ID: update-notify.
Step 8: You should see the message "Yay! There are notifications!"

The way Jake created a notification of changes.

These are all inside the navigator.serviceWorker.register.


First we need to check if the ServiceWorker is falsy and if so, we bail. The user already has the latest version if it wasn’t loaded via a service worker.

if(!navigator.serviceWorker.controller) {
return;
}

Now, if there’s a worker waiting, we trigger the notification and return.

if (reg.waiting) {
    indexController._updateReady();
    return;
}

Now, if there’s a worker installing, we should listen to the state change by using the if statement. Jake wanted to save some time so he created the method to be used. Be sure you put this in its own and not with service worker registration.

IndexController.prototype._trackInstalling = function() {
  reg.installing.addEventListener('statechange', function() {
    if (this.state == 'installed') {
      indexController._updateReady()
    }
  })
}

We will call trackInstalling rather than retyping that whole thing again.

if (reg.installing) {
    indexController._trackInstalling(reg.installing);
    return;
}

With the new method trackInstalling, we take the worker and listen to its state change event.


If there is an update, we will use trackInstalling again using reg.installing.

reg.addEventListener('updatefound', function() {
    indexController._trackInstalling(reg.installing);
    return;
})

3.24: Trigger an Update

We got the notification, but we need the user to be able to press the button to update the page, delete the old service worker, and refresh the page using the newest cache.

To achieve this, we get to use 3 new components.

  • self.skipWaiting() - a service worker can call this while it’s waiting or installing. It means that it shouldn’t queue behind another service worker. In other words: it should take over straight away.
    We want to use self.skipWaiting() when the user hits the refresh button in the update notification.

Now there’s a new question: How do we send the signal from the page to the waiting service worker?


The webpage can send messages to any service worker using postMessage.

from a page:

reg.installing.postMessage({foo: 'bar'})

in the service worker:

self.addEventListener('message', function(event) {
    event.data; // {foo: 'bar'}
}

With the postMessage and message eventListener working together the user can now click the refresh button it will send a message to our service worker telling it to call skipWaiting.


The page gets an event when its value changes meaning that a new service worker has taken over. This is the signal that we should reload the page.

navigator.serviceWorker.addEventListener('controllerchange', function() {
// navigator.serviceWorker.controller has changed
}

3.25: Quiz: Triggering an Update Quiz

Now we need to make that button to update the page.

Step 1: Ready the template

git reset --hard
git checkout task-update-reload

Step 2: head over to public>js>main>indexController.js
Step 3: Mike reminds us that there’s a method called \_updateReady which is the message that gets shown when there’s an update ready.
Step 4: In the same \_updateReady method we have to control what pressing the 'refresh' button will do.
There we will have to send a message to the new service worker and tell it to take control of pages immediately.
Step 5: back in public>main>js>sw>index.js there’s a different TODO at the very bottom. There we can listen for the message to take over page control.
Step 6: now we head back to indexController.js there’s a different TODO near the top. There we need to listen for the pages controlling service worker changing and using that as a signal to reload the page.
Step 7: Head over to wittr app’s Dev Tools and unregister the SW.
Step 8: Make a change in index.js. maybe a comment. anything.

The following is a bit tricky so you need to do these carefully

Step 9: Now you can refresh the wittr app page. You should get a notification to refresh or dismiss. Make sure you don’t click the buttons yet. Keep in mind you have 8 seconds to finish the following steps.
Step 10: If you want to confirm the changes, head over to the settings page (localhost:8889) and type in Test ID update-reload. You should see a loading image with nothing yet.
Step 11: Quickly go back to the wittr app and hit refresh.
Step 12: You should see the message "Yay! The page reloaded!"

How Jake made the button refresh

Inside indexController.js, the function when someone presses the 'refresh' button, we use the worker to send a post message. We use an "action" method to communicate with each other. The action will give the string 'skipWaiting'.

    worker.postMessage({
      action: 'skipWaiting'
    })

Inside sw’s script index.js we listen to see if the refresh button was pressed and the action: 'skipWaiting' was called.

self.addEventListener('message', function(event) {
  if (event.data.action == 'skipWaiting') {
    self.skipWaiting();
  }
})

Back inside indexController.js, we have a different listener that listens to see if there’s a new service worker installed so we reload the webpage.

  navigator.serviceWorker.addEventListener('controllerchange', function() {
    window.location.reload();
  })

Now that this is complete, we can check off another on the list: ✔ Unobtrusive app updates ✔ Get the user to use the latest version □ Continually update cache of posts □ Cache photos □ Cache avatars

3.26: Quiz: Caching the Page Skeleton

In this lesson, we will swap out the root page for a skeleton page.

Step 1: Ready the template

git reset --hard
git checkout task-page-skeleton

Step 2: Head over to the SW file public>js>sw>index.js Step 3: The actual TODO: is to change which page is being served into the cache. Right now is the root page. We want a page skeleton to be fetched when the root page is requested.
Step 4: Once you’re done, refresh the wittr app page and you will see a new service worker along with the notificate of a new update.
Step 5: To see if the changes worked, right click ont he page to view source on the root page. The source should be small.
Step 6: To confirm that everything was done correctly, go over to the settings page (localhost:8889) and in the Test ID type: serve-skeleton.
Step 8: You should see the message "Yay! The page skeleton is being served!"

How Jake changed the root page to a skeleton page

Step 1: The first thing he did was manually change the / to /skeleton inside the open cache function.
Step 2: Jake up’d the version of SW.
Step 3: Now for the way of changing the URL when requested…​.

This part is completely new to the lesson. Inside the new fetch event listener, he created a new variable that requests the new URL with url().

var requestUrl = new URL(event.request.url);

Next he checks to see if the request origin is the same as the current origin using the variable that was created. Jake reminds us that SW handles requests for other origins too, so we would need to be more specific. In this case we want to intercept root requests for the same origin.

if (requestUrl.origin === location.origin) { }

Now if we need to check if the response pathname we got was the root using the variable that was created.

if (requestUrl.pathname === '/') { }

If the response is the root we want to respond with the skeleton straight from the cache. We don’t need to go to the network as a fallback since the skeleton cache was part of the install step.

event.respondWith(caches.match('/skeleton'));
return;

Here’s how it looks all together:

self.addEventListener('fetch', function(event) {
var requestUrl = new URL(event.request.url);

if (requestUrl.origin === location.origin) {
  if (requestUrl.pathname === '/') {
    event.respondWith(caches.match('/skeleton'));
    return;
  }
}