247: Offline Apps Part 1
(view original Railscast)
In this episode we’re going to be working with a very simple Rails application called “Grocery List”. It stores a list of items and to use it we just type in the name of the item we want to add and click the “Add Item” button. The page will then reload with the new item added to the list.
While this application works fine while we’re online, if we use it on a mobile phone we might not always have reception and so won’t be able to access our grocery list. It would be useful if we could modify this application so that it works offline and we’ll show you how to do that in this episode.
Introducing The Cache Manifest
We’ll be demonstrating this application in a desktop browser but generally we’ll only want the mobile version of a site to be available offline. Episode 199 [watch, read] covered making a separate version of a site dedicated to mobile devices. Taking this approach means that we can make the mobile version simpler which will make it easier to make it available offline.
To get our site working offline we’ll be using a technique introduced in HTML5 called the cache manifest. One of the best sources of documentation on this is the site Dive Into HTML5. This site has a page on offline applications which covers this subject in some detail and is well worth reading in order to get up to speed on this topic.
The first step is to create a cache manifest file and there is a gem called rack-offline that can help with this. The simplest way to do use it is to add the following route to your application.
match "/application.manifest" => Rails::Offline
This route points to a Rails::Offline
Rack application. When we visit /application.manifest
a cache manifest file will be generated with some default settings. We’ll need to add a reference to the rack-offline gem in our Grocery List application’s Gemfile
and then run the bundle command to ensure that it is installed.
/Gemfile
gem 'rack-offline'
Next we’ll add the application.manifest
route.
/config/routes.rb
match "/application.manifest" => Rails::Offline
If we point a browser at http://localhost:3000/application.manifest
now it will return a cache manifest file. This file contains a list of the files that need to be downloaded for the application to work offline. By default the rack-offline gem will add all of the HTML, CSS and JavaScript files in the /public
directory. This default behaviour will work for small applications but it can be customized and there are details on how to do this in the rack-offline documentation so that you can choose which files will be included in the manifest.
Note that there is a hash at the top of the manifest file. Rack-offline uses this to identify specific revisions of the cache manifest. While our application is in development mode this hash will change every time we reload the page. In production mode, however, this hash only changes when one of the files listed in the manifest changes. When the hash changes this instructs the browser that the cache manifest has changed and that the files it lists need to be downloaded again. This will happen with every request in development mode but in production it will only happen when one of the files changes.
Even though we now have a cache manifest we still need to instruct our application to use it. We do that by adding a manifest
attribute to the HTML tag in the application’s layout file.
/app/views/layouts/application.html.erb
<html manifest="/application.manifest">
Every time someone visits a page in this application now the manifest attribute will ensure that all of the files included in the cache manifest
file are downloaded and also save the current page so that it is available for offline browsing.
If we visit the items page of our Grocery List application then all of the files listed in the manifest file are downloaded along with the items page itself. We can test this by visiting the page and then stopping the Rails server for the application. If we then open a new browser window and visit http://localhost:3000/items
we’ll still see the page even though the server is down as it will be served from the offline cache.
We still have a problem, though. If we try to reload the page we appear to lose the stylesheet.
In fact the browser cannot load the stylesheets or any of the application’s JavaScript files and the reason for this is the timestamps that Rails appends as a querystring to these files.
These timestamps mean that the browser considers the files to be different from the ones specified in the cache manifest and it therefore tries to download them from the server. We can solve this problem by removing the timestamps entirely by setting an environment variable in our application. If we add the line below near the top of the /config/application.rb
file the timestamps will be removed.
/config/application.rb
ENV["RAILS_ASSET_ID"] = ""
To get this working we’ll need to restart the server and visit the page again, refreshing it once of twice to ensure that everything is loaded and cached correctly. Once we’ve done that we’ll be able to stop the server again and the application will now work offline correctly and the JavaScript and CSS will be loaded from the cache as the timestamps will no longer be added to their filenames.
Problems With Caching
For the rest of this episode we’ll start up the server again so that we can simulate online browsing and we’ll show you a few potential problems. One thing that you’ll quickly notice is that changes that you make to your application won’t take effect immediately. Let’s say, for example, that we want to change the text on the button from “Add Item” to “Add”. Of course this is an easy change to make, we just need to change the button’s text in the form’s view code.
/app/views/items/index.html.erb
<%= form_for Item.new do |f| %> <%= f.text_field :name %> <%= f.submit "Add" %> <% end %>
When we save this file and reload the page in the browser we would expect the button’s text to change, but it doesn’t. The reason for this is that the browser returns the cached version of the page, even though the application is online. The browser doesn’t wait to see if the browser is online, it just returns the cached page immediately. Then, in the background it will check the cache manifest to see if it has updated, downloading the updated files if it has. If we reload the page a second time the button’s text will change as the latest version of the page will have be then been downloaded and cached.
This means that when you make a change in development you should get into the habit of hitting reload twice in order to make those changes take effect.
Another problem can occur when the cache points to a file that’s been renamed or removed. For example, if we were to remove the 422.html
file from the /public
directory then the manifest will no longer work. To demonstrate this we’ll change the button’s text again to “Add Another Item”.
/app/views/items/index.html.erb
<%= form_for Item.new do |f| %> <%= f.text_field :name %> <%= f.submit "Add Another Item" %> <% end %>
We can refresh the page as much as we want now and the button’s text won’t update. This is because the manifest is aborting when it gets to the 422.html
file that we removed and therefore never updates the cache for the grocery list page. Even though our application is online we’ll never see our changes.
This kind of problem can be difficult to debug as there’s no sign that an error is happening with the cache manifest. To help with this we can add some JavaScript that listens to the error event on the applicationCache
object. This way we can detect these errors and handle them.
Our application has jQuery installed and so we can add this error detection by adding the following code to our application.js
file.
/public/javascripts/application.js
$(function () { $(windows.applicationCache.bind('error', function () { alert('There was an error when loading the cache manifest.'); })) })
For our application’s problem this alert won’t show as the broken manifest won’t load the updated application.js
file but the next time we have a similar problem we’ll see a JavaScript alert so that we know that something’s wrong.
To fix the cache manifest we just need to restart the web server so that the manifest file updates itself. Now, when we make changes to our application the previous behaviour will be restored and refreshing the page twice will show any changes we make to our application.
There are some problems with offline caching that we might only notice in production and not while we’re developing our application. If we add an item to our grocery list, say, eggs, then the new item is added to the database. The list page is then dynamically updated from the database and we see both items listed. In development mode this seems to work: the item is added to the list and, when we refresh the page, it stays there.
What if we try this in production? To show this we’ll make a change to development.rb
and set cache_classes
to true. (If you’re using this technique in your own applications then you should test thoroughly in production mode.)
/config/development.rb
config.cache_classes = true
We’ll have to restart the server so that this change is picked up. Once we have we can try adding another item to the list, say “chunky bacon”.
The item we’ve added shows up as we’ve just made a POST request but if we reload the page then the item will disappear and this is because the old cached version of the page is shown. The cause of this is the cache manifest file. Now that we’re running our application in production mode the cache’s hash doesn’t change which means that the browser doesn’t know that the page has changed and shows the cached version. This problem is only visible in production mode, not in development.
The big question is: how do we handle dynamic content like this? This is a bigger issue than you might expect. The page can’t be updated dynamically from the server as it’s cached by the browser. What we can do is update the content with JavaScript and that’s what we’ll be looking at in the next episode.