The Challenge
A client of ours employs sales representatives who give demonstrations of their web applications to potential customers, and they often find themselves in locations without reliable internet access.
Until recently, they gave these demos using a clickable Flash-based .exe that lived on their PC laptop, which enabled them to circumvent the offline challenge, but it was inefficient; updates made to the demo required redistribution of the Flash .exe file to each and every laptop.
Additionally, there was a growing desire to give the demos using tablets rather than laptops, and a fair number of tablets simply do not support Flash.
The Solution
After a bit of experimentation, it was decided we’d recreate the demo using HTML, CSS, a cache manifest, AngularJS, and its ngRoute module. By combining these technologies, we could provide the sales reps with a solution that:
- can be used offline
- can automatically receive updates the next time their device goes online
- informs them when the demo is ready to be used offline
- runs on most modern web-enabled devices
- can appear to be an app on supporting devices
Cache Manifest 101
Before diving into how we built the solution, lets take a small detour to talk about three questions most people have when they start to think about offline websites:
How is this different from the standard cache?
The difference is in what the web browser will cache; if a website does not utilize a cache manifest, the browser will only cache what it’s rendering—nothing more.
For example, if you visit a particular website’s home page, the standard cacheing mechanism will only download the resourcess needed by the home page itself. If you subsequently go offline, request that home page’s URL again (which should render just fine), then try to visit another page in that website, you’ll be asked to connect to the internet before it can be rendered.
However, if that same website utilized a proper cache manifest, our example would have a different ending: each and every page in the website would be rendered just fine whether or not you had an internet connection.
Which browsers support this?
Support is, in our opinion, really good as of :
- IE 10+ for desktop and mobile
- Firefox 3.5+
- Firefox for Android 37+
- Safari 4+
- iOS Safari 3.2+
- Chrome 4+
- Chrome for Android 42+
- Android Browser 2.1+
- UC Browser for Android 9.9+
- Opera 11.5+
- Opera Mobile 12+
- Blackberry Browser 7+
This feature’s support table is a great resource to occasionally check for up-to-date information, tips, issues to consider, etc.
Are there any storage limitations?
Yes, but it’s difficult to determine the exact limits for some environments. Here’s what we know as of :
- IE for desktop and mobile: ~100MB limit
- Firefox for desktop: 500MB limit
- Firefox for Android: 5MB limit
- Safari for desktop: no limit
- iOS Safari: ~25MB limit
- Chrome for desktop and mobile, and Opera (using Blink): Up to 20% of the device’s available disk space
- Android Browser: no limit
If you have additional questions, please feel free to send them our way, or take a look at the Helpful Resources list at the bottom of this article; one of them may contain the answers you’re looking for.
Here’s How We Built the Solution
Note: For the sake of this article, the code blocks that follow are generic, stripped-down versions of what we provided to our client, but they’re certainly enough to get you started, and they’re chock-full of helpful comments that explain how each and every part works.
Make it work offline
To ensure the new demo works as expected without an internet connection, we utilized a cache manifest based on the one below. With it in place, the next time the demo is requested while the sales reps are online, the manifest will instruct supporting web browsers to download every resource needed for it to function normally the next time they’re offline.
CACHE MANIFEST
# Learn more:
# https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache
# -----------------------------------------------------------------------------
# It's necessary to tell web browsers to reconsider this manifest any time the
# website is updated, and you do so by changing *anything* inside the manifest.
# A common way to do this is by simply updating a commented-out string, like a
# date or a version number, or both:
# 2015-05-20:v3
# -----------------------------------------------------------------------------
# This is where you define all of the resources to be cached. Add new and/or
# remove old resourses as needed, keeping each one on its own line. Learn more:
# https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache#Explicit_entries
CACHE:
css/all.css
favicon.ico
fonts/fancy-font.eot
fonts/fancy-font.ttf
fonts/fancy-font.woff
fonts/fancy-font.svg
img/an-image.jpg
img/another-image.png
img/touch/apple-touch-icon.png
img/touch/chrome-touch-icon-192x192.png
img/touch/ms-touch-icon-144x144-precomposed.png
js/vendor/angular-1.2.26.min.js
js/vendor/ng-route.min.js
js/a-script.js
js/another-script.js
views/a.html
views/b.html
views/c.html
views/default.html
# -----------------------------------------------------------------------------
# Resources that must be retrieved from the network. The wild card ensures any
# resource not listed in the cache above will instead be downloaded from the
# network. Learn more:
# https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache#Network_entries
NETWORK:
*
# -----------------------------------------------------------------------------
# Fallbacks. In each row, if the first resource isn't available, the second
# resource is requested. Uncomment and update as needed. Learn more:
# https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache#Fallback_entries
FALLBACK:
# / offline.html
# /desired.html /fallback.html
# /images/desired.jpg /images/fallback.jpg
Tip: quickly add paths to the cache manifest
It’s incredibly inconvenient to type the path to each and every resource into the cache manifest, but there’s an easy way to generate a list of all of them which can then be copied, pasted, and adjusted as needed. Open your favorite command-line application, move into the project’s web root directory, and:
- On OS X: type find . and press return
- On Windows: type dir /b/s and press enter
You’ll be presented with a complete list of paths to each and every resource in the web root directory. Copy the list, paste it into your cache manifest under the CACHE:
heading, then adjust it as needed. For example, you may need to remove the leading forward-slash from each line, or remove a few lines that point to resources you don’t want cached, etc. While the list will surely require some amount of adjustment, doing so is much easier than typing each and every path manually.
By putting our manifest in a file named manifest.appcache, saving it to the web root directory, and referencing it in the <html>
element as follows, we were able to give supporting web browsers the information they need to prepare the demo for offline use:
<html manifest="manifest.appcache">
Wait, every resource?
Right about now you might be wondering, Is it necessary to add every resource to the cache manifest?
As with all things in web development life, the answer is: it depends. You’ll want to think carefully about exactly which resources should be in—and out of—the cache manifest.
We needed to cache nearly everything from our client’s new demo because the entire thing needed to work offline. However, if you’re considering adding a cache manifest to a marketing website, it’s probably—nay, definitely—overkill to send all of its resources to each and every visitor’s device when they may only go to a handful of its pages. Instead, add only those resources that will be requested by a majority of the website’s visitors.
If we did nothing else, the demo would work just fine offline. This project, however, had an additional goal: the demo should behave more like a native application. To achieve that, we turned it into a single-page website using AngularJS and its ngRoute module, a handful of special <meta>
and <link>
elements, and a few environment-specific icons.
Make it a single-page website
All of the heavy lifting is done in the index.html file below. Beyond that, we only need a handful of view files, which are loaded into index.html when they're requested.
index.html
<!doctype html>
<html manifest="manifest.appcache">
<head>
<!-- *******************************************************************
These elements help the website integrate with supporting devices,
making it behave more like a native application.
******************************************************************** -->
<!-- Set the website's width to match the device's width and scale: -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Inform devices that this website is "mobile web app capable": -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Some devices allow you to define the color of various parts of the
user interface when your website is launched: -->
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="msapplication-TileColor" content="#0099cc" />
<meta name="theme-color" content="#0099cc" />
<!-- Define the website's name, which is often utilized when a visitor
creates a home-screen icon for it: -->
<meta name="application-name" content="Website Name" />
<meta name="apple-mobile-web-app-title" content="Website Name" />
<!-- Define an icon for a few specific devices, which are utilized when
a visitor chooses to add your website to their home-screen: -->
<meta name="msapplication-TileImage" content="/img/touch/ms-touch-icon-144x144-precomposed.png" />
<link rel="apple-touch-icon" href="/img/touch/apple-touch-icon.png" />
<link rel="icon" sizes="192x192" href="/img/touch/chrome-touch-icon-192x192.png" />
</head>
<body data-ng-app="offlineDemo">
<!-- (Above) Define the AngularJS app name. -->
<!-- *******************************************************************
Basic navigation:
******************************************************************** -->
<a href="#/a">Thing A</a>
<a href="#/b">Thing B</b>
<a href="#/c">Thing C</c>
<!-- *******************************************************************
Each view file will be loaded into this element:
******************************************************************** -->
<div data-ng-view></div>
<!-- *******************************************************************
AngularJS and its ngRoute module are required:
******************************************************************** -->
<script src="/js/vendor/angular-1.2.26.min.js"></script>
<script src="/js/vendor/ng-route.min.js"></script>
<script>
/* *******************************************************************
Define which view file is loaded into <div data-ng-view></div> based
on the URL requested by the browser. The below code tells AngularJS:
- "When / is requested, load views/default.html."
- "When /a/ is requested, load views/a.html."
- "When /b/ is requested, load views/b.html."
- "When /c/ is requested, load views/c.html."
- "If anything else is requested, redirect to '/', which will
trigger the first .when() rule."
******************************************************************* */
angular.module('offlineDemo', ['ngRoute'])
.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/', {templateUrl: 'views/default.html'})
.when('/a/', {templateUrl: 'views/a.html'})
.when('/b/', {templateUrl: 'views/b.html'})
.when('/c/', {templateUrl: 'views/c.html'})
.otherwise({redirectTo: '/'});
}]);
/* *******************************************************************
This will inform visitors that the website is done downloading itself
onto their device, making it safe to go offline.
******************************************************************* */
window.applicationCache.addEventListener('cached', function() {
alert('Done downloading. It’s safe to go offline now.');
}, false);
/* *******************************************************************
This will inform visitors that the website has been updated, and it
will ask them to accept or reject the updated resource(s). This
occurs when three things happen:
1. The visitor has downloaded your website in the past
2. You have updated one or more resources AND the cache manifest
3. The visitor accesses the website, again, while they're online
******************************************************************* */
window.addEventListener('load', function() {
window.applicationCache.addEventListener('updateready', function() {
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
if (confirm('A new version is available. Load it?')) {
window.location.reload();
}
}
}, false);
}, false);
</script>
</body>
</html>
views/default.html
This is the default view. Whatever is in here will be rendered inside of index.html's
<div data-ng-view></div> element by default, as well as any time the browser requests
an undefined URL.
views/a.html
This is view A. Whatever is in here will be rendered inside of index.html's
<div data-ng-view></div> element any time the browser requests the URL
http://yourdomain.com/#a/
views/b.html
This is view B. Whatever is in here will be rendered inside of index.html's
<div data-ng-view></div> element any time the browser requests the URL
http://yourdomain.com/#b/
views/c.html
This is view C. Whatever is in here will be rendered inside of index.html's
<div data-ng-view></div> element any time the browser requests the URL
http://yourdomain.com/#c/
Wrapping Up
While index.html’s $routeProvider
configuration, above, is a bit simplistic—it will require manual adjustment as the number of views increases or decreases over time—it’s enough to get you started. Fortunately, there are plenty of articles about a dynamic routeprovider.
We hope you find this article helpful, and we’d love to hear from you if you have any questions about offline, single-page websites, or if you’d like to discuss a similar project of your own.
Helpful Resources
We found these resources to be incredibly helpful while building the new demo: