Why would you use AppCache ? An API that is messy, not as advanced as service-workers and moreover, which is being removed from the Web Standards ?…

With the Progressive Web Apps, we hear a lot about service-workers. They are very powerfull for a lot of things (including offline support). Though, they’re not supported on IE nor Safari … 🙁

So, until the rest of the browser vendors catch up, if you want to provide some offline experience to all your users, you’ll have to use AppCache which is still widely supported.

AppCache in a few words

You have to provide a manifest.appcache file, served with the content type text/cache-manifest

file, served with the content type This file will consist of three different parts: CACHE: files that will be explicitly cached after they’re downloaded for the first time (this is the default section) NETWORK: white-listed resources that require a connection to the server FALLBACK: fallback pages the browser should use if a resource is inaccessible

You will reference this manifest.appcache as an attribute on the html tag of the page that will use it

as an attribute on the tag of the page that will use it If the manifest.appcache file is updated, the browser will download the resources listed in this manifest (if not, or offline, it will use the cached resources)

More infos on MDN

AppCache in a SPA

Note: Skip this part if you don’t bother about providing different index.html file whether your users are online or offline.

If you’re developing a SPA, and you want to provide a different index.html entry point whether you are online or offline, you’ll have to use a little trick. You won’t reference your manifest.appcache directly in your index.html but in an other html file that you’ll include in an iframe to your index.html .

That way, the index.html file won’t be cached by default (as the master entry) and you’ll be able to define a fallback in the manifest.appcache

index.html

... <iframe src="./iframe-inject-appcache-manifest.html" style="display: none"></iframe> ...

iframe-inject-appcache-manifest.html

<html manifest="manifest.appcache"></html>

dummy manifest.appcache file

CACHE MANIFEST # v1 (some version id) assets/foo.png assets/bundle.js assets/style.css # more assets ... NETWORK: * # that way, you'll be able to force the fallback of index.html # to an other file when you're offline FALLBACK: . offline.html

Automate AppCache

If your app relies on a large code base, with a build step, you will need to automate this task. You’ll also have to ensure that AppCache doesn’t mess with your development workflow (meaning disabling it when developing).

I will describe the steps I took to automate AppCache support on topheman/rxjs-experiments, a little project using RxJS (no other frameworks involved). The workflow of this project is based on a seed I made and open-sourced: topheman/webpack-babel-starter.

Checkout the App

Step 1 – Define if we should “activate” AppCache

When you’re running webpack in dev-server mode, that means you’re developing, so you want your sources to be kept up to date (you don’t want AppCache to cache them).

webpack.config.js

const MODE_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') > -1 ? true : false; // ... const APPCACHE = process.env.APPCACHE ? JSON.parse(process.env.APPCACHE) : !MODE_DEV_SERVER;// if false, nothing will be cached by AppCache

Step 2 – Generate the manifest.appcache

If the APPCACHE constant was set to true, we generate a manifest containing the files that should be cached, otherwise (in development mode), we generate a manifest that contains nothing, that way, the browser will keep reloading fresh sources (and we’ll also specify bellow not to use any manifest at all in development mode – see step 4).

By doing so, launching your build on the command line with APPCACHE=false will create a manifest that won’t contain anything (usefull if you want to reset cache on testing devices – see README)

For that, we’ll use the appcache-webpack-plugin module.

webpack.config.js

const AppCachePlugin = require('appcache-webpack-plugin'); const plugins = []; // .... /** * AppCache setup - generates a manifest.appcache file based on config * that will be referenced in the iframe-inject-appcache-manifest.html file * which will itself be in an iframe tag in the index.html file * * Reason: So that index.html wont be cached * (if it were the one referencing manifest.appcache, it would be cached, and we couldn't manage FALLBACK correctly) * TLDR: AppCache sucks, but it's the only offline cross-browser "API" */ const appCacheConfig = { network: [ '*' ], settings: ['prefer-online'], output: 'manifest.appcache' }; if (APPCACHE) { // regular appcache manifest plugins.push(new AppCachePlugin(Object.assign({}, appCacheConfig, { exclude: [ /.*\.map$/, /^main(.*)\.js$/ // this is the js file emitted from webpack for main.css (since it's used in plain css, no need for it) ], fallback: ['. offline.html'] }))); } else { // appcache manifest that wont cache anything (to be used in development) plugins.push(new AppCachePlugin(Object.assign({}, appCacheConfig, { exclude: [/.*$/] }))); if (MODE_DEV_SERVER) { log.info('webpack', `[AppCache] No resources added to cache in development mode`); } else { log.info('webpack', `[AppCache] Cache resetted - nothing will be cached by AppCache`); } }

