I've consulted a lot of resources on Service Workers:
Updating your ServiceWorker
ServiceWorker: Revolution of the Web Platform
Jake Archibald's lovely SVGOMG.
However, I can't for the life of me figure out how to update the page after a new ServiceWorker has been installed. No matter what I do, my page is stuck on an old version, and only a hard refresh (Cmd-Shift-R) will fix it. No combination of 1) closing the tab, 2) closing Chrome, or 3) location.reload(true) will serve the new content.
I have a super simple example app mostly based on SVGOMG. On installation, I cache a bunch of resources using cache.addAll(), and I also do skipWaiting() if the current version's major version number doesn't match the active version's number (based on an IndexedDB lookup):
self.addEventListener('install', function install(event) {
event.waitUntil((async () => {
var activeVersionPromise = localForage.getItem('active-version');
var cache = await caches.open('cache-' + version);
await cache.addAll(staticContent);
var activeVersion = await activeVersionPromise;
if (!activeVersion ||
semver.parse(activeVersion).major === semver.parse(version).major) {
if (self.skipWaiting) { // wrapping in an if while Chrome 40 is still around
self.skipWaiting();
}
}
})());
});
I'm using a semver-inspired system where the major version number indicates that the new ServiceWorker can't be hot-swapped for the old one. This works on the ServiceWorker side - a bump from v1.0.0 to v1.0.1 causes the worker to be immediately installed on a refresh, whereas from v1.0.0 to v2.0.0, it waits for the tab to be closed and reopened before being installed.
Back in the main thread, I'm manually updating the ServiceWorker after registration – otherwise the page never even gets the memo that there's a new version of the ServiceWorker available (oddly I found very few mentions of this anywhere in the ServiceWorker literature):
navigator.serviceWorker.register('/sw-bundle.js', {
scope: './'
}).then(registration => {
if (typeof registration.update == 'function') {
registration.update();
}
});
However, the content that gets served to the main thread is always stuck on an old version of the page ("My version is 1.0.0"), regardless of whether I increment the version to 1.0.1 or 2.0.0.
I'm kind of stumped here. I was hoping to find an elegant semver-y solution to ServiceWorker versioning (hence my use of require('./package.json').version), but in my current implementation, the user is perpetually stuck on an old version of the page, unless they hard-refresh or manually clear out all their data. :/
Found the issue – you need to avoid any cache headers on the ServiceWorker JS file itself. Setting the cache to max-age=0 immediately solved the problem: https://github.com/nolanlawson/serviceworker-update-demo/pull/1
Cheers to Jake Archibald for setting me straight: https://twitter.com/jaffathecake/status/689214019308224513
External: stop and unregister a service worker using chrome://serviceworker-internals/
Internal from service worker itself: https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim and https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting
In Chrome Canary you can do all sort of Service Worker profiling and management like unregister, clear in the Applications Tab:
Pair that with chrome://devices/ to debug with your physical device.
Related
In this answer When and how does a PWA update itself? it is explained that a PWA is updated if manifest.json links are updated or if one character is modified from the service-worker.js file.
But what do we exactly mean by "updated"? Does this mean the install event is re-triggered and that all the listed files below are re-downloaded?
Is there a documentation confirming this is the case for all browsers on Android and iOS? (I don't know if the latter uses PWA the same way than on Android)
Here is an example service-worker.js file:
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open('myapp-store').then((cache) => cache.addAll([
'/app/index.html',
'/app/file1.jpg',
'/app/file2.mp3',
'/app/file3.png',
])),
);
});
self.addEventListener('fetch', (e) => {
console.log(e.request.url);
e.respondWith(
caches.match(e.request).then((response) => response || fetch(e.request)),
);
});
The service worker update flow is independent from any update checks that apply to the web app manifest.
"The Service Worker Lifecycle" is recommended reading, and it explains the complain initial install and update flow.
To borrow from that document and answer your question, yes, the install event of the updated service worker file will be executed once a browser detects that update. If your updated service worker file includes a cache.addAll() then that will be executed, and if the URLs being cached are identical to the same URLs previously cached, and the cache name is the same, then the old cached values will be overwritten with whatever is fetche during install.
Many folks include inline versioning information in their service worker file so that they can trigger an update by changing that value. They also might use a new cache name tied to that versioning information, so that instead of overwriting previously cached values, new cache entries will be created. This tends to be safer, since the old service worker will remain in control of open pages until the updated service worker activates, and using versioned cache names means that the old service worker can continue reading the cached values it expects.
I serve the static webapp on aws s3 that distributed through CDN (aws cloudfront). The files are ES6 based build version with rollup. For short info, except index.html rollup will generate new hash files every build the webapp. So the files are always unique every update, except index.html. Then on the aws cloudfront I put the index.html into invalidation cache list.
Well then, I will expect users always request the latest version of the webapp with that approach. Yes it works, but with a little note!
So once there is new updates, the browser is still loading the old index.html file on the first time. I have to refresh page to push the browser get the latest index.html. It's not good for the end users. They doen't want to know about refreshing, most will not know right?
One last experiment, I added small script inside on index.html to perform version validation like so :
<script>
fetch('/version.json').then(r => {
if (r.status == 200) {
return r.json()
} else {
alert("Found server updated, let us resync the contents!")
location.reload(true)
}
}).then(j => {
if (window.localStorage.getItem("app-version")) {
if (j.version != window.localStorage.getItem("app-version")) {
alert("Found server updated, let us sync the contents!")
window.localStorage.setItem("app-version", j.version)
location.reload(true)
}
} else {
window.localStorage.setItem("app-version", j.version)
}
})
</script>
That script worked as expected but wondering whether I will have better solution out there? Kindly to have another idea, please?
Expected behavior
Browser have knowledge the latest index.html immediately without any refresh/reload page from end users.
Thank you
Just use a timestamp in the URL to force the browser to get the latest version.
(()=>{
// Get the current timestamp
var now = new Date().getTime();
// Check for a ts parameter in the url (index.html?ts=2345234523)
const urlParams = new URLSearchParams(location.search);
var ts = urlParams.get('ts');
// If there is no timestamp parameter,
// or the timestamp parameter is older than 1 minute
// redirect the page to the latest version
if(!ts || now - ts > 60000){
location.href = `index.html?ts=${now}`;
}
})();
You'll want to put that near the top so you don't have a lot of things loading before the redirect.
I would take advantage of a service-worker to make it easier and more efficient to load and cache a cohesive version of the app.
The service worker has a built in mechanism for checking whether the app has been updated, which is does each time the app is launched.
What it won't do is trigger that check for you while the app is running. While you could poll regularly to check for updates, it's more efficient for both your server and users to have them subscribe for updates which you can do using something like Firebase RTDB (Real Time Database):
When you publish a new version and want to immediately force all running instances to update, you change the version or timestamp that they are subscribing to and have that trigger the service-worker update check, which then refreshes and reloads the app.
There are lots of available patterns for prompting users about any update in case they are in the middle of completing a form etc...
I'm using the MEAN stack (mongo, express, angular and node). I'm deploying relatively frequently to production...every couple of days. My concern is that I'm changing the client side code and the API at times and I would rather not have to ensure backwards compatibility of the API with previous versions of the client code.
In such a scenario, what is the most effective way of ensuring that all clients reload when I push to production? I have seen that Evernote for example has a pop-up that says something along the lines of please reload your browser for the latest version of Evernote. I would like to do something similiar...do I need to go down the path of socket.io or sock.js or am I missing something simple and there is a simpler way to achieve this?
Update:
AppCache was deprecated summer 2015 so the below is no longer the best solution. The new recommendation is to use Service Workers instead. However, Service Workers are currently still experimental with sketchy (read: probably no) support in IE and Safari.
Alternatively, many build tools now seamlessly incorporate cache-busting and file "versioning" techniques to address OPs question. WebPack is arguably the current leader in this space.
This might be a good use case for using HTML5's AppCache
You'd probably want to automate some of these steps into your deployment scripts, but here is some code you might find useful to get you started.
First, create your appcache manifest file. This will also allow you to cache resources in the client's browser until you explicitly modify the appcache manifest file's date.
/app.appcache:
CACHE MANIFEST
#v20150327.114142
CACHE:
/appcache.js
/an/image.jpg
/a/javascript/file.js
http://some.resource.com/a/css/file.css
NETWORK:
*
/
In app.appcache, the comment on line #v20150327.114142 is how we indicate to the browser that the manifest has changed and resources should be reloaded. It can be anything, really, as long as the file will look different to the browser from the previous version. During deployment of new code in your application, this line should be modified. Could also use a build ID instead.
Second, on any pages you want to use the appcache, modify the header tag as such:
<html manifest="/app.appcache"></html>
Finally, you'll need to add some Javascript to check the appcache for any changes, and if there are, do something about it. Here's an Angular module. For this answer, here's a vanilla example:
appcache.js:
window.applicationCache.addEventListener('updateready', function(e) {
if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
// Browser downloaded a new app cache.
// Swap it in and reload the page to get the latest hotness.
window.applicationCache.swapCache();
if (confirm('A new version of the application is available. Would you like to load it?')) {
window.location.reload();
}
}
else {
// Manifest didn't changed. Don't do anything.
}
}, false);
Alternatively, if AppCache won't work for your situation, a more ghetto solution would be to create a simple API endpoint that returns the current build ID or last deployment date-time. Your Angular application occasionally hits this endpoint and compares the result to it's internal version, and if different, reloads itself.
Or, you may consider a live-reload script (example), but, while very helpful in development, I'm not sure how good of an idea it is to use live/in-place-reloading of assets in production.
I will tell you my problem first then I will recommend a tentative solution. I wanted to force my user to log out and then log in when a production build is been deployed. At any point in time, there will be two versions of software deployed on production. A version which software which FE knows and a version which Backend knows. Most of the time they would be the same. At any point in time if they go out of sync then we need to reload the client to let the client know that a new production build has been pushed.
I am assuming 99.99% of the time the backend would have the knowledge of the latest version of the deployed software on production.
following are the two approaches which I would love to recommend:-
The backend API should always return the latest version of the software in the response header. On the frontend, we should have a common piece of code that would check if the versions returned by the API and that present on the FE are the same. if not then reload.
Whenever a user logs in. the BE should encode the latest software version in the JWT. And the FE should keep sending this as a bearer token along with every API request. The BE should also write a common interceptor for every API request. which would compare the software version in the JWT received from the API request and the
Maybe you can add hash to your client code file name. eg app-abcd23.js.
So the browser will reload the file instead of get it from cache. or you can just add the hash to url.eg app.js?hash=abcd23 but some browser may still use the cached version.
i know rails has assets-pipline to handle it, but i am not familiar with MEAN stack. there should be some package in npm for that purpose.
And i dont think it is really necessary to use socket.io if you want to notify the user their client code is out of date. you can define your version in both html meta tag and js file,if mismatch, show a popup and tell the user to refresh.
Try to limit your js/files to expire within smaller periodic time, ie: 1 days.
But in case you want something that pop-out and tell your user to reload (ctrl+f5) their browser, then simply make a script that popup that news if you just changed some of your files, mark the ip/session who have just reload/told to reload, so they will not be annoyed with multiple popup.
I was facing the same problem recently. I fixed this by appending my app's build number with my js/css files. All my script and style tags were included by a script in a common include files so it was trivial to add a 'build number' at the end of the js/css file path like this
/foo/bar/main.js?123
This 123 is a number that I keep track of in my same header file. I increment it whenever I want the client to force download all the js files of the app. This gives me control over when new versions are downloaded but still allows the browser to leverage cache for every request after the first one. That is until I push another update by increment the build number.
This also means I can have a cache expiry header of however long I want.
Set a unique key to local storage during the build process
I am using react static and loading up my own data file, in there i set the ID each time my content changes
Then the frontend client reads the key with from local storage
(if the key does not exist it must be the first visit of the browser)
if the key from local storage does not match it means the content has changed
fire line below to force reload
window.replace(window.location.href + '?' + key)
in my case i had to run this same line again a second latter
like
setTimeout( (window.replace(window.location.href + '?' + key))=> {} , 1000)
full code below:
const reloadIfFilesChanged = (cnt: number = 0, manifest: IManifest) => {
try {
// will fail if window does not exist
if (cnt > 10) {
return;
}
const id = localStorage.getItem('id');
if (!id) {
localStorage.setItem('id', manifest.id);
} else {
if (id !== manifest.id) {
// manifest has changed fire reload
// and set new id
localStorage.setItem('id', manifest.id);
location.replace(window.location.href + '?' + manifest.id);
setTimeout(() => {
location.replace(window.location.href + '?' + manifest.id + '1');
}, 1000);
}
}
} catch (e) {
// tslint:disable-next-line:no-parameter-reassignment
cnt++;
setTimeout(() => reloadIfFilesChanged(cnt, manifest), 1000);
}
};
I am running a simple HTML5 app that works in Chrome and Firefox. It uses a web worker, as in:
var worker = new Worker("the/worker/URL/Code.js");
I have experimented for over an hour in IE, and I finally found that the web worker's code is never reloaded. When I get the version that it has, to throw an error, the debugger shows me a completely outdated version of the worker code file, even though, all other files have been reloaded properly.
I flushed the cache, using the standard advice found everywhere: Safety -> Delete Browsing History -> Select items -> Ok -> Wait -> Ctrl+F5 to reload -> BAM, thee debugger still shows 100% the same file as several hours ago (remember that reloading works as expected in Chrome and FF).
When I look at the Network profiler, I see:
URL Protocol Method Result Type Received Taken Initiator Wait Start Request Response Cache read Gap
/js/core/WorkerScriptCode.js (Pending...) GET (Pending...) (Pending...) 0 B (Pending...) webworker 1311 0 0 0 0 31
I don't know why it says "Pending"; I can see that the worker already runs: I can see the work of the worker being done (e.g. the importScripts calls show up, and there are also above mentioned stacktraces). But it simply runs a completely outdated version, even though I flushed the entire cache tens of times.
Is this an uber-bug, or am I being stupid?
The same problem exists in Chrome. In order to get around it you can use a cache buster:
Main Code:
var buster = randomNumberOrBuildNumber;
var worker = new Worker( "the/worker/URL/Code.js?buster=" + buster );
Worker:
importScripts( 'script/to/import.js' + self.location.search );
This will apply ?buster=12345 (or whatever your randomNumberOrBuildNumber is set to) to both the loading of the worker and any importScripts() calls the worker makes.
For me the solution was to enable "Always refresh from server" on the network tab. After I enabled it and refreshed the page, the worker was updated every time I edited it.
Always refresh from server enabling in IE network
I'm developing a simple corporate AngularJS app and was planning on using Firebase for the backend.
The browsers I have to support are IE8 and Chrome (latest).
I have managed to fix all of the IE related quirks in the front end and can successfully retrieve data from my Firebase. As IE8 does not have support for WebSockets I assume it is using long polling. (this is fine performance-wise, the app is very simple and just pulls/updates two or three pieces of data).
Paradoxically, I am seeing the following error in Chrome repeatedly and it is failing to connect to Firebase. I am assuming this is due to the firewall/proxy of the corporate network.
WebSocket connection to 'wss://xxx.firebaseio.com/.es?v=5' failed: WebSocket is closed before the connection is established.
I have no control over the firewall/proxy, so my question is if I can force Chrome to use long polling too, using some sort of config flag when I create my Firebase reference?
I am using a mix of Angularfire and straight Firebase. The app works perfectly in IE so there does not appear to be anything wrong with my code. (Also simple test scripts encounter the same issue)
Update: The app does not work in Chrome (hence my question), so perhaps this is a bug I should raise with Firebase, but regardless a way to force long polling would (presumably) fix my issue.
you can use Firebase.INTERNAL.forceLongPolling(); to force long polling Firebase.INTERNAL.forceWebSockets(); to force web socket
I'm sure there is a better way but I just went in to firebase-debug.js and changed the following function:
fb.realtime.WebSocketConnection["isAvailable"] = function() {
var isOldAndroid = false;
if(typeof navigator !== "undefined" && navigator.userAgent) {
var oldAndroidRegex = /Android ([0-9]{0,}\.[0-9]{0,})/;
var oldAndroidMatch = navigator.userAgent.match(oldAndroidRegex);
if(oldAndroidMatch && oldAndroidMatch.length > 1) {
if(parseFloat(oldAndroidMatch[1]) < 4.4) {
isOldAndroid = true
}
}
}
return!isOldAndroid && fb.WebSocket !== null && !fb.realtime.WebSocketConnection.forceDisallow_
};
to instead read:
fb.realtime.WebSocketConnection["isAvailable"] = function() {
return false
};
This worked, Chrome now long polls automatically and my app can communicate with Firebase. I made the same change to the minified firebase.js but would obviously prefer a more future-proof workaround instead of this hack if anyone can suggest one.