#3 「あまりApplication cache(cache manifest)のことを甘く見ない方がいい」 Advent Calendar 2012

最近色々あってAndroidと心を通わせられるようになってきたago@kyo_ago)です。

このエントリは tech.kayac.com Advent Calendar 2012 3日目の記事です。

Application cache(cache manifest)とは

WHATWGやW3で議論されているHTML5 Offline Web Applicationの仕様の一部です。

細かい仕様等に関しては最後に参考URLをつけたのでそちらをご覧ください。

ここでは色々誤解の多いApplication cacheの使い方をご紹介したいと思います。

使い方

.appcacheの拡張子に対してtext/cache-manifestのMIMEタイプを設定してください。

.appcacheファイルは以下の形式で作成してください。

CACHE MANIFEST:
#更新用ID(日付+連番等)

キャッシュしたいファイルを列挙
10個くらいまでが無難

NETWORK:
*

ここで注意点ですが、「NETWORK:」にはキャッシュ対象ファイル以外をすべてダウンロードするために基本的に「*」を指定してください。
(「NETWORK:」は指定漏れが出るとブラウザが取得できないので、「*」以外の形式で指定するのはおすすめしません)

また、Application cacheは仕様上、html[manifest]を記述しているhtmlファイルもキャッシュしてしまうので注意してください。
HTML5のapplication cacheがつかえない件 - (ひ)メモ

html要素にmanifest属性を追加し、.appcacheファイルを参照してください。
(.appcacheは同じドメイン上に設置する必要があります)

<html manifest="sample.appcache">

静的ファイルメインのサイトでは上記の設定で問題ありません。
動的なサイトに関しては後ほど紹介するJSでキャッシュ確認を行う方法がお勧めです。

データクリアの仕方

よく「Application Cacheはデータをクリアできない」という話を聞きますが、以下の方法でデータのクリアが可能です。

PC環境

基本的にシークレットウィンドウならWindowを閉じるだけでキャッシュが消えるので、検証にはシークレットウィンドウを使うことをおすすめします。
(すいません。普段ほぼスマホ向けのJSを書いているので、PC版ブラウザのデータの消し方がわかりません)

スマホ環境

OS毎に以下の手順でキャッシュを消去することが可能です。

基本的にiOS、Androidの標準ブラウザ、Androidのアプリケーション内WebViewに関してはOSの設定から消去することが可能です。

ただ、iOSのアプリケーション内WebViewの場合、OSの設定から消去することができないため、アプリケーション側で何とかするかアプリケーションを入れなおすしか無いかもしれません。

iOS6、iOS5

「設定」->「Safari」->「Cookieとデータを消去」からデータをクリアできます。

また、iOS6は上記に加えて「設定」->「Safari」->「詳細」->「Webサイトデータ」からサイトごとにデータをクリアすることもできます。

iOS4

「設定」->「Safari」->「履歴を消去」->「Cookieを消去」->「キャッシュを消去」を行い、ホームボタンの2度押しでSafariのプロセスを終了するとデータをクリアできます。
(Cookieを消去する必要はないかもしれません)

Android

「設定」->「アプリケーション」->「アプリケーションの管理」->「すべて」->「ブラウザ」を選択し、「データを消去」するとデータがクリアされます。
(機種によっては日本語環境でも「ブラウザ」ではなく、「Browser」という名前で登録されている場合があります)

他に、アプリ内のWebViewの場合でも「アプリケーションの管理」から該当アプリの「データを消去」で消去可能です。

デモサイト

以下のサイトでApplication Cacheのデモを試すことが可能です。

HTML5 Demo: Offline Application: using manifest

Appcache Facts

また、Fiddler(Windows向けのLocalProxyアプリ)の場合、.appcache拡張子のファイルにはMIMEタイプをtext/cache-manifestで返してくれるのでこれを使っても確認できます。

Tips

ここからは細かい使い方を紹介します。

JSで指定機種だけApplication Cacheを使いたい

以下のようにdocument.writeで書きだしても有効になります。
(Chrome, iOSで確認)

