
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 Salesforce1. Luckily HTML5 Application Cache is well supported on Visualforce + Salesforce1 stack; please refer to this official document for more details. Getting started with Cache Manifest is easy, but problems come when cached resources are changed, and we need to force the browser to refresh them. Let’s start with a sample cache manifest for a Visualforce page, which might look similar to this:
<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 and 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 the following scenarios:
- Whenever developer changes any of the source file in eventsApp i.e. script/css or partials.
- 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.
- Add version numbers to resources and update it on any change in the app.
{!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’)} - Add version number as comment in Cache Manifest, which looks better than the above approach and takes less effort, too.
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’)} - Associate an Apex controller with the Cache Manifest page, and let the controller generate a dynamic new comment, in case any static resource is changed.
<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:
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 for both coding and packaging reasons. So I would highly recommend using this approach.Please note a few key points about “ThirdPartyLibsCacheManifestController”:
- Apart from static resources, it is checking timestamps of classes, page, and components as well. Please remove the part which 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.
- This line inthe manifest page is key for this fixture to work.
[xml]# Last Changed At : {!lastChangedTimeInMillis}[/xml]
Hope this helps in simplifying your app’s Cache Manifest!