14 minutes read
In the last installment we learned how to run JavaScripts over different threads by using WebWorkers. This time we’ll go a few steps further and learn how to use ServiceWorkers to achieve not only separation of code but also to build up client-side proxies. These proxies are the most important building blocks of so-called Progressive Web Apps as they allow web apps to function under rough conditions like slow bandwidths or broken connections. ServiceWorkers can also be used for push-based services or background synchronization as they run completely detached from the Browser DOM. A ServiceWorker is basically a SharedWorker with super-powers. However, the ServiceWorker API and its lifecycle management are rather complex but at least not as problematic as the infamous AppCache. The demo app we’re going to build is in many ways similar to the one from the previous article. Nevertheless some of the components have changed and the internal logic is very different from the last article. This is how our app will look like. It’s a typical master-detail-view application that displays customer data when we click on a row.
The sources are on GitHub and the demo app can be found here.
ServiceWorkers overview
A ServiceWorker is a script that runs separated from the Browser DOM without any user interaction and communicates only via message passing. It’s a Promise-reliant, client-side, programmable network proxy that can be used to alter the handling of requests sent from own web pages. To use ServiceWorkers we have to register them in the first place. After the successful registration the browser running it will proceed to the next lifecycle step and install the ServiceWorker. In most cases you’d like to cache static assets like CSS & JS-files or images during the installation. The installation itself is a critical step because any errors would render the ServiceWorker unusable. For example, if some of the assets aren’t available (404-er error) the installation would fail and the ServiceWorker won’t activate. The installation counts as successful if and only if there are no errors whatsoever. The opposite of this strict rule is that when your ServiceWorker starts you can be sure that all of your static assets are available from the cache. After a successful installation the ServiceWorker activates. At this step we usually cleanup old cache entries. After the activation the ServiceWorker will take care of all pages under its scope which we’ve defined during the registration.
Well, I assume that this short intro is a reason enough to simply stop reading and go to some other resource. Therefore, let me try to describe the above mess more visually:
- At the initial page load there’s no ServiceWorker available and a web page has to kick-off its installation.
- If something goes wrong we make a full stop.
- If our installation succeeds we’re then ‘officially’ activated and able to clean up old caches.
- Now, in the ‘activated mode’ we simply wait for requests or messages to do our work (for example, by serving cached images or CSS)
- If we’re without work for some time the browser will terminate our process but it will also reactivate it later when a request arrives.
The sum of all this is: ServiceWorkers are event-oriented. Events bring ServiceWorkers into existence, they keep them alive, and terminate them too. This is also the reason why the API is very imperative with all those addEventListener, waitUntil, CacheStorage.open, Cache.match calls. Unlike its predecessor technologies like Google Gears or AppCache, that were more declarative, ServiceWorkers are imperative and let the developer explicitly decide what should be done and how. And just like any other new technology ServiceWorkers impose some additional restrictions too:
- They can be terminated depending on their usage (we talked about its event-driven nature in the above paragraph)
- They demand proper HTTPS connections with valid certificates (yes, no self-signed certificates allowed, not even when doing local tests)
- They’re completely independent from any web page. They don’t even need pages to run.
- They can be updated.
- They must reside on the same origin as the web page that downloaded them. But there’s also an exception to this rule: importScripts
- The scope defined by a ServiceWorker must be within the scope of the web page that loaded it.
Now, I’m sure that the sum of these restrictions looks very developer-unfriendly but just think for a second: You’ve just installed a piece of software that can grab your connections and change responses at will. Would you really like to run such things without any security measures?
Also, it’s not really impossible to run your development code locally with self-signed certificates. Chrome users could simply start their browsers with the option –ignore-certificate-errors. You’d get a warning upon start but you’re at least allowed to test your ServiceWorkers locally with some non-verified certificate. As an alternative the GitHub Pages are a nice way to host ServiceWorkers as they’re served via HTTPS. The above demo application is one of them.
Using ServiceWorkers
The app we’re gonna build will comprise of three Angular 2 components, a single ServiceWorker and a simple JSON-based storage containing tabular data. The structure is a follows:
- We’re building a list of our customers that’s being shown in a master-detail view.
- Each time we click on a row some details about this particular customer will be shown in the Info Component.
- Our customer’s data resides in a JSON-file but one can easily replace it with some real API. However, take care of providing data via HTTPS and a valid certificate!
- Each time a resource is requested, for example a customer’s picture or CSS, our ServiceWorker will kick-in and deliver a cached version of it, if available.
- If there’s no cache-entry the original resource will be delivered but at the same time it’ll be cached.
And this is what happens when you open the Browser Console. The debug messages in blue are my own console.logs, not from the ServiceWorker API.
We see that we first load customers.json file and cache it immediately. Then, after we’ve clicked a row, the ServiceWorker caches a PNG-file to be served later. Now if we completely reload the page this output will be shown:
Our ServiceWorker is now delivering the complete app from its cache! There are many files not shown previously because the ServiceWorker didn’t exist at the first page load. But after the page has installed and activated it we’re suddenly capable of serving not only customer.json and PNG’s but also the JS/CSS & Images belonging to the app itself. How’s this possible? The answer lies in the installation step as this is the place where we define everything that should be cached from the very beginning, no matter if we’re going to request it any time soon. Later we’ll see in greater detail how to pre-cache such assets. Now let’s check the status of our ServiceWroker by opening the Application Tab.
We see the details about the script and its current status. Also, we’re able to simulate missing connectivity by activating the Offline-checkbox. Now let’s look what the Network output has to say about the ServiceWorker’s presence:
Yes, all of our scripts and assets have been served by the ServiceWorker. No server requests at all! This is the building block of the so-called ‘Progressive Web Apps’ as they can run even without a network connection. No dinosaurs anymore.
Writing ServiceWorkers
Our ServiceWorker is a single TypeScript file located in src/sw/my-sw.ts. Depending on the use-case we’re able to write implementations that can take care of different aspects like:
- Serving static assets: CSS, images etc.
- Pre-caching larger resources which’ll be used at some later point in time.
- Explicit storing of certain resources for later reuse like videos, music, or documents.
- Real-time Push notifications.
- Background synchronization.
For more details and examples I’d recommend this page.
We begin our source code by defining what the local cache should contain after the installation has completed successfully.
We define an array that contains several resources belonging to our site’s scope. Then we define a listener for the install event that’ll be waiting for the cache to complete. The cache object is of type CacheStorage that returns a Promise which resolves to the Cache object with the given name. In this cache we then add all of our resources from the above array. The awkward syntax I’m using here is because I had problems with the original API definition of addAll that didn’t want to accept my raw array.
I’m still not sure why TypeScript doesn’t accept the plain string-array but maybe some of you out there know the solution. However, by casting the Cache object and the array with Lodash I was finally able to pass the needed data.
After the installation succeeds we’re allowed to go to the next step and activate the worker. Here we’re going to remove any old cache entries:
We take our both caches, local and runtime, and delete all entries we can find in any of them. To find out the entries for deletion we iterate over the known cache-keys which are either the names of our local assets or previously serviced requests. All of those entries will end up in the array named cachesToDelete that we’ll iterate over inside an array of Promises. Ultimately, our ServiceWorker will successfully claim to be the active worker for the page.
Until now we’ve succeeded in executing these steps:
- Open local Cache and put all static assets into it.
- Cleanup both of the caches (Local/Runtime) by removing any known entries.
- Claim to be the one and only active worker for the page.
What’s left is the most interesting part: dealing with fetch events. This is the place where we can intercept requests, change responses or even suppress them. No wonder, ServiceWorkers demand usage of valid certificates and HTTPS.
At the beginning of our fetch listener we constrain our ServiceWorker to deal only with requests targeting the own domain as we’re not interested in any cross-origin calls. Then we define that we’ll respond either with a cached response if we find it in our cache or we’ll first cache the network response and then forward it to the client. This is why we utilize a second cache, RUNTIME_CACHE. Also, like in previous steps, everything we do is packed in Promises. If you’re new to them then I’d recommend you this guide.
Powering Angular 2 Apps with ServiceWorkers
I think I’ve shown you enough to convince you that ServiceWorkers are really powerful engines but now we’d surely like to see them working in a some more realistic scenario. Our app, although very simple, provides such an environment where the ServiceWorker gets registered. In src/app/components/my-app/my-app.component.ts we have this method:
Given a browser that supports the ServiceWorker API we can directly call the register function and provide it the location of our ServiceWorker’s script. The registration object returned gives back some useful information about the ServiceWorker’s scope. We can also provide additional options like setting the scope with: { scope: ‘scope/sub-scope/my-scope‘ }.
We then instantiate our CustomersClient and define an event handler for the button click which will initiate the loading of JSON-based data from CustomersClient.
Our customer’s data is being delivered as an Observable<JSON>. Out of this stream we take the Customers property and reference it in this.customersList variable. We then let Angular know that it’s time to update the views of the component and its children by calling the ChangeDetector via this.cd.markForCheck(). And because we’re building a master-detail-view application we also take care of row click events:
The first two lines in this method are for randomizing the images shown in the Info Component. The original images have been generated with the robohash.org API. On each click the data from the current row will be transferred into a ICustomerData object that contains all the original data from customer.json plus the picture field whose PNG-Name depends on the current random value. By using this little trick we continuously change images on each row click. And every time we request an image the ServiceWorker will check if it can serve a response from its cache. That’s why we see debug logs like these here:
The Info Component is just a plain Presentational Component with a simple template and a single Input() parameter. We also use the standard Angular2 ngIf directive to switch on/off customer’s info card structure.
The Info Component class structure is very simple and the important part of this definition is the Input() property data which is of type ICustomerData.
The Customers Component is similar to the one defined in the previous article. It’s basically a table based on jquery.dataTables plugin that receives our customers.json and constructs a proper tabular UI.
Building the App, building the ServiceWorker
To make the build process easier and quicker we’re using WebPack and some of its plugins. All the configuration files are located in config folder. To use webpack’s build mechanism one doesn’t have to call webpack itself as there are already several NPM scripts available in package.json.
- To build a new app just type in npm run build:prod
- For a new ServiceWorker build run npm run build:sw
Also there are several scripts to start a local webserver.
- One is webpack’s own dev-server that can be run with npm run start:hmr
- Also you can run a standalone http server with npm run server:prod
- My own preferred way to run a server locally is with Hapi.js by executing ts-node .\server.ts
- And of course there’s a really nice option of deploying everything to a GitHub Page and testing it from there.
Conclusion
ServiceWorkers are new, powerful machines designed to help us overcome age-old problems with the web. Low bandwidths, broken connections, push messages, and caching (oh yes, caching & naming!) are no small problems. They actually decide if our future users are still going to prefer walled garden apps over web apps. If we’re able to deliver perfect user experience without broken-connection dinosaurs and stale cache, supported by push-messages and background-sync then the future of web-app will be bright.
2 thoughts on “Intro to Angular 2 – Part 6 – ServiceWorkers”
Your scroll to top button position is simply annoying/!!!
I’m browsing with mobile landscape mode
Ty
Hi Nate,
Thanks for the info. And sorry about that.
The “back to top” button is now deactivated.
Kind regards,
Harris