Automate AppCache offline support in your Webpack build

webpack-logo

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
  • 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
  • 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)
  • 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:

chrome-devtools-application-panel

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

chrome-devtools-network-panel

4 thoughts on “Automate AppCache offline support in your Webpack build

  1. No no no no no, please don’t use AppCache any longer. It’s a terrible mess, it has been deprecated in the current HTML5.1 spec and browsers will remove support shortly (already announced for Firefox and Chrome). Use Service Workers instead.

  2. @Frederic – you clearly didn’t read past the first paragraph. The second paragraph says, “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”.

    Some of us have to develop for the real world, not the real-world-in-3-years-time.

  3. @Brett your post solved a lot of things for me thanks. If an application is built targeting appcache-webpack plugin how easy it will be move to serviceworkers in the future.

Leave a Reply

Your email address will not be published. Required fields are marked *