📜 ⬆️ ⬇️

Service Workers. Instructions for use



The number of Internet access from mobile devices is growing annually by 2-4% per year. The quality of communication does not keep pace with such a pace. As a result, even the best web application will provide a terrible experience if the user cannot download it.

The problem is that there is still no good mechanism for managing the cache of resources and the results of network requests. In my article I want to tell how Service Worker (SW) can help in this task. I will explain in the recipe format - which elements and in what proportion to mix, in order to get the desired result, based on the task and requirements.

Before SW, the problem of working in offline mode was solved by another API, AppCache. However, along with AppCache, it was a fact that it works fine in single-page applications, but is not well suited for multi-page sites. SW were designed to avoid these problems.
')

What is a Service Worker?


First, it is a script that the browser launches in the background, separate from the page, opening the door to features that do not require a web page or user interaction. Today, they perform such functions as push-notifications and background synchronization, in the future SW will support other things. Their key feature is the ability to intercept and process network requests, including software management of response caching.

Secondly, SW runs in a worker context, so it does not have access to the DOM and runs in a stream separate from the main JavaScript thread that controls your application, and therefore does not block it. It is designed to be completely asynchronous, so you cannot use synchronous APIs (XHR and LocalStorage) in SW.

Thirdly, for security reasons, SWs work only over HTTPS, since it is extremely dangerous to give outsiders the ability to change network requests.

What should I cache?


To make the application more responsive, we need to cache all static files:


Why can't we use LocalStorage for this situation?


Everything is very simple. LocalStorage is a synchronous API, has a limit of 5MB and allows storing only strings.

SW is getting better with it: it is asynchronous, is a proxy for requests, which allows Eddie Osmani to process and cache any request and according to the article Offline Storage for Progressive Web Apps :


I already like the Service Worker. How to use it?


Below I will talk about SW cooking recipes for creating responsive and understandable applications.

Preparations for the preparation of Service Workers


To write our own SW we need:


All that needs to be done is to connect index.js in index.html, in which the file sw.js will be registered

//  ,     Service Worker API. if ('serviceWorker' in navigator) { //      . navigator.serviceWorker.register('./sw.js') .then(() => navigator.serviceWorker.ready.then((worker) => { worker.sync.register('syncdata'); })) .catch((err) => console.log(err)); } 

In the sw.js file, we only need to define the basic events that SW will respond to.

 self.addEventListener('install', (event) => { console.log(''); }); self.addEventListener('activate', (event) => { console.log(''); }); self.addEventListener('fetch', (event) => { console.log('   '); }); 

You can find out more about lifecycle for SW in this article.

Recipe number 1 - Network or cache


Suitable for media sites, where a lot of media content. Pictures and videos can be given for a long time, our task is to cache them. With subsequent requests to the server, we will give the data from the cache. We mean that the data may already be irrelevant, for us the main thing here is to save the user from waiting for the files to load.


Decision

This option is suitable if the download speed of the content is your priority, but I would like to show the most relevant data.

The mechanism of operation is the following: there is a request for a resource with a time limit, for example 400ms, if the data has not been received during this time, we give them from the cache.

The SW in this recipe tries to get the most up-to-date content from the web, but if the request takes too long, the data will be taken from the cache. This problem can be solved by posting a timeout to the request.

 const CACHE = 'network-or-cache-v1'; const timeout = 400; //         (). self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE).then((cache) => cache.addAll([ '/img/background' ]) )); }); //   fetch,    ,   ,    timeout. self.addEventListener('fetch', (event) => { event.respondWith(fromNetwork(event.request, timeout) .catch((err) => { console.log(`Error: ${err.message()}`); return fromCache(event.request); })); }); // - . function fromNetwork(request, timeout) { return new Promise((fulfill, reject) => { var timeoutId = setTimeout(reject, timeout); fetch(request).then((response) => { clearTimeout(timeoutId); fulfill(response); }, reject); }); } function fromCache(request) { //     (CacheStorage API),    . //  ,       Promise  ,    `undefined` return caches.open(CACHE).then((cache) => cache.match(request).then((matching) => matching || Promise.reject('no-match') )); } 

Recipe number 2 - Cache only


The perfect recipe for landing pages, the task of which is to demonstrate the product to the user and thereby keep his attention on the site. Slow loading of content with a bad connection in this case is simply unacceptable, so the priority of this recipe is to return data from the cache for any connection. The exception is the first query and cache flush. The disadvantage of this method is that if you change the content, then the users will have a change after the cache becomes invalid. By default, SWs do re-registration 24 hours after installation.


Decision

All we do is registering SW and adding all our static resources to the cache; on subsequent calls to resources, the SW will always respond with data from the cache.

 const CACHE = 'cache-only-v1'; //         (). self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE).then((cache) => { return cache.addAll([ '/img/background' ]); }) ); }); //     ( fetch),     . self.addEventListener('fetch', (event) => event.respondWith(fromCache(event.request)); ); function fromCache(request) { return caches.open(CACHE).then((cache) => cache.match(request) .then((matching) => matching || Promise.reject('no-match')) ); } 

