There’s a feature of HTML5 called the offline application cache that allows users to run web apps even when they are not connected to the Internet. It works like this: when a user navigates to your web app, the browser downloads and stores all the files it needs to display the page (HTML, CSS, JavaScript, images, etc.). The next time the user navigates to your web app, the browser will recognize the URL and serve the files out of the local application cache instead of pulling them across the network.
The main component of the offline application cache is a cache manifest file that you host on your web server. I’m going to use a simple example to explain the concepts involved, and then I’ll show you how to apply what you’ve learned to the Kilo example we’ve been working on.
A manifest file is just a simple text document that lives on your web
server and is sent to the user’s device with a content type of
cache-manifest
. The manifest contains a list of files that
a user’s device must download and save in order to function. Consider a
web directory containing the following files:
index.html logo.jpg scripts/demo.js styles/screen.css
In this case, index.html
is the
page that will load into the browser when users visit your application.
The other files are referenced from within index.html
. To
make everything available offline, create a file named
demo.manifest
in the directory with
index.html
. Here’s a directory listing showing the added
file:
demo.manifest
index.html
logo.jpg
scripts/demo.js
styles/screen.css
Next, add the following lines to
demo.manifest
:
CACHE MANIFEST index.html logo.jpg scripts/demo.js styles/screen.css
The paths in the manifest are relative to the location of the manifest file. You can also use absolute URLs, like so:
CACHE MANIFEST http://www.example.com/index.html http://www.example.com/logo.jpg http://www.example.com/scripts/demo.js http://www.example.com/styles/screen.css
Now that the manifest file is created, you
need to link to it by adding a manifest attribute to the HTML tag inside
index.html
:
<html manifest="demo.manifest">
You must serve the manifest file with the
text/cache-manifest
content type or the browser will not
recognize it. If you are using the Apache web server or a compatible web server, you can accomplish this by
adding an .htaccess file to your
web directory with the following line:
AddType text/cache-manifest .manifest
If the .htaccess file doesn’t work for you, refer to the portion of your web
server documentation that pertains to MIME
types. You must associate the
file extension .manifest
with the MIME type of text/cache-manifest
. If your website is
hosted by a web hosting provider, your provider may have a control
panel for your website where you can add the appropriate MIME type.
I’ll also show you an example that uses a PHP script in place of the
.htaccess file a little later on
in this chapter.
Our offline application cache is now in working order. The next time a user browses to http://example.com/index.html, the page and its resources will load normally over the network. In the background, all the files listed in the manifest will be downloaded to the user’s local disk (or her iPhone’s flash memory). Once the download completes and the user refreshes the page, she’ll be accessing the local files only. She can now disconnect from the Internet and continue to access the web app.
So now that the user is accessing our files locally on her device, we have a new problem: how does she get updates when changes are made to the website?
When the user does have access to the Internet and navigates to the URL of our web app, her browser checks the manifest file on our site to see if it still matches the local copy. If the remote manifest has changed, the browser downloads all the files listed in it. It downloads these in the background to a temporary cache.
The comparison between the local manifest and the remote manifest is a byte-by-byte comparison of the file contents (including comments and blank lines). The file modification timestamp and changes to any of the resources themselves are irrelevant when determining whether or not changes have been made.
If something goes wrong during the download (e.g., the user loses her Internet connection), then the partially downloaded cache is automatically discarded and the previous one remains in effect. If the download is successful, the new local files will be used the next time the user launches the app.
It is possible to force the browser to always access
certain resources over the network. This means that the browser will not
cache those resources locally, and that they will not be available when
the user is offline. To specify a resource as online only, you use the
NETWORK:
keyword (the trailing :
is essential) in the manifest
file like so:
CACHE MANIFEST index.html scripts/demo.js styles/screen.css NETWORK: logo.jpg
Here, I’ve whitelisted logo.jpg by moving it into the
NETWORK
section of the manifest file. When the user is
offline, the image will show up as a broken image link (Figure 6-1). When he is online, it will appear
normally (Figure 6-2).
If you don’t want
offline users to see the broken image, you can use the
FALLBACK
keyword to specify a fallback resource like
so:
CACHE MANIFEST index.html scripts/demo.js styles/screen.css FALLBACK: logo.jpg offline.jpg
Now, when the user is offline, he’ll see offline.jpg (Figure 6-3), and when he’s online he’ll see logo.jpg (Figure 6-4).
This becomes even more useful when you consider that you can specify a single fallback image for multiple resources by using a partial path. Let’s say I add an images directory to my website and put some files in it:
/demo.manifest /index.html /images/logo.jpg /images/logo2.jpg /images/offline.jpg /scripts/demo.js /styles/screen.css
I can now tell the browser to fall back to offline.jpg for anything contained in the images directory like so:
CACHE MANIFEST index.html scripts/demo.js styles/screen.css FALLBACK: images/ images/offline.jpg
Now, when the user is offline, he’ll see offline.jpg (Figure 6-5), and when he’s online he’ll see logo.jpg and logo2.jpg (Figure 6-6).
Whether you should add resources to the
NETWORK
or FALLBACK
section of the manifest
file depends on the nature of your application. Keep in mind that the
offline application cache is primarily intended to store apps locally on
a device. It’s not really meant to be used to decrease server load,
increase performance, and so on.
In most cases you should be listing all of the files required to run your app in the manifest file. If you have a lot of dynamic content and you are not sure how to reference it in the manifest, your app is probably not a good fit for the offline application cache and you might want to consider a different approach (a client-side database, perhaps).
Now that we’re comfortable with how the offline app cache works, let’s apply it to the Kilo example we’ve been working on. Kilo consists of quite a few files, and manually listing them all in a manifest file would be a pain. Moreover, a single typo would invalidate the entire manifest file and prevent the application from working offline.
To address this issue, we’re going to write a little PHP file that reads the contents of the application directory (and its subdirectories) and creates the file list for us. Create a new file in your Kilo directory named manifest.php and add the following code:
<?php header('Content-Type: text/cache-manifest'), echo "CACHE MANIFEST "; $dir = new RecursiveDirectoryIterator("."); foreach(new RecursiveIteratorIterator($dir) as $file) { if ($file->IsFile() && $file != "./manifest.php" && substr($file->getFilename(), 0, 1) != ".") { echo $file . " "; } } ?>
I’m using the PHP header
function to output this file with the cache-manifest
content type. Doing this is
an alternative to using an .htaccess file to specify the content
type for the manifest file. In fact, you can remove the .htaccess file you created in The Basics of the Offline Application Cache, if you are not using it for any other
purpose.
As you saw earlier in this chapter, the
first line of a cache manifest file must be CACHE
MANIFEST
. As far as the browser is
concerned, this is the first line of the document; the PHP file runs
on the web server, and the browser only sees the output of commands
that emit text, such as echo
.
This line creates an object called
$dir
, which enumerates all the files in the current
directory. It does so recursively, which means that if you have any
files in subdirectories, it will find them, too.
Each time the program passes through this
loop, it sets the variable $file
to an object that
represents one of the files in the current directory. In English,
this line would read: “Each time through, set the file variable to
the next file found in the current directory or its
subdirectories.”
The if
statement here checks
to make sure that the file is actually a file (and not a directory
or symbolic link). It also ignores files named manifest.php or any file that starts with
a . (such as .htaccess).
The leading ./
is part of the file’s full path; the .
refers to the current directory and the /
separates
elements of the file’s path. So there’s always a ./
that
appears before the filename in the output. However, when I check for a
leading .
in the filename I use the
getFilename
function, which returns the filename without
the leading path. This way, I can detect files beginning with
.
even if they are buried in a subdirectory.
To the browser, manifest.php will look like this:
CACHE MANIFEST ./index.html ./jqtouch/jqtouch.css ./jqtouch/jqtouch.js ./jqtouch/jqtouch.transitions.js ./jqtouch/jquery.js ./kilo.css ./kilo.js ./themes/apple/img/backButton.png ./themes/apple/img/blueButton.png ./themes/apple/img/cancel.png ./themes/apple/img/chevron.png ./themes/apple/img/grayButton.png ./themes/apple/img/listArrowSel.png ./themes/apple/img/listGroup.png ./themes/apple/img/loading.gif ./themes/apple/img/on_off.png ./themes/apple/img/pinstripes.png ./themes/apple/img/selection.png ./themes/apple/img/thumb.png ./themes/apple/img/toggle.png ./themes/apple/img/toggleOn.png ./themes/apple/img/toolbar.png ./themes/apple/img/toolButton.png ./themes/apple/img/whiteButton.png ./themes/apple/theme.css ./themes/jqt/img/back_button.png ./themes/jqt/img/back_button_clicked.png ./themes/jqt/img/button.png ./themes/jqt/img/button_clicked.png ./themes/jqt/img/chevron.png ./themes/jqt/img/chevron_circle.png ./themes/jqt/img/grayButton.png ./themes/jqt/img/loading.gif ./themes/jqt/img/on_off.png ./themes/jqt/img/rowhead.png ./themes/jqt/img/toggle.png ./themes/jqt/img/toggleOn.png ./themes/jqt/img/toolbar.png ./themes/jqt/img/whiteButton.png ./themes/jqt/theme.css
Try loading the page yourself in a browser
(be sure to load it with an HTTP URL such as
http://localhost/~
).
If you see a lot more files in your listing, you may have some
extraneous files from the jQTouch distribution. The files LICENSE.txt, README.txt, and sample.htaccess are safe to delete, as are
the directories demos and
extensions. If you see a number
of directories named .svn, you
may also safely delete them, though they will not be visible in the
Mac OS X Finder (you can work with them from within the Terminal,
however).YOURUSERNAME
/manifest.php
Now open index.html and add a reference to manifest.php like so:
<html manifest="manifest.php">
Now that the manifest is generated dynamically, let’s modify it so that its contents change when any of the files in the directory change (remember that the client will redownload the application only if the manifest’s contents have changed). Here is the modified manifest.php:
<?php header('Content-Type: text/cache-manifest'), echo "CACHE MANIFEST "; $hashes = ""; $dir = new RecursiveDirectoryIterator("."); foreach(new RecursiveIteratorIterator($dir) as $file) { if ($file->IsFile() && $file != "./manifest.php" && substr($file->getFilename(), 0, 1) != ".") { echo $file . " "; $hashes .= md5_file($file); } } echo "# Hash: " . md5($hashes) . " "; ?>
Here, I’m initializing a string that will hold the hashed values of the files.
On this line I’m computing the hash of
each file using PHP’s md5_file
function (Message-Digest algorithm 5),
and appending it to the end of the $hashes
string. Any change to the file,
however small, will also change the results of the md5_file
function.
The hash is a 32-character string, such as
“4ac3c9c004cac7785fa6b132b4f18efc”.
Here’s where I take the big string of
hashes (all of the 32-character strings for each file concatenated
together), and compute an MD5 hash of the string itself. This gives
us a short (32 characters, instead of 32 multiplied by the number of
files) string that’s printed out as a comment (beginning with the
comment symbol #
).
From the viewpoint of the client browser, there’s nothing special about this line. It’s a comment, and the client browser ignores it. However, if one of the files is modified, this line will change, which means the manifest has changed.
Here’s an example of what the manifest looks like with this change (some of the lines have been truncated for brevity):
CACHE MANIFEST
./index.html
./jqtouch/jqtouch.css
./jqtouch/jqtouch.js
...
./themes/jqt/img/toolbar.png
./themes/jqt/img/whiteButton.png
./themes/jqt/theme.css
# Hash: ddaf5ebda18991c4a9da16c10f4e474a
The net result of all of this business is that changing a single character inside of any file in the entire directory tree will insert a new hash string into the manifest. This means that any edits we do to any Kilo files will essentially modify the manifest file, which in turn will trigger a download the next time a user launches the app. Pretty nifty, eh?
It can be tough to debug apps that use the offline application cache because there’s very little visibility into what is going on. You find yourself constantly wondering if your files have downloaded, or if you are viewing remote or local resources. Plus, switching your device between online and offline modes is not the snappiest procedure and can really slow down the develop, test, debug cycle.
There are two things you can do to help determine what’s going on when things aren’t playing nice: set up some console logging in JavaScript, and browse the application cache database.
If you want to see what’s happening from
the web server’s perspective, you can monitor its logfiles. For
example, if you are running a web server on a Mac computer, you can
open a Terminal window (Applications→Utilities→Terminal) and run these commands (the
$
is the Terminal shell prompt and should not be
typed):
$ cd /var/log/apache2/ $ tail -f access_log
This will display the web server’s log entries, showing information such as the date and time a document was accessed, as well as the name of the document. When you are done, press Control-C to stop following the log.
Adding the following JavaScript to your web apps during
development will make your life a lot easier, and can actually help
you internalize the process of what is going on. The following script
will send feedback to the console and free you from having to
constantly refresh the browser window (you can store the script in a
.js file that your HTML document
references via the script
element’s src
attribute):
// Convenience array of status values var cacheStatusValues = []; cacheStatusValues[0] = 'uncached'; cacheStatusValues[1] = 'idle'; cacheStatusValues[2] = 'checking'; cacheStatusValues[3] = 'downloading'; cacheStatusValues[4] = 'updateready'; cacheStatusValues[5] = 'obsolete'; // Listeners for all possible events var cache = window.applicationCache; cache.addEventListener('cached', logEvent, false); cache.addEventListener('checking', logEvent, false); cache.addEventListener('downloading', logEvent, false); cache.addEventListener('error', logEvent, false); cache.addEventListener('noupdate', logEvent, false); cache.addEventListener('obsolete', logEvent, false); cache.addEventListener('progress', logEvent, false); cache.addEventListener('updateready', logEvent, false); // Log every event to the console function logEvent(e) { var online, status, type, message; online = (navigator.onLine) ? 'yes' : 'no'; status = cacheStatusValues[cache.status]; type = e.type; message = 'online: ' + online; message+= ', event: ' + type; message+= ', status: ' + status; if (type == 'error' && navigator.onLine) { message+= ' (prolly a syntax error in manifest)'; } console.log(message); } // Swap in newly downloaded files when update is ready window.applicationCache.addEventListener( 'updateready', function(){ window.applicationCache.swapCache(); console.log('swap cache has been called'), }, false ); // Check for manifest changes every 10 seconds setInterval(function(){cache.update()}, 10000);
This might look like a lot of code, but there really isn’t that much going on here:
The first seven lines are just me
setting up an array of status values for the application cache
object. There are six possible values defined by the HTML5 spec,
and here I’m mapping their integer values to a short description
(e.g., status 3 means “downloading”). I’ve included them to make
the logging more descriptive down in the logEvent
function.
In the next chunk of code, I’m setting
up an event listener for every possible event defined by the spec.
Each one calls the logEvent
function.
The logEvent
function
takes the event as input and makes a few simple calculations in
order to compose a descriptive log message. Note that if the event
type is error
and the user is
online, there is probably a syntax error in the remote manifest.
Syntax errors are extremely easy to make in the manifest because
all of the paths have to be valid. If you rename or move a file
but forget to update the manifest, future updates will
fail.
You can view the console messages in desktop Safari by selecting Develop→Show Error Console. You can view the console messages in the iPhone Simulator by going to Settings→Safari→Developer and turning the Debug Console on. When debugging is turned on, Mobile Safari displays a header above the location bar (Figure 6-7) that allows you to navigate to the debugging console (Figure 6-8).
If you don’t see the Develop menu in the Safari menu bar, open your Safari application preferences, click the Advanced tab, and make sure that “Show Develop menu in menu bar” is checked.
If you load the web page in your browser and then open the console, you’ll see new messages appear every 10 seconds (Figure 6-9). If you don’t see anything, update the version number in demo.manifest and reload the page in your browser twice. I strongly encourage you to play around with this until you really have a feel for what’s going on. You can tinker around with the manifest (change the contents and save it, rename it, move it to another directory, etc.) and watch the results of your actions pop into the console like magic.
If you are having serious trouble debugging your offline web app, there is a way to get under the hood and see what’s going on. If you load your app in the iPhone Simulator, it stores the cached resources in a SQLite database that you can peruse with the sqlite3 command-line interface. Of course, having some knowledge of SQL would help here, but you can get pretty far by mimicking the examples in this section.
You will need to install the iPhone SDK from Apple in order to get the simulator. You can get the SDK by registering as an Apple developer at http://developer.apple.com/iphone/. Registration costs nothing, but you will need to enroll in an iPhone developer program (note that an Apple developer is different from an iPhone developer) if you want to submit your apps to the App Store.
On my machine, the iPhone Simulator app cache database is located here:
/Users/jstark/Library/Application Support/iPhone Simulator/User/Library/Caches/com.apple.WebAppCache/ApplicationCache.db
The com.apple.WebAppCache directory and ApplicationCache.db database will not exist unless you have loaded the web application on the iPhone Simulator at least once.
Using the sqlite3 command-line interface,
you can poke around in the database to get an idea of what’s going on.
First, you have to connect to the database. Open the Terminal
(Applications→Utilities→Terminal) and type the commands that follow.
(The $
is the Terminal prompt and should not be
typed.)
$ cd "$HOME/Library/Application Support/iPhone Simulator" $ cd User/Library/Caches/com.apple.WebAppCache/ $ sqlite3 ApplicationCache.db
On the Mac, desktop Safari’s application cache can be found in a directory adjacent to your temporary directory. You can get to it in the terminal with:
$ cd $TMPDIR/../-Caches-/com.apple.Safari/ $ sqlite3 ApplicationCache.db
Once connected, you’ll see something like:
SQLite version 3.6.17 Enter ".help" for instructions Enter SQL statements terminated with a ";" sqlite>
Now you can type SQLite control statements
and arbitrary SQL commands at the
sqlite>
prompt. To see a list of SQLite control
statements, type .help
at the prompt. You’ll see a long
list of commands, of which these are the most important for our
purposes:
.exit Exit this program .header(s) ON|OFF Turn display of headers on or off .help Show this message .mode MODE ?TABLE? Set output mode where MODE is one of: csv Comma-separated values column Left-aligned columns. (See .width) html HTML <table> code insert SQL insert statements for TABLE line One value per line list Values delimited by .separator string tabs Tab-separated values tcl TCL list elements .quit Exit this program .tables ?PATTERN? List names of tables matching a LIKE pattern
To retrieve a list of tables used in the cache manifest database, use the .tables
command:
sqlite> .tables
CacheEntries CacheResourceData CacheWhitelistURLs FallbackURLs
CacheGroups CacheResources Caches
Before I start querying the tables, I’m
going to set .headers
to ON
, which will add
field names to the output, and set .mode
to
line
to make things easier to read. Type the commands
shown in bold (sqlite>
is the SQLite prompt):
sqlite> .headers on sqlite> .mode line
CacheGroups
is the top level of the data model. It contains a row
for each version of the manifest. Type the command shown in bold
(don’t forget the ;
):
sqlite> select * from CacheGroups;
id = 1
manifestHostHash = 2669513278
manifestURL = http://jonathanstark.com/labs/kilo10/kilo.manifest
newestCache = 7
id = 2
manifestHostHash = 2669513278
manifestURL = http://jonathanstark.com/labs/cache-manifest-bug/test.manifest
newestCache = 6
id = 5
manifestHostHash = 2669513278
manifestURL = http://jonathanstark.com/labs/kilo11/kilo.manifest
newestCache = 13
id = 6
manifestHostHash = 2669513278
manifestURL = http://jonathanstark.com/labs/app-cache-3/demo.manifest
newestCache = 14
As you can see, I have four cache groups on my machine. You probably only have one at this point. The fields break down like this:
id
A unique autoincrement serial number assigned to the row. Every time Mobile Safari inserts a row into this table, this number is incremented. If, for some reason, Mobile Safari needs to delete a row, you will see gaps in the sequence.
manifestHostHash
manifestURL
newestCache
This is a Caches
row ID (i.e., a
foreign key to the Caches
table) that indicates which
cache to use.
A column in a database table is
considered a key when it identifies something. For example, a
unique key identifies a row in the table unambiguously. A
primary key is a unique key that has been designated as
the key you use to identify a row. For example,
two columns are potential unique keys because there is only one row
in the CacheGroups
table for any
given value of these columns: id
and manifestURL
. However,
id
is a simple numeric key, and
it’s very fast to make comparisons to it (and it requires less
storage for other tables to refer to it). So, id
is both a unique key and the primary
key for the CacheGroups
table.
A foreign key is a link from one table to another. The cacheGroup
column in the Caches
table (discussed next) identifies a
row in the CacheGroups
table, establishing a
link from a row in one table to the other.
Now, switch to column mode and select all
rows from the Caches
table:
sqlite> .mode column sqlite> select * from Caches; id cacheGroup ---------- ---------- 6 2 7 1 13 5 14 6
The Caches
table has just two fields: id
(primary key for the Caches row), and
cacheGroup
(foreign key that links a Caches id
to a row in the CacheGroups
table). If Safari were in
the process of downloading a new cache, there would be two Cache rows
for the CacheGroup
(one current,
one temporary). In all other cases, there is only one Cache row per
CacheGroup
.
Next, let’s select all of the rows from the
CacheEntries
table:
sqlite> select * from CacheEntries;
cache type resource
---------- ---------- ----------
6 1 67
6 4 68
6 2 69
7 4 70
7 4 71
7 4 72
7 4 73
7 2 74
7 4 75
7 4 76
7 4 77
7 1 78
7 4 79
13 4 160
13 4 161
13 4 162
13 4 163
13 2 164
13 4 165
13 4 166
13 4 167
13 4 168
13 1 169
13 4 170
13 4 171
13 4 172
13 4 173
13 4 174
13 4 175
14 4 176
14 16 177
14 4 178
14 1 179
14 4 180
14 2 181
Not much to look at here. Just two foreign
keys (cache
, which is a foreign key
to the Caches.id
column, and
resource
, which is a foreign key to
CacheResources.id
) and a type
field. I’ll redo that query with a
join
to the CacheResources
table so you can see how the
type corresponds to the actual files. Notice that first I set the
column widths so the URLs don’t get cut off (the ...>
prompt indicates that I pressed Return before finishing the statement
with the ;
terminator):
sqlite> .width 5 4 8 24 80 sqlite> select cache, type, resource, mimetype, url ...> from CacheEntries,CacheResources where resource=id order by type; -- -- --- ----------- -------------------------------------------------------------- 6 1 67 text/htm... http://jonathanstark.com/labs/cache-manifest-bug/ 7 1 78 text/htm... http://jonathanstark.com/labs/kilo10/#home 13 1 169 text/htm... http://jonathanstark.com/labs/kilo11/#home 14 1 179 text/htm... http://jonathanstark.com/labs/app-cache-3/ 6 2 69 text/cac... http://jonathanstark.com/labs/cache-manifest-bug/test.manifest 7 2 74 text/cac... http://jonathanstark.com/labs/kilo10/kilo.manifest 13 2 164 text/cac... http://jonathanstark.com/labs/kilo11/kilo.manifest 14 2 181 text/cac... http://jonathanstark.com/labs/app-cache-3/demo.manifest 6 4 68 image/pn... http://jonathanstark.com/labs/kilo10/icon.png 7 4 70 text/css... http://jonathanstark.com/labs/kilo10/jqtouch/jqtouch.css 7 4 71 image/pn... http://jonathanstark.com/labs/kilo10/icon.png 7 4 72 text/css... http://jonathanstark.com/labs/kilo10/themes/jqt/theme.css 7 4 73 image/pn... http://jonathanstark.com/labs/kilo10/startupScreen.png 7 4 75 applicat... http://jonathanstark.com/labs/kilo10/jqtouch/jqtouch.js 7 4 76 applicat... http://jonathanstark.com/labs/kilo10/kilo.js 7 4 77 applicat... http://jonathanstark.com/labs/kilo10/jqtouch/jquery.js 7 4 79 image/x-... http://jonathanstark.com/favicon.ico 13 4 160 applicat... http://jonathanstark.com/labs/kilo11/kilo.js 13 4 161 text/css... http://jonathanstark.com/labs/kilo11/jqtouch/jqtouch.css 13 4 162 image/pn... http://jonathanstark.com/labs/kilo11/icon.png 13 4 163 image/x-... http://jonathanstark.com/favicon.ico 13 4 165 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/img/button.png 13 4 166 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/ img/chevron.png 13 4 167 text/css... http://jonathanstark.com/labs/kilo11/themes/jqt/theme.css 13 4 168 applicat... http://jonathanstark.com/labs/kilo11/jqtouch/jquery.js 13 4 170 applicat... http://jonathanstark.com/labs/kilo11/jqtouch/jqtouch.js 13 4 171 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/ img/back_button.png 13 4 172 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/img/toolbar.png 13 4 173 image/pn... http://jonathanstark.com/labs/kilo11/startupScreen.png 13 4 174 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/ img/back_button_clicked.png 13 4 175 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/ img/button_clicked.png 14 4 176 text/htm... http://jonathanstark.com/labs/app-cache-3/index.html 14 4 178 applicat... http://jonathanstark.com/labs/app-cache-3/scripts/demo.js 14 4 180 text/css... http://jonathanstark.com/labs/app-cache-3/styles/screen.css 14 16 177 image/jp... http://jonathanstark.com/labs/app-cache-3/images/offline.jpg
Reviewing this list reveals that type 1 indicates a host file, type 2 is a manifest file, type 4 is any normal static resource, and type 16 is a fallback resource.
Let’s switch back to line mode and pull
some data from the CacheResources
table to see what is going on in there. Here’s resource row 73
(if you’re trying this out yourself, replace 73 with a valid id
value from the results you got in the
previous query of the CacheResources
table):
sqlite> .mode line sqlite> select * from CacheResources where id=73; id = 73 url = http://jonathanstark.com/labs/kilo10/startupScreen.png statusCode = 200 responseURL = http://jonathanstark.com/labs/kilo10/startupScreen.png mimeType = image/png textEncodingName = headers = Date:Thu, 24 Sep 2009 19:16:09 GMT X-Pad:avoid browser bug Connection:close Content-Length:12303 Last-Modified:Fri, 18 Sep 2009 05:02:26 GMT Server:Apache/2.2.8 (Fedora) Etag:"52c88b-300f-473d309c45c80" Content-Type:image/png Accept-Ranges:bytes data = 73
If you are familiar with the way HTTP requests work, you’ll recognize that this is exactly the data that you’d need to fake a network response. Here Mobile Safari has all the info needed to serve up a PNG file to the browser (or in this case, to itself; it is storing the information needed to reproduce the behavior of the web server that originally provided the file).
Well, in fact it has all of the info except
for the actual image data. The image data is stored in a blob field in
CacheResourceData
. I’d include it
here, but it’s binary and not much to look at. It’s interesting to
note that even text datafiles (HTML, CSS, JavaScript, etc.) and the
like are stored as binary data in the blob field in CacheResourceData
.
Let’s take a look at the CacheWhitelistURLs
table, which contains all the elements identified in the
NETWORK:
section of the manifest:
sqlite> .width 80 5 sqlite> .mode column sqlite> select * from CacheWhitelistURLs; url cache ---------------------------------------------------------------------------- ------ http://jonathanstark.com/labs/kilo10/themes/jqt/img/back_button.png 7 http://jonathanstark.com/labs/kilo10/themes/jqt/img/back_button_clicked.png 7 http://jonathanstark.com/labs/kilo10/themes/jqt/img/button.png 7 http://jonathanstark.com/labs/kilo10/themes/jqt/img/button_clicked.png 7 http://jonathanstark.com/labs/kilo10/themes/jqt/img/chevron.png 7 http://jonathanstark.com/labs/kilo10/themes/jqt/img/toolbar.png 7
Here we just have the cache id
and the URL to the online resource. If
cache id
7 is requested by the
browser, these six images will be retrieved from their remote location
if the user is online. If the user is offline, they will show up as
broken links because they are not stored locally. It’s worth noting
that the URLs have been fully expanded to absolute URLs, even though
they were listed in the manifest as relative URLs.
And finally, let’s take a look at the
FallbackURLs
table (everything from the FALLBACK:
section of
the manifest):
sqlite> .mode line sqlite> select * from FallbackURLs; namespace = http://jonathanstark.com/labs/app-cache-3/images/ fallbackURL = http://jonathanstark.com/labs/app-cache-3/images/offline.jpg cache = 14
As you can see, I
currently have only one row in the FallbackURLs
table. If cache id
14 is requested by the browser, and any
URLs that begin with
http://jonathanstark.com/labs/app-cache-3/images/
fail
for whatever reason (the user is offline, images are missing, etc.),
the fallbackURL
will be used
instead.
I apologize if this section is a bit complex, but at this point it’s all we’ve got. Maybe browser vendors will implement some sort of user interface that will allow us to browse the application cache—similar to those for the local storage and client-side database—but until that time comes, this is our only option for prowling around in the depths of client-side storage.
In this chapter, you’ve learned how to give users access to a web app, even when they have no connection to the Internet. This offline mode applies whether the app is loaded in Mobile Safari, or launched in full screen mode from a Web Clip icon on the desktop. With this new addition to your programming toolbox, you now have the ability to create a full-screen, offline app that is virtually indistinguishable from a native application downloaded from the App Store.
Of course, a pure web app such as this is still limited by the security constraints that exist for all web apps. For example, a web app can’t access the Address Book, the camera, the accelerometer, or vibration on the iPhone. In the next chapter, I’ll address these issues and more with the assistance of an open source project called PhoneGap.
18.222.196.175