You’ll get something like that, in the manifest.appcache file:

CACHE MANIFEST # 080ee8707955837abbfd manifest.json assets/a9ed8d16d634ec723cafde03f9e02db8.png assets/9977021f17cc1f03ad5524e9da402e71.png assets/e9641b075ba14a849b5ba5500b943d7b.png assets/5baf3575ad4a2925fe1852a677706695.png assets/19a2ed0744623d88714b63564ac1bc16.png assets/9b367318866d4f69c51f1c5e6a533029.png assets/f4769f9bdb7466be65088239c12046d1.eot assets/fa2772327f55d8198301fdb8bcfc8158.woff assets/448c34a56d699c29117adc64c43affeb.woff2 assets/e18bbf611f2a2e43afc071aa2f4e1512.ttf assets/89889688147bd7575d6327160d64e760.svg assets/8c587de6845a752d0202dd14de7e7a99.png bundle-080ee8707955837abbfd.js main-080ee8707955837abbfd.css NETWORK: * FALLBACK: . offline.html SETTINGS: prefer-online

Step 3 – Generate the iframe with the link to the manifest

In my case, I use the html-webpack-plugin to generate html files (such as the index.html ). You might be using other tools for this part (such as gulp or grunt), you’ll still be able to access all webpack’s infos via the stats object.

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin'); const plugins = []; // ... // generate iframe-inject-appcache-manifest.html - injected via iframe in index.html // (so that it won't be cached by appcache - otherwise, referencing manifest directly would automatically cache it) plugins.push(new HtmlWebpackPlugin(Object.assign( {}, htmlPluginConfig, { template: 'src/iframe-inject-appcache-manifest.ejs', filename: 'iframe-inject-appcache-manifest.html' } )));

src/iframe-inject-appcache-manifest.ejs

<html manifest="manifest.appcache"></html>

Step 4 – Generate index.html and offline.html

Both files will be generated from the src/index.ejs template, using the html-webpack-plugin, passing different values to customize the render. In that case, a simple conditional on htmlWebpackPlugin.options.MODE will make the major difference between index.html and offline.html generated files.

As you’ll read in the code:

The iframe containing the code is only added to the index.html (not to the offline.html which is used as a fallback)

(not to the which is used as a fallback) If we’re in development mode, no manifest is referenced on the html tag to ensure that the page won’t use AppCache at all in development

src/index.ejs

<!DOCTYPE html> <html lang="en"<% if (htmlWebpackPlugin.options.MODE_DEV_SERVER) { /** only in devserver, so that there wont be any manifest referenced */ %> manifest="none"<% } %>> ... <body class="<%= htmlWebpackPlugin.options.MODE %>"> ... <% if (htmlWebpackPlugin.options.MODE === 'online') { %> <!-- Requiring a simple html file that references the manifest.appcache, so that index.html isn't the one which references it. Reason: index.html will be added to cache in that case and it wouldn't be possible to properly serve offline.html TLDR: Appcache is fucked up ... --> <iframe src="./iframe-inject-appcache-manifest.html" style="display: none"></iframe> <% } %> ... </body> </html>

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin'); const plugins = []; // ... const htmlPluginConfig = { title: 'Topheman - RxJS Experiments', template: 'src/index.ejs', // Load a custom template inject: MODE_DEV_SERVER, // inject scripts in dev-server mode - in build mode, use the template tags MODE_DEV_SERVER: MODE_DEV_SERVER // ... }; // generate index.html plugins.push(new HtmlWebpackPlugin(Object.assign( {}, htmlPluginConfig, { MODE: 'online' } ))); // generate offline.html plugins.push(new HtmlWebpackPlugin(Object.assign( {}, htmlPluginConfig, { MODE: 'offline', filename: 'offline.html' } )));

Conclusion

I’m very excited about service-workers, but until they are fully supported by browser vendors, it’s difficult to fully rely on them, so AppCache can be a correct fallback for offline support.

I shared my approach, it might not be the best one, at least, I hope it will help some of you. If you have better workflows / solutions, please share them.

Tophe

PS: I decided to write that post after watching The “Progressive” in Progressive Web Apps by Patrick Kettner at the ChromeDevSummit. In his talk, he is presenting the same kind of approach as the one I described above, without all the webpack automation part.

This is a feature I added a few months before to topheman/rxjs-experiments, but never really documented. Seeing that other people came up with the same kind of solution was just what I needed to share mine.

Resources

Use the chrome-devtools “Application” Panel to monitor AppCache:

Use the chrome-devtools “Network” Panel to emulate offline mode: