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 typetext/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 thehtml
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)
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.
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).
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.
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.
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 theoffline.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
<!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>
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:
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.
@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.
@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.
@Brett thanks, a great help – AppCache is sadly still the only way to build a cross platform offline SPA.