Recipe number 3 - Cache and update


This recipe solves the problem of the relevance of the data, which was not in the recipe number 2.
In other words, we will get updated content, but with a delay until the next page load.


Decision

As in the previous version, in this recipe, the SW first responds from the cache to deliver quick answers, but it also updates the cache data from the network.

 const CACHE = 'cache-and-update-v1'; //         (). self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE).then((cache) => cache.addAll(['/img/background'])) ); }); //   fetch,   ,         self.addEventListener('fetch', function(event) { //   `respondWith()`,        . event.respondWith(fromCache(event.request)); // `waitUntil()` ,     worker'a     . event.waitUntil(update(event.request)); }); function fromCache(request) { return caches.open(CACHE).then((cache) => cache.match(request).then((matching) => matching || Promise.reject('no-match') )); } function update(request) { return caches.open(CACHE).then((cache) => fetch(request).then((response) => cache.put(request, response) ) ); } 

Recipe number 4 - Cache, update and refresh


Recipe extension number 3. In this solution, we update the content in the background, but we can always indicate to the user that the data on the page has changed. An example is the creation of applications that edit content in the background. So, you read the article on the news site and get a notification that the data on the page has been updated and more recent information has appeared.




Decision

The recipe allows SW to respond from the cache in order to give quick answers as well as update the data in the cache from the network. When the request is completed successfully, the user interface will be updated automatically or via UI control.

Use the cached content data, but at the same time, perform a query to update the cache entry and inform the UI of the new data.

 const CACHE = 'cache-update-and-refresh-v1'; //         (). self.addEventListener('install', (event) => { event.waitUntil( caches .open(CACHE) .then((cache) => cache.addAll(['/img/background'])) ); }); //               . self.addEventListener('fetch', (event) => { //     ,  `respondWith()`  `waitUntil()` event.respondWith(fromCache(event.request)); event.waitUntil( update(event.request) //  ,   ""      . .then(refresh) ); }); function fromCache(request) { return caches.open(CACHE).then((cache) => cache.match(request).then((matching) => matching || Promise.reject('no-match') )); } function update(request) { return caches.open(CACHE).then((cache) => fetch(request).then((response) => cache.put(request, response.clone()).then(() => response) ) ); } //       . function refresh(response) { return self.clients.matchAll().then((clients) => { clients.forEach((client) => { //   ETag    // https://en.wikipedia.org/wiki/HTTP_ETag const message = { type: 'refresh', url: response.url, eTag: response.headers.get('ETag') }; //     . client.postMessage(JSON.stringify(message)); }); }); } 

Recipe number 5 - Embedded fallback


There is a problem when the default browser gives you a message that you are offline. I call this a problem because:




The best solution in this situation would be to show the user a user-defined fragment of the offline cache. With the help of SW, we can prepare a pre-prepared answer saying that the application is offline and its functionality is limited for a certain time.


Decision

It is necessary to give the fallback-data if there is no access to resources (network and cache).
Data is prepared in advance and put as static resources available to SW.

 const CACHE = 'offline-fallback-v1'; //         (). self.addEventListener('install', (event) => { event.waitUntil( caches .open(CACHE) .then((cache) => cache.addAll(['/img/background'])) // `skipWaiting()` ,      SW //    ,    . .then(() => self.skipWaiting()) ); }); self.addEventListener('activate', (event) => { // `self.clients.claim()`  SW      , //     `skipWaiting()`,   `fallback`    . event.waitUntil(self.clients.claim()); }); self.addEventListener('fetch', function(event) { //      . //     ,   `Embedded fallback`. event.respondWith(networkOrCache(event.request) .catch(() => useFallback())); }); function networkOrCache(request) { return fetch(request) .then((response) => response.ok ? response : fromCache(request)) .catch(() => fromCache(request)); } //  Fallback     . const FALLBACK = '<div>\n' + ' <div>App Title</div>\n' + ' <div>you are offline</div>\n' + ' <img src="/svg/or/base64/of/your/dinosaur" alt="dinosaur"/>\n' + '</div>'; //    , .      . function useFallback() { return Promise.resolve(new Response(FALLBACK, { headers: { 'Content-Type': 'text/html; charset=utf-8' }})); } function fromCache(request) { return caches.open(CACHE).then((cache) => cache.match(request).then((matching) => matching || Promise.reject('no-match') )); } 

Conclusion


Above, we covered the basic recipes for using SW for applications.
They are described as more complicated. If you have a simple landing page - you do not need to climb into the wilds, just use Cache only or Network or cache. For more complex applications, use the remaining recipes.

The article was conceived initial in a series of articles about the SW API. I want to understand how interesting and useful the topic is. Waiting for your comments and suggestions.

Source: https://habr.com/ru/post/345552/


All Articles