⬆️ ⬇️

Some subtleties of using Service Workers





Foreword



Service Workers (Service Workers, readers forgive me) are today a useful addition to the main functionality of the site: here you can work offline, and background data synchronization, and trendy push notifications.



However, a large number of articles about service workers look quite concise and describe simple examples. I will try to draw attention to some features of the work of service workers, so some basic knowledge is required. The starting point may be this article ( translation ) or a slightly more detailed article .



Several service workers on the same domain



Registration (registration) of a specific service worker has such a thing as scope . It determines which pages on a particular domain will fall under its control. You can register several service workers on the same domain, but with different scope. If you try to register them with different names, but with one scope, then the worker installed later will “replace” his earlier brother.

')

By the way, to ensure that the file at the specified path can be set as a service worker on the path above (this behavior is prohibited by default, you can increase the path, you can reduce it), then you can use the Service-Worker-Allowed http header .



The getRegistration () method with no parameters returns the service worker's registration that is appropriate for the current page, and may be inactive. It also means that we will receive the same registration by the nested path, if not more appropriate. This can lead to surprises if several service workers are expected to work on the same domain.



Consider an example: we have an installed service worker with scope /. Let it be a news site and we provide offline versions of texts. There is also a control panel along the path / admin / with its own service worker. If the second service worker has not yet tried to install, then getRegistaration () will return the registration of the first service worker and this can lead to errors (for example, we will send notifications from the admin panel to the service worker who is not ready for them at all).



getRegistration has an optional parameter - scope. If you specify it, the method will return the registration that is most appropriate (not necessarily equal) to the transferred scope. Thus, we can unsubscribe from service workers on the embedded pages, or even get any registrations from the current domain, you just need to know the appropriate scope.



And if we do not know the entire scope, that is, the getRegistrations () method, which simply returns all registrations from the current domain as an array. Requires Firefox or Chrome 45+.



Link between page and service worker



The ability to exchange data between a service worker and a subordinate page can lead rather to original work patterns. For example, you can immediately send data from the cache, in parallel by requesting new ones; as soon as there is new data - put it in the cache and send it to the page.



The example on serviceworke.rs shows a simple way to communicate with a service worker:



navigator.serviceWorker.controller.postMessage(message.value); 


Here the controller is the service worker controlling the page. In recent browsers (all versions of Firefox and Chrome 51+), you can simply answer this request:



 self.addEventListener('message', function (event) { event.source.postMessage('response'); }); 


In older versions, you had to bypass all the tabs and find the right one, or even create a MessageChannel. Also now we have the ability to send a message to the tab from the fetch event. All this is described in the article , except that we already have modern api.



Another point is the storage of data in the service worker. People who have already tried the service workers may have noticed that LocalStorage is not there. This is because in service workers, the course was set for a fully asynchronous api (with the exception, perhaps, of importScripts ). But inside are still available:





Both caches and indexedDB are available in the usual way on pages, completely sharing data with the worker. If we turn to the previous paragraph, we can also come to the conclusion that several service workers on the same origin will share the data! In this case, you should not rub the caches of another service worker, for example, by checking them by the prefix:



 var CACHE_PREFIX = 'my-page-'; var CACHE_VERSION = 1; var CACHE_NAME = CACHE_PREFIX + CACHE_VERSION; self.addEventListener('activate', function(event) { event.waitUntil( caches.keys().then(function (cacheNames) { return Promise.all( cacheNames.map(function (cacheName) { if (cacheName.indexOf(CACHE_PREFIX) === 0) { return caches.delete(cacheName); } }) ); }) ); }); 


Or something like that, but then you will need to have in one place a complete list of possible caches.



But with all this it is worth remembering that no one guarantees 100% safety of the data in the storages. The browser can automatically clean CacheStorage and indexedDB if there is not enough disk space, and the user can do it himself.



Cross domain requests and other interactions with other domains



With the introduction of fetch, the situation could seem a bit confusing (there are different request / response modes), and with service workers everything becomes two times more complicated: one fetch is on the client side, the second is on the side of the service worker.



