I work on a AngularJS Single-page application which once loaded (all the resources are fetched from the HTTP server) only accesses the REST backend to get/send data. The task is how to make sure users are always using the latest version of the frontend code (.js, .css and .html files) when the users never leave the page and never refresh the browser.
I think there are several options:
the frontend asks the HTTP server every N minutes what is the latest frontend code version
the backend includes the latest frontend code version in every response (e.g. an additional HTTP header) which is inspected by the frontend code
the backend returns a cookie with the latest frontend code version and checks if the version provided by the user is outdated
websocket connection between the browser and the backend (not an option for me)
Do you know other approaches?
you can also use html5 application cache, this way every time something changes, the website will update itself in the first visit, pretty handy and faster than ordinary cache.
Recently I have used the 2nd approach in my Agnularjs SPA. What I did is
In my angularjs APP's .htaccess and my API Server's .htaccess I have added a header:
<IfModule mod_headers.c>
Header add appVersion "1.2"
</IfModule>
Now I will receive this header whenever I am getting anything from API server or frontend(js, html, css, images)
now in my index.html i have defined a variable something like
var frontendVersion = 1.2
I have a http Response interceptor that will look for each response and I have done something like this
myAPP.factory('httpRespInterceptor', function () {
return {
response: function (response) {
var appVersion = response.headers('appVersion')
if(appVersion == frontendVersion) {
console.log('ok')
} else if (appVersion != null) {
location.reload();
}
return response;
}
};
});
I have added a null check because when i am loading a same view twice I am getting Empty Header Object and in my console i don't see any request that is being sent to the server..don't have a clue why this is happening if someone can shed a light it will be grateful :)
so whenever I will make a change in my code/update the application i will change my header appVersion to 1.3 and my index.html variable to var frontendVersion = 1.3
I hope this helps. And also Please let me know if there are best solutions other than this,
Thanks,
Related
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 fetch some data and display it on a web page after that. It works OK.
I want to show the downloading progress on the web page, but the next code does not work in a content script properly.
(async () => {
const response = await fetch("https://i.imgur.com/Rvvi2kq.mp4");
const reader = response.body.getReader();
const contentLength = +response.headers.get('Content-Length');
alert(contentLength) // 0
// other code...
})();
It works properly (shows 2886550, not 0) only if I run it in the context of the page in the same domain (i.imgur.com for this example).
Does it can work (properly) in a content script or at least in a background script? And works when I fetch a data from not the same domain too?
Is there any way to fetch a data (not just download to Downloads folder) for working with it after that and see downloading (fetching) progress?
Upd: The code above* works properly in the background script, but only in Firefox and Chromium 76+ based browsers. It was a Chromium's bug, that the code shows 0.
*It's a part of the code from here.
Imgur.com's server does not send a Access-Control-Expose-Headers header exposing content-length, so download progress indicators are not possible. It could possibly be faster to serve static content with HTTP/2 from your own domain/server since you would not be opening new socket connections to other CDNs. You could also use your server as a proxy to Imjur.com, but you run the risk of them blocking your server's IP
The fetch-progress-indicators examples show various download progress indicators with Fetch.
So I am trying to implement silent renewal in an ionic 3 application, I am still learning about the whole thing so I'll try to be as descriptive as possible please correct me if I am wrong.
Authentication
I am using Implicit flow for my authentication using the In App Browser.
The user is redirected to the authentication server page
After a success authentication I retrieve the id-token & access-token
As I understand the id-token is for authentication and access-token is for authorization with the API.
I have followed this article to help me set this up (Particularly the "Deploy to Mobile Device" section) for my Identity Server.
As done in the article I am using angular-oauth2-oidc to assist me with storing information about redirect links, issuer, etc...
I have attempted to achieve this in 3 different ways, 2 of them work but I don't understand how to retrieve the new access-token and id-token, and the last one returns an error. each of them left me with questions.
First: oauthService
The angular-oauth2-oidc library has a silentRefresh() method, and this github MD describes how to use it using a hidden iframe very vaguely so I barely understand how this works. I have created a silent-refresh.html page in the same directory, but using http://localhost:8000/silent-refresh.html return's a 404. Calling the silentRefresh() method successfully re-authenticates on the server side but the request times-out on the client side, as said in the MD file, something is wrong with the iframe.
Question 1: Does the library create a new iframe and then waits for a response to the http://localhost:8000/silent-refresh.html page but is never found so I never get my response? How to proceed to retrieve my tokens?
Second: iframe
So here I follow this article where I create an iframe and add it to the body. I create my own url (similar to the one made by the silentRefresh() method), and assign it to the src of the iframe. Again on the server side everything is fine and it tries to return the tokens to http://localhost:8000.
public startRenew(url: string) {
this._sessionIframe.src = url;
return new Promise((resolve) => {
this._sessionIframe.onload = () => {
resolve();
}
});
}
Question 2: How to proceed to retrieve the tokens? as it doesn't update my tokens automatically, and I don't see how the code above can do it.
Third: In App Browser
I thought this would work fine as I already know how to process the request using the In App Browser. So I tried to use the same url that worked for the iframe in the 2nd part. However this returns an error: prompt=none was requested. But user is not authenticated. on the server side, which tells that the server can't find the session so it doesn't know who is requesting the token renewal.
Question 3: Is there a specific reason this won't work other than I made a mistake?
NOTE: Took longer than expected to write this will edit this in a bit.
oAuthService
So I looked in to the implementation of the silent refresh, to see what it does. It creates an iframe with a default id, unless you override it. That cleared up a lot of confusion as to what was actually happening.
The next mistake I did was placing the silent-refresh.html file in the src/ folder, that makes it inaccessible to the iframe. Instead the file should have been placed in the www/ folder.
Then inside the iframe I kept getting the net::ERR_CONNECTION_REFUSED error. This was due to CORS and is solved by editing the Client int the Config.cs file on the Authentication Server:
AllowedCorsOrigins = { "http://localhost:8100", "http://<ip>:8100/", "http://<ip>:8100/silent-refresh.html" },
WARNING: This didn't work once I wanted to take this outside serving in the browser (ionic serve) or emulating on my device with ionic cordova run android -c-l-s, these made the root url return something like http://<ip>/. But once ran with ionic cordova run android (without the flags), window.location.href would return file:///<example>/<something>/index.html, using this sort of path (file:///<example>/<something>/silent-refresh.html) as a redirect url caused an ERR_UNSAFE_REDIRECT error to display in the iframe.
Perhaps silentRefresh() is an Angular only solution?
In App Browser
The mistake that caused the original error was having clearsessioncache and clearcache set to yes when creating the browser, caused the session to be wiped so the authentication server didn't know who it was duh, now reduced to this:
const browser = window.cordova.InAppBrowser.open(oauthUrl, '_blank',
'location=no, hidden=yes'
);
Regular redirect url of http://localhost:8100 could be used to catch the request with the new tokens. And the silent-refresh.html page is not needed.
Here is the code for creating the oauthUrl:
buildOAuthRefreshUrl(nonce): string {
return this.oauthService.issuer + '/connect/authorize?' +
'response_type=id_token%20token' +
'&client_id=' + this.oauthService.clientId +
'&state=' + nonce +
'&redirect_uri=' + encodeURIComponent(this.oauthService.redirectUri) +
'&scope=' + encodeURI(this.oauthService.scope) +
'&nonce=' + nonce +
'&prompt=none';
}
The rest of the code is pretty much identical to the originally mentioned article
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 playing with an application cache and having problems with the swapCache function.
I have created the world's simplest cache manifest file:
CACHE MANIFEST
# Timestamp: 2013-03-01 11:28:49
CACHE:
media/myImage.png
NETWORK:
*
Running the application for the 1st time gives me this in the console:
Creating Application Cache with manifest http://blah_blah/offline.appcache
Application Cache Checking event
Application Cache Downloading event
Application Cache Progress event (0 of 1) http://blah_blah/media/myImage.png
Application Cache Progress event (1 of 1)
Application Cache Cached event
All well so far. Then I swap out the image and change the timestamp in the manifest file and get the following:
Adding master entry to Application Cache with manifest http://blah_blah/offline.appcache
Application Cache Downloading event
Application Cache Progress event (0 of 2) http://blah_blah/media/myImage.png
Application Cache Progress event (1 of 2) http://blah_blah/Widget/?invoke=myWidgetFunctionName
Application Cache Progress event (2 of 2)
Application Cache UpdateReady event
At which point the applicationCache.swapCache() function is called giving me a DOM exception 11 error.
MIME types all correctly configured on the webserver.
Anyone got any ideas / can point me in the right direction?
(I have read all the commonly linked appcache stuff online and can't see what am I doing wrong)
Thanks!
EDIT:
As I mentioned in the comments below, setting the expires headers on my web server for *.appcache files to expire immediately seems to have it working although I am still getting the DOM exception error(!?). I found the following blog entry which may help:
Possible Fix for Offline App Cache INVALIDSTATEERR
...but I have no idea how to set the MIME types client side. My google-Fu skillz have deserted me. Anyone?
I had the same issue. For a while I just disabled the cache if the browser was not chrome, but then I decided to try again, setting the mime-type as suggested. Firefox no longer throws an exception when I call swapCache(), and the whole refreshing process now works as expected. The mime-type has to be set server-side since the request isn't initiated from your web page, but the browser, so you have no control over how it reads the response. You have a couple options here. If you are using apache or IIS, you can do as koko suggested. If you are using a framework that handles routing for you and you configure the mapping of URLs to responses, such as rails or a python wsgi server, then you can typically set the content type manually. Here is my snippet of what I am using in my Python app using Bottle.py (WSGI based):
# BEFORE
#bottle.route(r"/<path:re:.+\.(manifest|appcache)>", auth=False)
def serve_cache_manifest(path):
return bottle.static_file(path, SITE_DIR)
# AFTER
#bottle.route(r"/<path:re:.+\.(manifest|appcache)>", auth=False)
def serve_cache_manifest(path):
return bottle.static_file(path, SITE_DIR, mimetype='text/cache-manifest')
Bottle comes with a utility function that handles returning static files that I am using. It has an optional parameter to set the mime-type.
tl;dr If you can't add the mime-type to your server configuration, you can almost always set it in your server side code (if you have
any).
I would suggest trying to comment out the catchall NETWORK whitelist.
NETWORK:
# *
The * would seem to require network access for all file, according to
https://developer.mozilla.org/en-US/docs/HTML/Using_the_application_cache
I commented out all NETWORK entries for now for a simple web application of mine and it works well.