<!DOCTYPE html>
<script>
    document.write('<html manifest="cache.appcache">');
</script>
<head><title></title></head>
<body></body>
</html>

ただ、以下の方法では有効になりませんでした。

  • DOM生成後にJSからdocument.documentElementへmanifestをsetAttributeする
  • html[manifest]にdata schemeでmanifest fileを記述する
  • 動的にiframe作ってその中にdocument.writeでを書き込む

読込元htmlファイルをキャッシュしたくない

無理っぽいです。

「NETWORK:」に読み込み元htmlファイル名を記述してみたりしたんですが、読込元htmlファイルをキャッシュしない方法は見つかりませんでした。

古いキャッシュを使いたくない

以下のJSでキャッシュの更新を確認できるため、キャッシュが最新か確認してから処理を行うことができます。

application cache loading js ? Gist

キャッシュが古い場合にどういう案内をするか

サイトの性質によって違うと思いますが、ざっくり以下の様な案内が考えられます。

  1. 2回読み込み後は最新のデータになるので気にしない
  2. キャッシュが古いことをalertし、ユーザにリロードを促す
  3. キャッシュが古いことをalertし、JSから強制的にreloadする
  4. JSから強制的にreloadする

下に行くほどユーザの選択肢がなくなりますが、キャッシュが古い場合サービスの提供に支障が出るのであれば選択肢になりうると思います。

一番最後の手法はもっともユーザの選択肢がない手法になりますが、サイトの読み込み開始時はローディング画像のみを表示し、キャッシュの更新確認後コンテンツを表示するのであればユーザビリティ的に大きな問題にはならないと思います。
(キャッシュが最新であればページはかなり高速に読み込まれるため)

ただし、この方法を使う場合、キャッシュ対象ファイルが多いと最初の読み込みやファイル更新時に時間がかかるので注意してください。
(スマホ環境であれば10ファイル程度をおすすめします)

jQueryとの連携

jQuery 1.6以降を使っている場合、「古いキャッシュを使いたくない」で紹介したapplication cache loading js ? GistjQuery.holdReady() ? jQuery APIを組み合わせることでDOMReadyをキャッシュ確認後に遅延することができます。

上記のJSを使用しない場合、以下の「JSでイベントを補足するには」を参照してください。

JSでイベントを補足するには

キャッシュの更新はhtml[manifest]が解釈された段階で通信が発生するため、JSが実行されたタイミングではすでにイベントが発行済みの可能性があります。
(特にイベントの監視をするJSをscript[src]で読み込んでいる場合、ダウンロード開始のイベントは取れない可能性が高い)

これに関してはイベントの他にstatus属性から状況が取れるため、statusを監視して発生済みのイベントはその時点で呼び出すことで監視することができます。
(一度発生済みのイベントは発生後に監視を開始してもその後は発生しません)

具体的には以下のようにイベントのbindと同時にstatusの確認もおこなってください。

<!DOCTYPE html>
<html manifest="cache.appcache">
<head>
<script>
    (function () {
        var app = applicationCache;
        // この時点でcheckingは発火済みの可能性がある 
        app.addEventListener('checking', checking, false);
        // 必ず同時にイベントが発火済みでないか確認する
        if (app.status === app.CHECKING) {
            checking();
        }
        function checking () {
            alert('checking');
        }
    })();
</script>
<title></title></head>
<body></body>
</html>

機種依存に関して

手元の環境で確認したところ、上で紹介した.appcache形式に関してはiOS4, 5, 6, Android 2.1, 2.2, 2.3, 4.1、Chromeに関してはキャッシュの更新、APIの呼び出しで問題は見つかりませんでした。

ただ、Android 2.1, 2.2, 4.1に関してはキャッシュ対象ファイルをXHRで取得しようとするとxhr.status === 0になる症状が確認されたので注意してください。

これに関してはxhr.status === 0を成功とみなすことで正常に取得できますが、ライブラリによってはxhr.status === 200以外を失敗とみなすものもあるので、その場合は直接XHRを使用することをおすすめします。

キャッシュファイル数

