September 7, 2014

Auto Updating HTML5 Cache Manifest for Salesforce 1 / Visualforce

For well known performance reasons, client side caching of web resources like stylesheets, images and scripts is important. This becomes pretty important for those who are creating mobile/tablet Visualforce pages for Salesforce 1. Luckily HTML5  Application Cache is well supported on Visualforce + Salesforce 1 stack, please refer this official document for more details.

Getting started with Cache Manifest is easy, but problem comes when cached resources are changed, and we need to force browser to refresh them ?


Lets start with a sample cache manifest for a visualforce page, which might look similar to this one:


<apex:page contentType="text/cache-manifest" applyHtmlTag="false" 
     standardStylesheets="false" showHeader="false">CACHE MANIFEST

NETWORK:
*

CACHE:
# all static resources to be cached
{!URLFOR($Resource.html5Frameworks,'sf1-bootstrap/css/bootstrap.min.css')}
{!URLFOR($Resource.html5Frameworks,'sf1-bootstrap/js/bootstrap.min.js')}
{!URLFOR($Resource.html5Frameworks,'angular.min.js')}
{!URLFOR($Resource.eventsApp, 'styles/base.css')}
{!URLFOR($Resource.eventsApp, 'partials/view1.html')}
{!URLFOR($Resource.eventsApp, 'partials/view2.html')}
{!URLFOR($Resource.eventsApp, 'app.js')}

</apex:page>


This above app tries to cache resources from regular HTML5 frameworks like css and scripts from angular, bootstrap. Apart from that the Angular app stuff is packed in a zipped resource named "eventsApp", we want to cache scripts, css and partial templates of this resource as well.

Out of above cached resources angular and bootstrap resources will rarely change, unless we upgrade versions. But the eventsApp resources will require updates, in following scenarios:

  1. Whenever developer changes any of the source file in eventsApp i.e. script/css or partials.
  2. A new stable version of app is uploaded as managed package. This should invalidate all cached resources on customer's devices with new one in package.
Forcing browser to update cached resources is easy. Any single bit change in Cache Manifest forces browser to update cached resources. This change is not necessarily a code change in manifest, it could be a comment change as well.

So, we have few ways to achieve cache update in above scenarios:
  1. Add version numbers to resources and update it on any change in app i.e.
    {!URLFOR($Resource.eventsApp, 'v1/styles/base.css')}
    {!URLFOR($Resource.eventsApp, 'v1/partials/view1.html')}
    {!URLFOR($Resource.eventsApp, 'v1/partials/view2.html')}
    {!URLFOR($Resource.eventsApp, 'v1/app.js')}
    
    
  2. Add version number as comment in cache manifest, which looks better than above approach and less effort too i.e. 
    CACHE:
    # Last updated for EventsApp version : 1.1
    # all static resources to be cached
    {!URLFOR($Resource.html5Frameworks,'sf1-bootstrap/css/bootstrap.min.css')}
    {!URLFOR($Resource.html5Frameworks,'sf1-bootstrap/js/bootstrap.min.js')}
    {!URLFOR($Resource.html5Frameworks,'angular.min.js')}
    {!URLFOR($Resource.eventsApp, 'styles/base.css')}
    {!URLFOR($Resource.eventsApp, 'partials/view1.html')}
    {!URLFOR($Resource.eventsApp, 'partials/view2.html')}
    {!URLFOR($Resource.eventsApp, 'app.js')}
    
    
  3. Associate an Apex controller with Cache Manifest page, and let the controller generate a dynamic new comment, in case any static resource is changed, i.e.

    <apex:page contentType="text/cache-manifest" applyHtmlTag="false" 
      controller="ThirdPartyLibsCacheManifestController"
         standardStylesheets="false" showHeader="false">CACHE MANIFEST
    # Last Changed At : {!lastChangedTimeInMillis}
    
    NETWORK:
    *
    
    CACHE:
    # all static resources to be cached
    {!URLFOR($Resource.html5Frameworks,'sf1-bootstrap/css/bootstrap.min.css')}
    {!URLFOR($Resource.html5Frameworks,'angular.min.js')}
    {!URLFOR($Resource.eventsApp, 'styles/base.css')}
    {!URLFOR($Resource.eventsApp, 'partials/view1.html')}
    {!URLFOR($Resource.eventsApp, 'partials/view2.html')}
    {!URLFOR($Resource.eventsApp, 'app.js')}
    
    </apex:page>
    
    

    And here is how controller looks like:

    public class ThirdPartyLibsCacheManifestController {
    
        public Long lastChangedTimeInMillis {get; private set;}
        
        public ThirdPartyLibsCacheManifestController() {
            
            //  Your logic to figure out Namespace prefix, best way could be to query a global apex class
            //  and fetch namespace prefix from it.        
            String nameSpacePrefix = '[...]';
            
            StaticResource latestStaticResource = [SELECT LastModifiedDate 
                                            FROM StaticResource 
                                            Where NamespacePrefix like :nameSpacePrefix 
                                            // add more filters as required here
                                            ORDER BY LastModifiedDate DESC Limit 1];
    
            /*
                Get last mod timestamps for relevant classes and pages which are in cache
                This will make sure that the container page and its class changes leads to a cache auto update as well
            */
            ApexPage latestPage = [SELECT LastModifiedDate FROM ApexPage 
                                Where NamespacePrefix like :nameSpacePrefix 
                                // add more filters as required here
                                ORDER BY LastModifiedDate DESC Limit 1];       
            ApexClass latestClass = [SELECT LastModifiedDate FROM ApexClass 
                                Where NamespacePrefix like :nameSpacePrefix 
                                // add more filters as required here
                                ORDER BY LastModifiedDate DESC Limit 1];
            ApexComponent latestComponent = [SELECT LastModifiedDate FROM ApexComponent 
                                Where NamespacePrefix like :nameSpacePrefix 
                                // add more filters as required here
                                ORDER BY LastModifiedDate DESC Limit 1];
    
            // Collect all timestamps
            List<Datetime> allTimeStamps = new List<DateTime>{
                latestPage.LastModifiedDate,  
                latestStaticResource.LastModifiedDate,  
                latestClass.LastModifiedDate,  
                latestComponent.LastModifiedDate
            };
            
            allTimeStamps.sort();
            // get the latest one
            Datetime latest = allTimeStamps.get( allTimeStamps.size() - 1 );
    
            this.lastChangedTimeInMillis = latest.getTime();
        }
    }
    
    
The last approach is automatic and developer friendly from both coding and packaging reasons. So I would highly recommend using this approach.

Please note a few key points about  "ThirdPartyLibsCacheManifestController":
  1. Apart from static resources, its checking timestamps of classes, pages and components as well. Please remove the which part doesn't makes sense in your app, i.e. remove the ApexClass soql call, if your app is mostly using "Remote Objects" only, with no Apex extensions or controllers.
  2. This line in manifest page is key for this fixture to work
    # Last Changed At : {!lastChangedTimeInMillis}

Hope this helps in simplifying your app's cache manifest stuff !