I am trying to change appcache to serviceworker in my application for offline work. I have removed manifest tag from html page and created serviceworker.js as of lifecycle events. But, appcache events are being used like this below:
function framework(){
this.appCache = window.applicationCache;
//more other codes
}
var fw = new framework();
Now, lifecycle events are checked like this:
if (fw.appCache) {
/* Checking for an update. Always the first event fired in the sequence.*/ fw.appCache.addEventListener('checking', handleCacheEvent, false);
/* An update was found. The browser is fetching resources.*/ fw.appCache.addEventListener('downloading', handleCacheEvent, false);
/* Fired if the manifest file returns a 404 or 410.
This results in the application cache being deleted. */ fw.appCache.addEventListener('obsolete', handleCacheEvent, false);
/* The manifest returns 404 or 410, the download failed,
or the manifest changed while the download was in progress.*/ fw.appCache.addEventListener('error', handleCacheError, false);
/* Fired for each resource listed in the manifest as it is being fetched.*/fw.appCache.addEventListener('progress', handleProgressEvent, false);
/*Fired after the first cache of the manifest. */ fw.appCache.addEventListener('cached', function(event){handleCacheEvent(event);removeProcessing();$("#processingTextId").html("");}, false);
/* Fired after the first download of the manifest.*/ fw.appCache.addEventListener('noupdate', function(event){handleCacheEvent(event);removeProcessing(); $("#processingTextId").html("");}, false);
/* Fired when the manifest resources have been newly redownloaded. */ fw.appCache.addEventListener('updateready', function(e) {if (fw.appCache.status == fw.appCache.UPDATEREADY) {alert('Successful'); try{fw.appCache.swapCache();window.location.reload();} catch(err){}}}, false);
}
function handleCacheEvent(event) {$("#processingTextId").html(event.type);}
function handleProgressEvent(event) {$("#processingTextId").html("(" + event.loaded + " of "+ event.total +")");}
function handleCacheError(event) {$("#processingTextId").html("Cache failed to update!")
So, my question is how to replace this events with service worker events. My serviceworker is registered and caching the assets properly. Now, i am doing like this in index.html
Registartion
<script type="text/javascript">
if('serviceWorker' in navigator){
navigator.serviceWorker.register('serviceworker.js')
.then(registration =>{
console.log('registration successful:',registration.scope);
})
.catch(err=>{
console.log('registration failed:',err);
});
}
</script>
I have created the seperate serviceworker.js.
How to replace those appcache events with serviceworker?
You won't end up with any of those events automatically when using a service worker. Also, the model for when a service worker populates and updates a cache is much more "open ended" than with AppCache, so translating service worker caching into equivalent AppCache events is not always possible.
In general, though, here are two things that can help:
Read up on the Service Worker Lifecycle. Some events that you might care about could be approximated by listening for equivalent changes to the service worker lifecycle. For instance, if you precache some URLs during service worker installation, then a newly registered service worker leaving the installing state would be roughly equivalent to when an AppCache manifest finishes caching. Similarly, if you detect when there's an update to a previously registered service worker (and that update is due to a change in the list of URLs to precache), then that would roughly correspond to when an AppCache manifest is updated.
If your service worker uses "runtime" caching, where URLs are added to the cache inside of a fetch handler, that you could use the following technique to tell your client page(s) when new items have been cached, using postMessage() to communicate.
Part of your service worker's JavaScript:
const cacheAddAndNotify = async (cacheName, url) => {
const cache = await caches.open(cacheName);
await cache.add(url);
const clients = await self.clients.matchAll();
for (const client of clients) {
client.postMessage({cacheName, url});
}
};
self.addEventListener('fetch', (event) => {
// Use cacheAddAndNotify() to add to your cache and notify all clients.
});
Part of your web app's JavaScript:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (event) => {
const {cacheName, url} = event.data;
// cacheName and url represent the entry that was just cached.
});
}
Again, the specifics of what you listen for and how you react to it are really going to depend on exactly what logic you have in your service worker. Don't expect there to be a 1:1 mapping between all events. Instead, use this migration as an opportunity to think about what cache-related changes you actually care about, and focus on listening for those (via service worker lifecycle events, or postMessage() communication).
Related
I'm building a PWA that is primary for offline use so I'm caching everything during the install process:
self.addEventListener("install", event => {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll(everything);
})
);
});
Is there any way during this step to communicate the progress of the caching to the client?
It doesn't look like the event in the "install" listener has a clientId to postMessages to.
Do new BroadcastChannel()s work in service workers?
I don't need a precise bytes progress/loaded, maybe just a files completed. Could I replace client.addAll with something like this?
for(let file of everything) {
await cache.add(file);
/* ...message clients another file is cached */
}
Having a preact app generated by preact-cli (uses workbox), my objective is to register a 'message' event handler on the service worker, post a message from the app and finally receive a response back.
Something like:
/* app.js */
navigator.serviceWorker.postMessage('marco');
const response = ? // get the response somehow
/* sw.js */
addEventListener('message', function (e) { return 'polo' });
I don't have much experience with service workers and there are a lot of moving parts that confuse me, like workbox doing magic on service worker, preact hiding away the code that registers the sercice worker and service workers being tricky to debug in general.
So far I've placed a sw.js file in the src/ directory as instructed by the preact-cli docs here: https://preactjs.com/cli/service-worker/
I know I am supposed to attach an event listener but I can't find documentation on which object to do so.
(Neither Workbox nor Preact have much to do with this question. Workbox allows you to use any additional code in your service worker that you'd like, and Preact should as well for your client app.)
This example page demonstrates sending a message from a client page to a service worker and then responding, using MessageChannel. The relevant helper code used on the client page looks like:
function sendMessage(message) {
return new Promise(function(resolve, reject) {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
// The response from the service worker is in event.data
if (event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
navigator.serviceWorker.controller.postMessage(message,
[messageChannel.port2]);
});
}
And then in your service worker, you used the MessageChannel's port to respond:
self.addEventListener('message', function(event) {
// Your code here.
event.ports[0].postMessage({
error: // Set error if you want to indicate a failure.
message: // This will show up as event.data.message.
});
});
You could also do the same thing using the Comlink library to simplify the logic.
I am using workbox v4.3.1 to provide offline capability to the users of the web application.
While everything works perfectly in Chrome as you would expect a PWA to work (i.e everything is cached locally, all the updates from the APP are captured in IndexedDB and synced back to the server when the application is back online.
However the major use case for me is to provide support for iOS Safari and as a PWA.
While all the pages are cached locally using the Service Worker in Safari and all the offline updates are also captured in the indexed DB as shown below,
However, when the connection returns online, the sync event is not triggered by the browser (Safari in this case). While background sync is not supported natively by Safari, I would expect that when I refresh the page, SW initialisation should trigger the sync event manually if it finds some data to be refreshed to the server in the indexed DB.
But this is not happening and I tried to manually listen for the "message" - "replayRequests" and then replay the requests - that did not work as well.
Any help here would be appreciated. Here is the service worker code for reference.
// If we're not in the context of a Web Worker, then don't do anything
if ("function" === typeof importScripts) {
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"
);
//Plugins
// Background Sync Plugin.
const bgSyncPlugin = new workbox.backgroundSync.Plugin("offlineSyncQueue", {
maxRetentionTime: 24 * 60
});
// Alternate method for creating a queue and managing the events ourselves.
const queue = new workbox.backgroundSync.Queue("offlineSyncQueue");
workbox.routing.registerRoute(
matchCb,
workbox.strategies.networkOnly({
plugins: [
{
fetchDidFail: async ({ request }) => {
await queue.addRequest(request);
}
}
]
}),
"POST"
);
// CacheKeyControlPlugin
const myCacheKeyPlugin = {
cacheKeyWillBeUsed: async ({ request, mode }) => {
normalizedUrl = removeTimeParam(request.url);
return new Request(normalizedUrl);
}
};
if (workbox) {
console.info("SW - Workbox is available and successfully installed");
} else {
console.info("SW - Workbox unavailable");
}
//Intercept all api requests
var matchCb = ({ url, event }) => {
// Filter out the presence api calls
return url.pathname.indexOf("somethingidontwanttocache") == -1;
};
function removeTimeParam(urlString) {
let url = new URL(urlString);
url.searchParams.delete("time");
return url.toString();
}
/* //Pre cache a page and see if it works offline - Temp code
workbox.precaching.precache(getPageAPIRequestURLs(), {
cleanUrls: false
}); */
workbox.routing.registerRoute(
matchCb,
new workbox.strategies.CacheFirst({
cacheName: "application-cache",
plugins: [myCacheKeyPlugin]
})
);
self.addEventListener("message", event => {
if (event.data === "replayRequests") {
queue.replayRequests();
}
});
}
workbox-background-sync emulates background sync functionality in browsers that lack native support by replaying queued requests whenever the service worker process starts up. The service worker process is meant to be lightweight and short lived, and is killed aggressively when there's a period of time without any events, and then is started up again in response to further events.
Reloading a web page may cause the service worker process to start up, assuming it had previously been stopped. But if the service worker is still running, then reloading the page will just cause a fetch event to be fired on the existing process.
The interval at which a service worker process can remain idle before it's killed is browser-dependent.
Chrome's DevTools offers a method of inspecting the state of a service worker and starting/stopping it on demand, but I don't believe Safari's DevTools offers that functionality. If you wanted to guanratee that a service worker was stopped and then start it up again, I would quit Safari, reopen it, and then navigate back to your web app.
My team and I have a project that was originally built as a PWA, but have since decided to scrap that idea as we realized it would need to change much more frequently than originally intended. However, the service worker is already live, as well as a newly redesigned landing page for the website. Despite all our efforts to clear the PWA caching, our clients are still reporting that they are receiving the old cached version of the website.
Currently, we have the service worker set up to delete all caches upon install (and whenever anything at all happens as a precaution), as well as some JavaScript to unregister the service worker when the new page actually loads. However, the problem is that none of this runs until the user makes a request to the website, and at that point the browser is already loading the cached content. Is it possible to clear this cache and prevent the browser from loading any content that was already cached?
Current service-worker.js
// Caching
var cacheCore = 'mkeSculptCore-0330121058';
var cacheAssets = 'mkeSculptAssets-0330121058';
self.addEventListener('install', function (event) {
self.skipWaiting();
caches.keys().then(function (names) {
for (let name of names)
caches.delete(name);
});
});
self.addEventListener('activate', function (event) {
caches.keys().then(function (names) {
for (let name of names)
caches.delete(name);
});
});
self.addEventListener('fetch', function (event) {
caches.keys().then(function (names) {
for (let name of names)
caches.delete(name);
});
});
Script in index.html
(function () {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function (registrations) {
//returns installed service workers
if (registrations.length) {
for (let registration of registrations) {
registration.unregister();
}
}
});
}
})();
So far, I've read a few other similar StackOverflow answers, including this one, but they tend to rely on users manually doing something to fetch the new content, ie. via a hard reload or disabling the service worker manually through the browser settings. However, in my case, we cannot rely on manual user actions.
One way to solve this issue is to add a timestamp at end of the file(js, css) name so each time when it is making a request, the cache key is not available in the service worker and thus it tends to get a new version of the file at each load.
<script type="text/javascript" src="/js/scipt1.js?t=05042018121212"/>
For appending a new timestamp dynamically in the file name, please check this answer
But this may not be reliable if HTML itself is cached.
add this before to "update" all contents:
$.each(['index.html','file1.js','file2.js','file3.js'],function(index,file) {
$.get(file+'?t='+new Date().getTime(), function(){});
});
location.reload(true);
for the ServiceWorker to stop all windows using it must be closed.
If it's a webapp you can use window.close();
This code just loads a fresh version of the files in the list.
If there are any internal caches they will be all updated.
Create-react-app comes with a registerServiceWorker.js file that contains code to register a service worker. I'm just a bit confused as to how it works. The code in question is:
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
What needs to happen for that first console log, the one that displays "New content is available; please refresh," to display?
More specifically, how can I trigger this code to run when index.html changes (in the event that a script filename changes).
Let's break it down step by step.
navigator.serviceWorker.register Promise is resolved when the valid Service Worker existence has been established
registration.onupdatefound registers a listener for an event that is fired when the HTTP request for Service Worker has been resolved to some other file than previously (or when the SW has been found for the first time)
registration.installing.onstatechange registers a listener for the new Service Worker's lifecycle changes (from installing to installed, from installed to activating etc.)
if (installingWorker.state === 'installed') filters out all the states other than installed - so its positive branch will be executed after each new SW has been installed
if (navigator.serviceWorker.controller) checks if the page is currently controlled by any (previous) Service Worker. If true then we're handling the aforementioned update scenario here.
So summing up - this console.log will execute after the updated (not the first one) Service Worker has been correctly installed.
It will not be triggered after index.html change. It's only Service Worker code (pointed to by serviceWorker.register method) that is checked against. Note also that normally browsers (or Chrome at least?) do not check for the new SW version for 24h after the current one was downloaded. Note also that plain old HTTP cache set for the Service Worker file might mess up here if it was send with too aggresive cache header.