Autoversion your SPA index.html

The curse of the cache

If you are in an ongoing development stage with a mobile application, you will often come across issues with your customers browser cache. Even if everyone was aware that your site actually works and just needs a hard reset – it isnt always possible. Take for example an Android js/html app launched as a Homescreen App. It seems the only way to force these things to update all of their files is to empty the entire chrome cache.

This is especially a problem when you have a data driven app retrieving its data from something like an odata dataservice. You can update the data service (for example – add a parameter to a get service) and it works perfectly with your front end as the current version. Then someone with a cached version of the client app tries to access the service and it breaks. Not a good look.

On the other hand, the cache exists for a reason. It makes your application far more responsive and reduces the traffic significantly. So, while we want to maintain the cache, we want to flag to the browser when an individual file for the application has changed. If only there was a way…

Forcing a reload

Well, there is. You can add a version to each file retrieved from the server. If the file version number changes, then the browser will get the new version and not use the cache. The easiest way to do this is to specify the version as a query string. To force a refresh, change the version number.

Instead of

<script src=”views/LogOnPopup.js” type=”text/javascript”> </script>

Use

<script src=”views/LogOnPopup.js?v=3″ type=”text/javascript”> </script>

In the case of an SPA in pure JS/HTML, this is made relatively easy because all the files are identified in the index.html. This is the case I will deal with here. If you are dealing with dynamically generated pages using something like asp.net (or heaven forbid php), you will need more exotic solutions than presented here – things like rewrite rules etc. Beyond the scope.

There is a whole stack about this at http://stackoverflow.com/questions/118884/how-to-force-browser-to-reload-cached-css-js-files. A lot of reading, but it helped me figure out a neat solution.

Versioning your index.html

So, if you have a decent sized SPA, you don’t want to be manually versioning your files. It is a PITA, and you will inevitably stuff it up. One solution (as outlined here) is to automagically add the date each file was last modified as the query string as part of the deployment process.

The following code uses c# and HTML Agility Pack (get it from nuget) to parse an index.html file and save it as a new file. I wrote a little windows app that has a GUI – download it here

The guts of the code looks like this;


       private void ParseIndex(string inFile, string addPath, string outFile)
        {
            string path = Path.GetDirectoryName(inFile);
            HtmlAgilityPack.HtmlDocument document = new HtmlAgilityPack.HtmlDocument();
            document.Load(inFile);
            foreach (HtmlNode link in document.DocumentNode.Descendants("script"))
            {
                if (link.Attributes["src"]!=null)
                {
                    resetQueryString(path, addPath, link, "src");
                }
            }
            foreach (HtmlNode link in document.DocumentNode.Descendants("link"))
            {
                if (link.Attributes["href"] != null && link.Attributes["type"] != null)
                {
                    if (link.Attributes["type"].Value == "text/css" || link.Attributes["type"].Value == "text/html")
                    {
                        resetQueryString(path, addPath, link, "href");
                    }
                }
            }
            document.Save(outFile);
            MessageBox.Show("Your file has been processed.", "Autoversion complete");
        }

        private void resetQueryString(string path, string addPath, HtmlNode link, string attrType)
        {
            string currFileName = link.Attributes[attrType].Value;

            string uripath = currFileName;
            if (currFileName.Contains('?')) uripath = currFileName.Substring(0, currFileName.IndexOf('?'));
            string baseFile = Path.Combine(path, uripath);
            if (!File.Exists(baseFile)) baseFile = Path.Combine(addPath, uripath);
            if (!File.Exists(baseFile)) return;
            DateTime lastModified = System.IO.File.GetLastWriteTime(baseFile);
            link.Attributes[attrType].Value = uripath + "?v=" + lastModified.ToString("yyyyMMddhhmm");
        }

 

The addPath parameter is an additional root path to search for each file in the index on to get its modified date. This is because in my solution I have two projects that are compiled into one. The index.html assumes they are both in the current (which they are when compiled), but at development they are not.