The goal was to deliver an application, in our case a web app to manage our timesheet solution, that works for all main browsers installed on top of Android and iOS. The application is currently used on multiple browser/devices, including but not limited to Chrome, Firefox and Opera running on iPhones, iPads on iOS 10.X and Android 5.X+ devices.
For TL;DR people out there, there is a live working example including all coding and many self explanatory comments. Check the Quick Links.
Why Building An Offline Web App
There are immediate advantages from developing a web app vs a native app:
- Unified development skillset.
- Easier deployment and automatic updates.
- Extensive collection of open source web libraries.
- No dependency on stores nor operating system.
But we were also well aware of the existing constraints:
- Market familiarity to look for apps on the two main app stores
- Limitations to certain device capabilities (Web APIs are catching up though).
But our reason could not be simpler: We strongly believe in the future of web apps. Google is certainly going in that direction. And did you know Steve Jobs himself shared the same vision? A very powerful presentation on web apps from Patrick Kettner (Edge team @ Microsoft) helped us decide.
The Teammates: the web app stack
We have shared this path with a few dependable friends: The rookie Service Worker and the somehow more veteran – and sometimes misunderstood AppCache.
IndexedDB came to the rescue to help facilitate conversations with any of the other two, and to set up the basis for a common ground (sic).
The Game: How to make your app a PWA
1. Using IndexedDB for PWA
Although there are several libraries out there to help you use IndexedDB (Debie, jquery-indexeddb, pouchDB, db.js and more), performance and reliability are among our top priorities so we decided to leverage the standard IndexedDB API following the MDN documentation to have absolute control over the final delivery. This was quite a straightforward process and we could adjust the development to our specific needs.
Although the IndexedDB coding may be worth a second post to share our hits and misses, it works like a charm in both Android and iOS and it is not the goal for this article.
2. Setting it up like a native app
Both Android and iOS provide the mechanism to add an icon to the home screen, so that users can initiate the app with just one touch, like any other native app. Applications run from the home page will use the entire screen and will hide the browser navigation bar. This feature is well documented on Web fundamentals for Android and on tuts+ for iOS. This is an optional step but highly recommended for your web app to be initiated as any other mobile app. It is not however the purpose of our tutorial.
3. Working offline
Making the app work offline for Android is quite straight forward using Service Workers (HTTPS being the only requirement). AppCache setup is also simple but required a bit more caring. Working offline is also what used to make a big difference between a native and a PWA. Not anymore. This is the real deal and the plan is to explain below how we made it work for us. Keep reading. We provide a step by step guide for both scenarios.
3.1 Service Workers for PWA
Before you continue, you need to be familiar with how promises work. If that is not yet the case, a classic read would be this article from Mr. Service Worker (aka Jake Archibald). However,ap in this case, I found the MDN tutorial more self explanatory. Now, to start implementing a service worker solution, an option is to go to MDN again, but my personal recommendation is to have a look at Matt Gaunt’s introduction to Service Workers.
Here’s a quick summary of the steps we followed for our implementation.
1. Register the Service worker
The browser needs to support serviceWorker, so make sure to add the check. This code is to be implemented as starting point of your application (index.html in our example).
2. Enable Offline usage at page load
In order to work offline, you need to cache the files that the application needs to run. You need to create an independent file (ex:sw.js) containing the service worker logic. This file needs to be placed at the lowest folder including all the elements that you want to use offline (i.e. if you have files within multiple folders that you want to cache, the sw file needs to be placed on the root folder including all those upper folders).
By default, the SW will be installed but will only become active the next time the user loads the app. It is possible however to precache all pages from the application while installing the Service worker (even before it becomes active).
Simply capture both the install and activate events for the service worker and add the predefined list of elements to the cache.
This step needs to be performed on the SW code itself.
Declare a variable to specify the list of resources to be cached:
Define handler for the install event and skip the waiting:
Cache list of predefined resources during activate event, using the cache API:
Finally, make sure the current becomes the active service worker:
Even if the user goes offline after loading the page, they will be able to navigate through the entire application (as long as elements are properly defined in the urlsToCache array).
3. Retrieve resources from the cache
Add a listener to all fetch actions from the browser. The SW has the ability to capture the request before sending the response to the browser:
Please note that self in this case does not correspond to window, which does not exist in the service worker context, but to the service worker instance, which is independent from the browser window. Next, use the event to send the response to the browser:
At this stage, we can send the requested object from the cache, if it is already there:
Or we can request it from the network otherwise:
4. Cache all resources
It is optional to add some logic that will cache any other resource accessed by the user while navigating through the app. You even have the option to skip the previous step and only cache resources as the user accesses them.
Within the fetch handled above, if the resource is not already in the cache, we may want to save it there for a later use. For that, we first capture the response from the prior promise:
And then use the cache API to save the response:
name_your_cache_container being the name of the cache that you can see from the Web Console when debugging your app. You can use any string there.
5. Manage updates
If there is a change in the sw.js file, the next time the user loads the app the new SW will be identified and loaded on a waiting state, until the app closes again so the older SW version can be released. By default also, any change to any cached page will not be detected by the app as it goes cache-first for performance reasons.
As result, any change to any web page that you want to be detected by the app needs to be accompanied by a change to the SW file (sw.js) and it will not be captured by the browser until the user refreshes the app.
It is possible however to force this refresh when any change to the SW is detected. (Some authors prefer to display a message informing the user of the new updates and let them decide whether to refresh or not).
In order to automatically refresh, make sure the service worker is updated whenever a new version is available. Include this check in your web page (index.html in our example), right after the SW registration process:
To test this feature, you will have to deploy this app to your own server, make a change to the html page AND the sw.js file, deploy and reload.
3.2 AppCache management
HOWEVER, and this is a big however, if you want to deploy a web application with offline capabilities on iOS, AppCache is so far your only option, and as we learned, sometimes painfully, it works just fine.
Below are the required steps we followed to make it work.
1. Define list of items to be cached
This is a mandatory step with AppCache and it can be defined on an independent file called appcache manifest. You can name it the way you want but a common naming convention is to use: offline.appcache.
Mandatory line to start your file with:
Add a line to maintain the version. This will help the browser to identify when the cache files need to be refreshed. You can enter any text, but the recommendation is to keep a numeric counter to keep track of your versioning internally.
Use ‘#’for comments within the appcache manifest file.
# BeeBole Mobile AppCache v251
Define the list of files to be cached. You can use absolute or relative paths here. The list needs to be defined by CACHE:
If all your resources can be retrieved from the cache while offline, the following line applies. This can be modified if specific resources need to be white listed and retrieved from the network even when offline (sic):
Finally, it is possible to add a fallback resource in case any resource is not available from the cache while offline. Most typical usage is to display an offline image to indicate that there is no access to the network resource. It works by pairs, indicating a fallback resource for each URI. To use the same fallback for all resources, you can use ‘/’.
And this is how it looks all together:
# BeeBole Mobile AppCache v251
2. Call the appcache manifest from the web app.
Simply include the following code within your html page, with the proper manifest file location:
The problem here is that the offline.appcache file needs to be available on page load time, which may not always be the case. There is a trick (as proposed by Patrick Kettner) to load the appcache dynamically, by injecting it in a hidden iframe.
- Create an external file (ex: load-appcache.html) with just the following content
3. Make sure the last version is used
Until this point, you are making sure that the appCache is refreshed whenever a change exists in the manifest file (remember that it is required for the manifest to be modified – increasing the version for instance – each time you modify any file within the application, or else the AppCache will keep feeding from the cached resources.).
The application will use the previously cached resources until the page refreshes. You can force this refresh by detecting the change on the AppCache status.
Simply add a listener to reload the application if a new manifest has been identified. It is required for the manifest to be modified (increasing the version for instance) each time you modify any file within the application:
Our BeeBole Timesheet mobile web app: Live example
As we stated at the beginning, it is possible to have a PWA working offline for both Android and iOS devices. You can see it for yourself: register for a 30 day free trial, no credit card required, and add our timesheet app to your homescreen.
To make this whole post clearer, check out https://beebole.github.io/mobile-app-demo/ where you can find all the above pieces working together.
Best way to test it is to see the app load, then go on airplane mode and navigate through the bunnies. You should see all bunnies even if you did not load them prior to going offline.
Commented sources are available at Github.
FAQs and tips
How do I make sure my Chrome console uses the last version of the files when developing and testing on localhost?
Although the article describes how to make sure the browser will update the last version of the app when deployed to any server, it is true that localhost sometimes behaves its own way when developing with Chrome. The best way to make sure your app is using the last files is to manually clean up the cache files, which you can do via Chrome Console – Application – Cache Storage. Right click on the name of your cache (FILES_CACHE in our example above) and delete. If there is no arrow to expand the Cache Storage level, just right click there and select Refresh.
Why is the browser saying that I am offline when I reload the page when on airplane mode?
AppCache is very sensitive about the pages that are cached, and not very good in communicating when there is something it does not like. As soon as there is anything that the browser is trying to load and it has not been properly defined in the cache manifest, the application will fail entirely and the browser will state that you need a connection. Triple check that you have included all files. A common mistake is to forget about the favicon.ico if you have one in your server.
So far, so good…Leave us a comment with your own questions or examples. Have you built your own mobile web app? Would you mind sharing your thoughts here with us?