I'm trying to figure out what happens if I have a service worker registered on a live site called sw.js and then I rename the service worker to service-worker.js. Now the old one isn't found but it is still showing the old cached version.
How long does it take for it to register the new renamed service worker or how does this work at all?
Edit
This is how I have register the service worker in a react application:
componentDidMount() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/service-worker.js")
.then(registration => {
console.log("service worker registration successful: ", registration);
})
.catch(err => {
console.warn("service worker registration failed", err.message);
});
}
}
The newly created service worker (renamed) cannot take over the old one because the old one is still active and controlling the client.
the new service worker(renamed one) will wait until the existing worker is controlling zero clients.
Now imagine a service worker sw.js installed and active (controlling the client),
Chrome will visualize the process for you like this
1. The service worker is registered and active
2. Now let's rename the service worker file to sw2.js
You can see that chrome is telling you that something has changed about the service worker. but the current one will keep controlling the client until you force the new one to take control by clicking on the skipWaitting button or by flushing your cache. clicking on the button will cause the sw2.js to take controll over the sw1.js
Now if you need to do this programmatically, you can do it in the install event inside your service worker by calling self.skipWaiting().
self.addEventListener('install', (e) => {
let cache = caches.open(cacheName).then((c) => {
c.addAll([
// my files
]);
});
self.skipWaiting();
e.waitUntil(cache);
});
The following animated image from Jake Archibald's article The Service Worker Lifecycle can make the idea more clear.
You have to update the instance creation code to reflect this change where your shared worker is being initialized and used, for example your current code would look like
var worker = new SharedWorker("ws.js");
That will need to be updated to
var worker = new SharedWorker("service-worker.js");
I was able to solve it by setting up a server which listens to both /service-worker.js and /sw.js get requests.
Since the service worker was renamed from sw.js to service-worker.js it was not finding the old service worker at http://example.com/sw.js so what I did was the following:
createServer((req, res) => {
const parsedUrl = parse(req.url, true);
const { pathname } = parsedUrl;
// new service worker
if (pathname === "/service-worker.js") {
const filePath = join(__dirname, "..", pathname);
app.serveStatic(req, res, filePath);
// added new endpoint to fetch the new service worker but with the
// old path
} else if (pathname === "/sw.js") {
const filePath = join(__dirname, "..", "/service-worker.js");
app.serveStatic(req, res, filePath);
} else {
handle(req, res, parsedUrl);
}
}).listen(port, err => {
if (err) throw err;
console.log(`> Ready on http://localhost:${port}`);
});
As you can see, I added a second path to serve the same service worker but with the /sw.js endpoint, one for the old sw.js and the other one for the newer service-worker.js.
Now when old visitors that have the old active sw.js will download the newer one and upon revisit, they will automatically fetch the newer renamed service-worker.js service worker.
Related
I asked a question last week about this and got a very helpful answer but I am still struggling to get this working as it is supposed to, although at this point I'm not entirely sure what I've been asked to do is possible.
So this service worker is supposed to activate when ?testMode=true is added to the URL and this seems to be happening okay. The service worker is then supposed to intercept specific requests before they happen and then redirect it to mock data instead. What I have got at the moment will store the mock data and call it when specific requests are made but it doesn't actually stop the initial request from happening as it still appears within the network tab.
So for example if the request contains /units/all?availability=Active&roomTypeHandle=kitchens, the service worker is meant to intercept this and instead of that request going through, mock-data/unitData.json is supposed to be used instead.
This is what I have so far:
TestMode.ts
class TestMode {
constructor() {
if (!this.isEnabled()) {
return;
}
if (!('serviceWorker' in navigator)) {
console.log('Browser does not support service workers');
return;
}
this.init();
}
private init(): void {
navigator.serviceWorker
.register('planner-worker/js/worker.min.js')
.then(this.handleRegistration)
.catch((error) => { throw new Error('Service Worker registration failed: ' + error.message); });
navigator.serviceWorker.addEventListener('message', event => {
// event is a MessageEvent object
console.log(`The service worker sent me a message: ${event.data}`);
});
navigator.serviceWorker.ready.then( registration => {
if (!registration.active) {
console.log('failed to communicate')
return;
}
registration.active.postMessage("Hi service worker");
});
}
private handleRegistration(registration: ServiceWorkerRegistration): void {
registration.update();
console.log('Registration successful, scope is:', registration.scope);
}
private isEnabled(): boolean {
return locationService.hasParam('testMode');
}
}
export default new TestMode();
serviceWorker.js
const CACHE_NAME = 'mockData-cache';
const MOCK_DATA = {
'/units/all?availability=Active&roomTypeHandle=kitchens': 'mock-data/unitData.json',
'/frontal-ranges/kitchens?' : 'mock-data/kitchensData.json',
'/carcase-ranges/?availability=Active&roomTypeHandle=kitchens' : 'mock-data/carcaseRangesData.json',
'/products/830368/related?roomTypeHandle=kitchens&productStateHandle=Active&limit=1000&campaignPhaseId=183&retailStore=Finishing%20Touches%20%28Extra%29'
: 'mock-data/relatedItems.json'
};
self.addEventListener('install', event => {
console.log('Attempting to install service worker and cache static assets');
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(Object.values(MOCK_DATA));
})
);
});
self.addEventListener('activate', function(event) {
return self.clients.claim();
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
const pathAndQuery = url.pathname + url.search;
if (pathAndQuery in MOCK_DATA) {
const cacheKey = MOCK_DATA[pathAndQuery];
event.respondWith(
caches.match(cacheKey, {
cacheName: CACHE_NAME,
})
);
}
});
Another thing that happens which I'm not sure how to get around is that the fetch event only happens once. By that I mean that when a change is made to serviceWorker.js, the mock data is stored in the cache and the files appear in the network tab of dev tools, but if I refresh the page, or close it and reopen in another tab, the mock data is no longer in the network tab and it's as if the service worker is not being used at all. How can I update this so that it's always used? I can see in the console log that the service worker is registered, but it just don't seem to get used.
Apologies if I shouldn't be asking 2 questions in 1 post, just really not sure how to solve my issue. Thanks in advance for any guidance.
Turns out my issue was a scope one. Once moving where the service worker was stored it started working as intended. Realised this was the case as I figured out the fetch event wasn't actually firing.
I am trying to add a serviceworker to an existing React app with this filesystem layout:
Filesystem
Basically a bit of initialization code is stored in the public folder, and all code of importance is in the src folder. In the serviceWorker.js file, I made an array of filenames to cache and call that array in the 'install' event listener, and if I check DevTools I can see that the filenames are present in the cache: when I preview the data in Chrome DevTools however, I see that the code inside the cached files is all from index.html. In fact, I can add anything I want to the filename array and I will find it in cached storage only to find that it is storing the index.html code. It seems like no matter what file I try to add to the cache, only index.html gets loaded.
ServiceWorker.js:
let CACHE_NAME = "MG-cache-v2";
const urlsToCache = [
'/',
'/index.html',
'/src/App.js',
'/monkey'
];
self.addEventListener('install', function (event) {
//perform install steps
event.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log('Opened MG_Cache');
return cache.addAll(urlsToCache);
}).catch(function (error) {
console.error("Error loading cache files: ", error);
})
);
self.skipWaiting();
});
self.addEventListener('fetch', function (event) {
event.respondWith(caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(async function () {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.filter((cacheName) => {
//Return true if you want to remove this cache,
//but remember that caches are shared across the whole origin
return;
}).map(cacheName => caches.delete(cacheName))
);
})
})
Portion of index.html:
<script>
if ('serviceWorker' in navigator)
{
window.addEventListener('load', function () {
navigator.serviceWorker.register('serviceWorker.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>
Google Devtools Preview:
All files are the same
I have tried a variety of naming strategies in the filename array but all have ended with the same result. At this point I'm at a complete loss.
EDIT: While this does not solve my problem, I found an answer to another problem that gives a little guidance. It seems like the server never finds the file I request and thus returns index.html. I tried placing the serviceWorker.js file in the src folder and moving the service worker registration to App.js and got an error:
`DOMException: Failed to register a ServiceWorker for scope ('http://localhost:3000/src/') with script ('http://localhost:3000/src/serviceWorker.js'): The script has an unsupported MIME type ('text/html'). `
This suggests that the server somehow doesn't have access to the src folder, only public. Any idea why that may be?
An important piece of information I left out it that I'm using Create-React-App. Because of the enforced layout of the filesystem, the serviceWorker must be placed in the public folder: at the same time, the scope of service workers by default is the folder that they are placed in. According to this answer, changing the scope of the service worker to be a level above the folder that it is in requires adding to the HTTP header response of the service worker (not entirely sure what that means), and doing something like that assumes you have some form of a local server set up. Alas, thus far I have just been using npm start to test my app and pushing onto nsmp to make the site live, thus have negleted to do any form of server implementation myself (I know, not very smart of me).
My hotfix was to create a new temporary app with the npx create-react-app my-app --template cra-template-pwa command, copy all files pertaining to service workers from that app (serviceWorkerRegistration.js, service-worker.js, index.js, potentially setupTests.js), and paste them into my app. Then I could simply follow this tutorial. Now my site works offline.
My single page web application, is written as PWA. It's a ASP.NET Web API application, which is hosted under IIS. My application uses a service worker, which precaches my application shell. Other content is written to indexedDB or updated in the service worker cache on-the-fly, as the user clicks through the application.
I have a problem with code changes. When I update code, and refresh my browser, I can see my changes directly in the javascript and html code. When I go offline, my application falls back on the code, which is in my service worker. This code is still old code. It seems not to be updated, when i reload my application. My service worker looks like this:
const urlsToCache = ['.'
, 'index.html'
, 'favicon.ico'
, 'service-worker.js'
// More sources are cached here
];
self.addEventListener('install', function (event) {
console.log('\'Install\' event triggered.');
event.waitUntil(
caches.open(CACHE).then(function (cache) {
console.log('Application shell and content has been cached.')
return cache.addAll(urlsToCache);
}).then(function (event) {
return self.skipWaiting();
})
);
});
self.addEventListener('activate', function (event) {
console.log('\'Activate\' event triggered.');
return self.clients.claim();
});
For registering the service worker, my code looks like this:
navigator.serviceWorker.register('./service-worker.js')
.then(function (registration) {
console.log('Registered: ', registration);
registration.update();
console.log('Updated: ', registration);
resolve(registration);
}).catch(function (error) {
console.log('Registration failed: ', error);
reject(error);
});
So when the service worker get registered, it also checks for updates. However, it only updates when the service worker code itself is changed. Not when code within my application shell is changed without changing the service worker code itself.
How can i make sure that the code in my service worker is reloaded when I change my application code and refresh my page?
I haven't used http-server npm module before!
i was just getting starting with using service worker by creating an example app using this blog as a source
In this the author have used http-server to demonstrate service worker!
From the blog, I copied the code snippet to check if chrome is supported
const check = () => {
if (!('serviceWorker' in navigator)) {
throw new Error('No Service Worker support!')
}
if (!('PushManager' in window)) {
throw new Error('No Push API Support!')
}
console.log('Everything works fine')
}
const main = () => {
check()
}
main()
Notice the console.log('Everything works fine')
Now, as i proceeded with Article, I changed my main.js as instructed to this
const check = () => {
if (!('serviceWorker' in navigator)) {
throw new Error('No Service Worker support!')
}
if (!('PushManager' in window)) {
throw new Error('No Push API Support!')
}
console.log(`Browser supports Pushmanager and Service worker`)
}
// Async so we can use await
const main = async () => {
check()
const swRegistration = await registerServiceWorker();
}
//To run service worker we need to first register service worker
//Registering Service Worker
const registerServiceWorker = async () => {
const swRegistration = await navigator.serviceWorker.register('service.js'); //notice the file name
return swRegistration;
}
but when I go the page, it keeps logging this from our first code snippet in the console
Everything works fine
I checked the main.js, and it appears the server is still serving the previous main.js instead of new one.
If I go manually to my file and open it without the server, the main.js shows my new code changes.
I have tried turning of the server (using ctrl/cmd + c) and restarting it again but no help.
Can someone help me out in fixing it?
Looks like http-server tells the browser to cache pages for an hour by default:
-c Set cache time (in seconds) for cache-control max-age header, e.g. -c10 for 10 seconds (defaults to '3600'). To disable caching, use -c-1.
Restart the server with http-server -c-1, then do a hard refresh in the browser (Ctrl/Cmd+Shift+R in Firefox or Chrome).
I'm trying to make the update of my ServiceWorker work.
I have an old service worker, some windows are under it's control. Now there is a new version of the ServiceWorker. It get installed properly. But it does not get activated for new pages. My aim is to keep the old one for old pages and the new for every new tab/pages viewed.
Any idea how to accomplish this ?
Edit:
Is how I check that the the new service worker is not updated:
I have a sendMessage method:
sendMessage(message): Promise {
const channel = new MessageChannel();
const p1 = channel.port1;
const result = new Promise(resolve => {
p1.onmessage = ({ data }) => {
resolve(data);
};
});
serviceWorker.controller.postMessage(message, [channel.port2]);
return result;
}
Then I use it to check on page start
this.ready = this.sendMessage('version')
.then(version => {
if (version !== process.env.BUILD_NUMBER) {
console.log('$$ Version mismatch, reinstalling service worker');
throw new Error('build mismatch');
}
return serviceWorker.controller;
});
And I answer in the ServiceWorker
self.addEventListener('message', ({ data, ports }) => {
const client = ports[0];
if (data === 'version') {
client.postMessage(process.env.BUILD_NUMBER);
}
});
Edit
Adding:
event.waitUntil(self.clients.claim());
helps but it activate the ServiceWorker also for old clients.
Thanks
Calling self.skipWaiting() within a service worker's install event handler will cause the newly installed service worker to immediately activate, even if there's an existing, older service worker that's currently active for other clients. Without self.skipWaiting(), the newly installed service worker will remain in a waiting state until all other clients of the previous service worker have been closed.
Calling self.clients.claim() from inside the activate event handler is sometimes used as well, but only if you want the newly activated service worker to take control of already-open clients that are either uncontrolled, or controlled by an older version of the service worker. In your case, it sounds like you do not want that behavior, so you shouldn't call self.clients.claim().