We create service workers by
navigator.serviceWorker.register('sw.js', { scope: '/' });
We can create new Workers without an external file like this,
var worker = function() { console.log('worker called'); };
var blob = new Blob( [ '(' , worker.toString() , ')()' ], {
type: 'application/javascript'
});
var bloburl = URL.createObjectURL( blob );
var w = new Worker(bloburl);
With the approach of using blob to create ServiceWorkers, we will get a Security Error as the bloburl would be blob:chrome-extension..., and the origin won't be supported by Service Workers.
Is it possible to create a service worker without external file and use the scope as / ?
I would strongly recommend not trying to find a way around the requirement that the service worker implementation code live in a standalone file. There's a very important of the service worker lifecycle, updates, that relies on your browser being able to fetch your registered service worker JavaScript resource periodically and do a byte-for-byte comparison to see if anything has changed.
If something has changed in your service worker code, then the new code will be considered the installing service worker, and the old service worker code will eventually be considered the redundant service worker as soon as all pages that have the old code registered and unloaded/closed.
While a bit difficult to wrap your head around at first, understanding and making use of the different service worker lifecycle states/events are important if you're concerned about cache management. If it weren't for this update logic, once you registered a service worker for a given scope once, it would never give up control, and you'd be stuck if you had a bug in your code/needed to add new functionality.
One hacky way is to use the the same javascript file understand the context and act as a ServiceWorker as well as the one calling it.
HTML
<script src="main.js"></script>
main.js
if(!this.document) {
self.addEventListener('install', function(e) {
console.log('service worker installation');
});
} else {
navigator.serviceWorker.register('main.js')
}
To prevent maintaining this as a big file main.js, we could use,
if(!this.document) {
//service worker js
importScripts('sw.js');
else {
//loadscript document.js by injecting a script tag
}
But it might come back to using a separate sw.js file for service worker to be a better solution. This would be helpful if one'd want a single entry point to the scripts.
Related
Registering a service worker is done in index.html with (eg):
<script>
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
</script>
If that code is followed by something that requests a resource, eg:
<script src="a.js"></script>
is that request guaranteed to trigger the 'fetch' event handler in the service worker (and so, potentially, be served from a cache)?
Or, should any code that causes a network access in index.html be added dynamically in the then() callback of the register() function (and, is THAT then guaranteed to be served by the service worker's 'fetch' event handler)?
I would recommend reading through "The Service Worker Lifecycle" for more general information.
The answer to most of your questions is "no," since what you're talking about is the initial registration of the service worker. Registering a service worker kicks off an installation and activation process that's independent from the promise returned by register(). The only thing you could infer from that promise is whether starting the process succeeded or not.
What you're asking about—whether a fetch handler will be invoked or not—relies on a service worker being in control of the current page.
In terms of JavaScript, if you want to answer the question "is this page controlled by (any) service worker?", you can do that by checking whether or not navigator.serviceWorker.controller is undefined.
If you want to write code that will only execute once there's a service worker in control of the current page (with the caveat that it might never execute, if something prevented the service worker from properly activating), you could do that by creating a promise that will resolve immediately if there's already a controller, and will otherwise resolve once the controllerchange event fires:
const p = new Promise(r => {
if (navigator.serviceWorker.controller) return r();
navigator.serviceWorker.addEventListener('controllerchange', e => r());
});
// Later, if you want code to execute only if the page is controlled:
p.then(() => {
// There's a SW in control at this point.
});
Inside your service worker, you can add the following to your activate handler to ensure that as soon as a newly installed service worker activates, it takes control of any open pages (including the page that registered it for the first time):
self.addEventListener('activate', () => self.clients.claim());
If you don't include self.clients.claim() inside your service worker's activate handler, then the page that starts out uncontrolled will never start being controlled, even though it's registered a service worker that has activated.
I'm currently evaluating whether k6 fits our load testing needs. We have a fairly traditional website architecture that uses Apache webservers with PHP und a MySQL database. Sending simple HTTP requests with k6 looks simple enough and I think we will be able to test all major functionality with it, as we don't rely on JavaScript that much and most pages are static.
However, I'm unsure how to deal with resources (stylesheets, images, etc.) that are referenced in the HTML that is returned in the requests. We need to load them as well, as this sometimes leads to database requests, which must be part of the load test.
Is there some out-of-the-box functionality in k6 that allows you to load all the resources like a browser would? I'm aware that k6 does NOT render the page and I don't need it to. I only need to request all the resources inside the HTML.
You basically have two options, both with their caveats:
Record your session - you can either export har directly from the browser as shown there or use an extension made for your browser here is firefox and chromes. Both should be usable without a k6 cloud account you just need to set them to download the har and it will automatically (and somewhat silently) download them when you hit stop. And then either use the in k6 har converter (which is deprecated, but still works) or the new har-to-k6 one which.
This method is particularly good if you have a lot of pages and/or resources and even works if you have a single page style of application as it just gets what the browser requested as a HAR and then transforms it into a script. And if there were no dynamic things that need to be inputed (username/password) the final script can be used as is most of the time.
The biggest problem with this approach is that if you add a css file you need to redo this whole exercise. This is even more problematic if you css/js file name change on each change or something like that. Which is what the next method is good for:
Use parseHTML and then find the elements you care about and make a request for them.
import http from "k6/http";
import {parseHTML} from "k6/html";
export default function() {
const res = http.get("https://stackoverflow.com");
const doc = parseHTML(res.body);
doc.find("link").toArray().forEach(function (item) {
console.log(item.attr("href"));
// make http gets for it
// or added them to an array and make one batch request
});
}
will produce
NFO[0001] https://cdn.sstatic.net/Sites/stackoverflow/img/favicon.ico?v=4f32ecc8f43d
INFO[0001] https://cdn.sstatic.net/Sites/stackoverflow/img/apple-touch-icon.png?v=c78bd457575a
INFO[0001] https://cdn.sstatic.net/Sites/stackoverflow/img/apple-touch-icon.png?v=c78bd457575a
INFO[0001] /opensearch.xml
INFO[0001] https://cdn.sstatic.net/Shared/stacks.css?v=53507c7c6e93
INFO[0001] https://cdn.sstatic.net/Sites/stackoverflow/primary.css?v=d3fa9a72fd53
INFO[0001] https://cdn.sstatic.net/Shared/Product/product.css?v=c9b2e1772562
INFO[0001] /feeds
INFO[0001] https://cdn.sstatic.net/Shared/Channels/channels.css?v=f9809e9ffa90
As you can see some of the urls are relative and not absolute so you will need to handle this. And in this example only some are css, so probably more filtering is needed.
The problem here is that you need to write the code and if you add a relative link or something else you need to handle it. Luckily k6 is scriptable so you can reuse the code :D.
I've followed Михаил Стойков suggestion and written my own function to load resources. You can set the way resources are loaded (batch or sequential gets with options.concurrentResourceLoading).
/**
* #param {http.RefinedResponse<http.ResponseType>} response
*/
export function getResources(response) {
const resources = [];
response
.html()
.find('*[href]:not(a)')
.each((index, element) => {
resources.push(element.attributes().href.value);
});
response
.html()
.find('*[src]:not(a)')
.each((index, element) => {
resources.push(element.attributes().src.value);
});
if (options.concurrentResourceLoading) {
const responses = http.batch(
resources.map((r) => {
return ['GET', resolveUrl(r, response.url), null, {
headers: createHeader()
}];
})
);
responses.forEach(() => {
check(response, {
'resource returns status 200': (r) => r.status === 200,
});
});
} else {
resources.forEach((r) => {
const res = http.get(resolveUrl(r, response.url), {
headers: createHeader(),
});
!check(res, {
'resource returns status 200': (r) => r.status === 200,
});
});
}
}
Usually whenever I read a blog post about PWA's, the tutorial seems to just precache every single asset. But this seems to go against the app shell pattern a bit, which as I understand is: Cache the bare necessities (only the app shell), and runtime cache as you go. (Please correct me if I understood this incorrectly)
Imagine I have this single page application, it's a simple index.html with a web component: <my-app>. That <my-app> component sets up some routes which looks a little bit like this, I'm using Vaadin router and web components, but I imagine the problem would be the same using React with React Router or something similar.
router.setRoutes([
{
path: '/',
component: 'app-main', // statically loaded
},
{
path: '/posts',
component: 'app-posts',
action: () => { import('./app-posts.js');} // dynamically loaded
},
/* many, many, many more routes */
{
path: '/offline', // redirect here when a resource is not cached and failed to get from network
component: 'app-offline', // also statically loaded
}
]);
My app may have many many routes, and may get very large. I don't want to precache all those resources straight away, but only cache the stuff I absolutely need, so in this case: my index.html, my-app.js, app-main.js, and app-offline.js. I want to cache app-posts.js at runtime, when it's requested.
Setting up runtime caching is simple enough, but my problem arises when my user visits one of the potentially many many routes that is not cached yet (because maybe the user hasn't visited that route before, so the js file may not have loaded/cached yet), and the user has no internet connection.
What I want to happen, in that case (when a route is not cached yet and there is no network), is for the user to be redirected to the /offline route, which is handled by my client side router. I could easily do something like: import('./app-posts.js').catch(() => /* redirect user to /offline */), but I'm wondering if there is a way to achieve this from workbox itself.
So in a nutshell:
When a js file hasn't been cached yet, and the user has no network, and so the request for the file fails: let workbox redirect the page to the /offline route.
Option 1 (not always useful):
As far as I can see and according to this answer, you cannot open a new window or change the URL of the browser from within the service worker. However you can open a new window only if the clients.openWindow() function is called from within the notificationclick event.
Option 2 (hardest):
You could use the WindowClient.navigate method within the activate event of the service worker however is a bit trickier as you still need to check if the file requested exists in the cache or not.
Option 3 (easiest & hackiest):
Otherwise, you could respond with a new Request object to the offline page:
const cacheOnly = new workbox.strategies.CacheOnly();
const networkFirst = new workbox.strategies.NetworkFirst();
workbox.routing.registerRoute(
/\/posts.|\/articles/,
async args => {
const offlineRequest = new Request('/offline.html');
try {
const response = await networkFirst.handle(args);
return response || await cacheOnly.handle({request: offlineRequest});
} catch (error) {
return await cacheOnly.handle({request: offlineRequest})
}
}
);
and then rewrite the URL of the browser in your offline.html file:
<head>
<script>
window.history.replaceState({}, 'You are offline', '/offline');
</script>
</head>
The above logic in Option 3 will respond to the requested URL by using the network first. If the network is not available will fallback to the cache and even if the request is not found in the cache, will fetch the offline.html file instead. Once the offline.html file is parsed, the browser URL will be replaced to /offline.
Most examples of registering a Service worker do so through JavaScript. For example (From MDN):
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js', {
scope: './'
}).then(function (registration) {
var serviceWorker;
if (registration.installing) {
serviceWorker = registration.installing;
document.querySelector('#kind').textContent = 'installing';
} else if (registration.waiting) {
serviceWorker = registration.waiting;
document.querySelector('#kind').textContent = 'waiting';
} else if (registration.active) {
serviceWorker = registration.active;
document.querySelector('#kind').textContent = 'active';
}
if (serviceWorker) {
// logState(serviceWorker.state);
serviceWorker.addEventListener('statechange', function (e) {
// logState(e.target.state);
});
}
}).catch (function (error) {
// Something went wrong during registration. The service-worker.js file
// might be unavailable or contain a syntax error.
});
} else {
// The current browser doesn't support service workers.
}
But I noticed in the Web App Manifest standard that there is a serviceworker member:
"serviceworker": {
"src": "sw.js",
"scope": "/",
"update_via_cache": "none"
}
This is the only place I've seen this referred to.
This raises two questions for me:
1 Which approach SHOULD I use? What are the trade-offs?
The declarative benefit of the manifest approach is obvious, but if I use that approach, how do I reference the registration object in order to track events similar to the script approach? (installing | waiting | active | failed).
Assuming it IS possible to reference the registration object appropriately, can it miss events? Such as finish installing before I could register an event listener to it.
2 What are the caching implications
Since the manifest would be saved in the offline cache, and this manifest would reference the service-worker script, what are the cache implications? Does the 24 hour rule still apply assuming I do NOT store the script in the offline cache? The update_via_cache member is not a simple thing to read in the spec.
It looks like it was added to the spec back in October of 2016, and there is some background discussion in the issue tracker.
My interpretation is that the use case is providing service worker bootstrapping metadata that is relevant when installing a web app via a non-browser mechanism, e.g. via an app store. I don't see any mention of that field in the guidance about Microsoft Store ingestion, though.
So... as of right now, I am not clear that any browsers honor the serviceworker field in the web app manifest, and if your concern is having a functional service worker registration for "browser" use cases, do it using JavaScript.
Your best bet for follow ups would be to ask on the web app manifest's GitHub issue tracker.
I have noticed that the time waiting for service workers to respond with items from the cache is not as fast as you would expect it to be. I have seen the same wait times with both sw-precache and a custom written service worker.
What are the possible causes for this wait time time and how could I reduce it?
My fetch event on the custom service worker looks like:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
Do you have 'Update on reload' checked within your Chrome Dev Tools under the Application -> Service Worker tab?
If so then I think this may be the problem as it'll be re-running all your Service Worker code, which can be quite a lot of code when using sw-precache, on each reload.
Event though I can't answer the possible causes for this strange wait time, I do know how to reduce it.
We are able to intercept a fetch event in service worker with event.respondWith(). Somehow, in my case, when my page needs to load a vendor's javascript via script tag with my service worker defaults to intercept every fetch event to perform cache-then-network (for assets) or network-only (for data fetching) like this:
if (shouldLoadOfflinePage) {
fetchEvent.respondWith(cacheManager.fromCache(new Request(offlinePage)));
} else if (shouldFromCache) {
fetchEvent.respondWith(cacheManager.fromCache(fetchEvent.request));
} else {
fetchEvent.respondWith(fetch(fetchEvent.request));
}
The last block intercepts a network-only request which is pretty unnecessary to do. This unnecessary block somehow causes a blocking load (but I don't know what blocks it):
long request to serviceworker wait time: ~400ms
So I decided not to intercept unnecessary fetch-interception (of course by removing the last block):
if (shouldLoadOfflinePage) {
fetchEvent.respondWith(cacheManager.fromCache(new Request(offlinePage)));
} else if (shouldFromCache) {
fetchEvent.respondWith(cacheManager.fromCache(fetchEvent.request));
}
Then my page needs only 16ms to load the aforementioned file.
Hope this will help
clear out the indexedDB will help to reduce the time it takes to "Request to Service Worker". You could delete it by js:
indexedDB.deleteDatabase(location.host);
or do it manually:
/Users/[USERNAME]/Library/Application
Support/Google/Chrome/Default/IndexedDB/