The simplest understanding to which you can come: “deceive” CORS and get access to content from another domain without headers will not work. It is important to separate the two uses: with access from the javascript side and without it. For example, you can replace one picture with another without problems: it is enough to specify the mode: 'no-cors' in the fetch of the service worker and it does not matter what the headers are. If you do not use 'no-cors', fetch will wait for the CORS headers, and if they are not available, everything will end in error.



More strictly speaking, any request (Request) from the page has mode. For example, a picture request is 'no-cors', and a picture request with the crossOrigin attribute (anonymous or use-credentials) is already 'cors'. Requests via XMLHttpRequest are always in 'cors' mode. And fetch allows you to set the mode directly.



The response (Response) has a type property. Requests for the current domain - 'basic'. Otherwise, if the request mode is 'cors', then the type of the response will also be 'cors', if there are necessary headers. The 'opaque' response mode can be accessed on a request in the 'no-cors' mode, in it you cannot access any response data.



Not all possible types of query modes are described here, but this should be enough for a general understanding. More information can be found in the article describing fetch .



Now let's try to combine everything. The request goes off the page, the service worker intercepts it and does its fetch, gets an answer. Until now, the situation has been dismantled, but now there will be a nuance: when sending a response with the type 'opaque' in response to a page request. which was not done with the no-cors mode, we get an error.



In addition to just requests, we can install a service worker to another domain. No, we will not get control over another page through our service worker - the conditions for the service worker remain the same (the script itself must be on the same domain to which the worker is registered). To do this, you can use an iframe from the required domain - no user permissions are required and the iframe can be made simply invisible.



Another interesting feature that is now in its earlier version is Foreign Fetch . If a regular service worker controls requests from a page in its scope (a page in scope, not requests), then foreign fetch allows you to control requests for your domain. Suppose the usual event fetch will be triggered when requesting a library on a CDN, and foreignFetch will be triggered by all requests for this library on any sites! This is a curious opportunity that can be used, for example, by analytics services.



Testing



There are certain difficulties with writing tests for service workers. Making a test is not so simple: if we want to check offline mode, then we need to somehow emulate network errors, if we want to check for an update, we need to replace the file with a new one and the like.



Additional problems also consist in the fact that currently “headless” browsers do not support service workers, which means that we need real ones.



There is a worthwhile article on the topic of service worker testing. It has links to a couple of tools: sw-unit-test-sample and platinum-sw (an element for Polymer, it also has a couple of tests). The article also describes an interesting trick: creating an iframe so that it is controlled by the service worker being tested. Generally speaking, the iframe and object elements have another feature: requests for them and their contents bypass the current page service worker using their own service workers.



The fact that caches is available on the page itself may be useful when testing to clean and check the contents of the cache.



An important nuance in the work of autotests is to determine when a service worker controls the page and can intercept requests. Simple navigator.serviceWorker.ready is not always the right solution - ready is triggered when the worker's service is activated, but before the completion of clients.claim (). Described in more detail here , as one of the solutions is to listen to the controllerchange event .



Service Worker Update



There are several nuances when updating service workers that you should pay attention to.



Despite the support of caching headers when requesting a service worker script, browsers reduce the cache lifetime to 24 hours. This is done in order not to accidentally leave the site to the user in a killed state for a long period of time. Here is a good answer to StackOverflow about caching.



Another caveat: the update works only if the service worker script itself has been updated, and the definition of this happens by-byte. From this it follows that updating files that are connected via importScripts will not lead to updating the service worker itself .



When updating, some files are often added to the cache from the network. But at the same time browser cache works! As with fetch calls inside a service worker. It is necessary either to be sure that the files have not changed (for example, to include the version / hash in the file name), or to load resources bypassing the cache. To load resources bypassing the cache, you can either manually call fetch and then add the response to the cache (remember to check response.ok, for example), or use the cache option: 'no-cache' Request'a (currently only works in Firefox Nightly) . Both are described in the article Jake Archibald .



It is also worth mentioning that the request behind the service worker's script during the update bypasses the fetch event handler of the current service worker.



miscellanea



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



All Articles