JSや画像、CSSのダウンロードと違って、ダウンロード後にコードの実行や要素の表示を行わないためダウンロード自体は高速に処理されますが、実際にダウンロード自体は行われるためキャッシュファイル数は多くしないことをおすすめします。

検証方法

ChromeのシークレットウィンドウはCache manifestの領域も独立しているので毎回Windowを開き直すだけで検証できます。
また、consoleにCache manifestの更新状況も表示されるので更新監視も行うことができます。

「実際にキャッシュを使っているか」はChrome Developer Toolsの設定でDisable Cacheを設定した後Networkタブで(from cache)になっていることで確認できます。

一部のファイルだけキャッシュさせるには

cache manifest内の一部のキャッシュだけ更新することはできません。

ただ、JSからキャッシュ更新前後でファイルの内容を比較することで、「対象のファイルが更新された時のみリロードする」といったことは実現できます。

問題点

ここではApplication cacheの問題点を挙げます。

部分更新ができない

キャッシュ対象ファイルのうち一部のファイルのみを更新することはできません。
(すべてのファイルをダウンロードしてからではないとキャッシュの更新が行われません)

このため、必ず「全更新」か「未更新」のいずれかの状態になります。

つまり、キャッシュ対象のファイル数が多いなどの理由でユーザが毎回ダウンロード完了前にページ遷移を行なっている場合、いつまでもキャッシュが更新されないといった状態になる可能性があります。
この理由からもキャッシュ対象のファイル数は多くしないことをおすすめします。

更新状況が取れない

progressイベントはありますが、「なんのファイルを更新中なのか」を取ることはできません。

また、エラーになった場合でも「どのファイルでエラーが発生しているのか」を取ることはできません。

ただ、そもそもJSからキャッシュを追加したり削除したりできないため、これ以上の情報を取得できてもあまりできることはないかもしれません
(結局全部のファイルをダウンロードしないとキャッシュの更新ができないため)

JSからキャッシュの細かい操作ができない

JSからキャッシュのダウンロード状況等を確認するAPIはありますが、キャッシュの追加、削除を行うことはできません。

このため、「JSで取得したファイルをキャッシュに追加する」、「JSから更新確認を行うために一部のファイルのキャッシュを破棄する」、「JSからサーバ上のデータと比較して古くなっているキャッシュのみ更新する」といったことは行えません。

ただ、この点に関してはapplicationCache.update()で.appcacheの更新確認と再取得、applicationCache.swapCache()で取得したキャッシュの更新を行うことは可能です。

よくある質問

危険なんだよね?

「NETWORK:に*を指定する」を守れば危険じゃないです。

また、JSからは強制更新もできるので、「必ずネット上からダウンロードするJS」を仕込むことでいざというときも安全に使うことができます。
(このJSはFALLBACK:で指定しておくほうがいいかもしれません)

でも難しいんでしょ?

ある程度決まった形式であれば簡単です。

ただ、基本的にあまりリッチなAPIではないので、細かい部分を調整するのは難しいです。

ほんとに効果あるの?

キャッシュとしてはかなり効果的です。

ただし、キャッシュ対象のファイルは実際にダウンロード自体は行われるため、キャッシュ対象のファイルが多い状態で更新が頻繁だとかえってページの表示に時間がかかる可能性はあります。

どんなサイトでも使える?

アクセス毎に毎回出力内容が変わるような動的なページに関してはあまりおすすめしません。
(毎回キャッシュを更新する場合、かえって遅くなります)

ただ、あまり更新がない箇所をiframeで切り出してその中だけ使用したり、毎回更新されないページのみ使用するといった方法は可能です。

参考サイト

[HTML5] ApplicationCache めも - l4l

Using the application cache - HTML | MDN

アプリケーション キャッシュ初心者ガイド - HTML5 Rocks

5.7.3 The cache manifest syntax ? 5.7 Offline Web applications ? HTML5
5.7.9 Application cache API ? 5.7 Offline Web applications ? HTML5


明日は社内で神と呼ばれる@mix3神のお話の予定です。