質問

Recently I discovered a problem relating to caching in my GWT app when running on iOS 6.x devices in web-app (fullscreen) mode. The problem is that iOS seems to ignore my cache policy directives on the bulky permutation files (<hash>.cache.html.)

I have a servlet filter setting cache headers on static resources including *.cache.html files, e.g.:

# Response header snippet
Expires: Fri, 26 Jul 2013 09:58:28 GMT
Cache-Control: public, max-age=8640000
ETag: W/"322107-1359629388000"
Last-Modified: Thu, 31 Jan 2013 10:49:48 GMT

However, as soon as I put in web-app support and add the app to my home screen the iOS device will request the permutation file on every load and sends neither If-None-Match nor If-Modified-Since request headers. Web-app support is added via a <meta> tag:

<meta name="apple-mobile-web-app-capable" content="yes" />

I haven't been able to find this issue documented anywhere and am not certain it is a bug. It is, however, what I have experienced. Caching works as expected in my desktop browser where I can inspect the received headers. Nowhere do I sniff the user agent and distinguish based on this information so all clients will receive the same headers.


I was able to "solve" the immediate problem via an HTML5 cache manifest file as presented in this Google I/O talk: "Google I/O 2010 - GWT Linkers target HTML5 WebWorkers & more" where a custom GWT Linker generates a manifest file containing all generated permutations, e.g.:

CACHE MANIFEST
<hash1>.cache.html
<hash2>.cache.html
...
<hashN>.cache.html

And adds the manifest directly in the host page (<module>.html):

<!doctype html>
<html manifest="[module-path]/offline.manifest">
...

This is all well except that all clients will now have to load all permutations eventhough only one is needed! I my case 18 permutations each ~5MB over 3G or Edge :( Is this really the best solution?

役に立ちましたか?

解決

What I ended up doing was investigating a little further and finding that instead of adding the manifest reference in the host page I could add it to all permutation scripts. The manifest itself can be a generic (and practically empty) file:

Authors are encouraged to include the main page in the manifest also, but in practice the page that referenced the manifest is automatically cached even if it isn't explicitly mentioned. (source)

Thus, I wrote a simple Linker that:

  1. Creates an offline manifest file
  2. Adds a manifest attribute to the <html> tag of all permutation scripts (*.cache.html).

The Linker is called ManifestLinker and is rather simple:

package com.example.linker;

// Imports

@LinkerOrder(Order.POST)
public class ManifestLinker extends AbstractLinker {
    private static final String MANIFEST_FILE = "manifest.nocache.appcache";
    private static final String HTML_FIND = "<html>";
    private static final String HTML_REPLACE = "<html manifest=\"" + MANIFEST_FILE + "\">";

    /* (non-Javadoc)
     * @see com.google.gwt.core.ext.Linker#getDescription()
     */
    @Override
    public String getDescription() {
        return "`Manifest Linker`: Adds AppCache support for static `.cache.html` resources.";
    }

    @Override
    public ArtifactSet link(TreeLogger logger, LinkerContext context, ArtifactSet artifacts) throws UnableToCompleteException {
        ArtifactSet output = new ArtifactSet(artifacts);

        output.add(buildManifest(logger));

        for (EmittedArtifact artifact : artifacts.find(EmittedArtifact.class)) {
            if (artifact.getVisibility() == Visibility.Public && artifact.getPartialPath().endsWith(".cache.html")) {
                logger.log(TreeLogger.TRACE, "Processing file: " + artifact.getPartialPath());

                String cacheHtml = Util.readStreamAsString(artifact.getContents(logger));
                if (cacheHtml.startsWith(HTML_FIND)) {
                    cacheHtml = HTML_REPLACE + cacheHtml.substring(HTML_FIND.length()); // Replace `<html>` tag.

                    output.replace(copyArtifact(logger, artifact, cacheHtml));
                }
            }
        }

        logger.log(TreeLogger.INFO, "Manifest created and linked successfully.");

        return output;
    }

    private EmittedArtifact copyArtifact(TreeLogger logger, EmittedArtifact original, String contents) throws UnableToCompleteException {
        EmittedArtifact copy = emitString(logger, contents, original.getPartialPath());
        copy.setVisibility(original.getVisibility());

        return copy;
    }

    private EmittedArtifact buildManifest(TreeLogger logger) throws UnableToCompleteException {
        StringBuilder builder = new StringBuilder();
        builder.append("CACHE MANIFEST\n")
               .append("# Generated by ")
               .append(getClass().getSimpleName())
               .append(": ")
               .append(System.currentTimeMillis())
               .append(".\n\n")
               .append("NETWORK:\n")
               .append("*\n");

        SyntheticArtifact manifest = emitString(logger, builder.toString(), MANIFEST_FILE);

        return manifest;
    }
}

In my <module>.gwt.xml file I define and add the linker:

<?xml version="1.0" encoding="UTF-8"?>
<module>
    ...
    <define-linker name="manifest" class="com.example.linker.ManifestLinker" />
    <add-linker name="manifest" />
    ...

Also, I make sure that the right content type is set via my web.xml:

...
<mime-mapping>
    <extension>appcache</extension>
    <mime-type>text/cache-manifest</mime-type>
</mime-mapping>
...

The emitted manifest.nocache.appcache is simple too:

CACHE MANIFEST
# Generated by ManifestLinker: 1366702621298.

NETWORK:
*
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top