ここでは、「オフラインファースト」をはじめとする、モバイルなどの回線が不安定な状況を想定したWebアプリケーション設計に関して、キャッシュ方法やよく使われるAPIなどを紹介したいと思います。
「オフラインファースト」とは2012年ごろから提唱されていた、「回線がオフラインになることを前提にアプリケーションの設計を行う思想」のことで、オフライン前提に設計することにより回線状況によらないサービス提供や、効率的な通信をベースにした高速な動作を目指すものです。
それではここからはキャッシュ方法とそれぞれ向いているコンテンツの紹介を行います。
読み込みデータのキャッシュ
ApplicationCacheやlocalStorage、オンメモリキャッシュなどを使って、「サーバから取得したデータをローカルに保持する」ものです。
これは一般に「クライアントサイドキャッシュ」と呼ばれるもので、ネットワークがオンラインの時に読み込んだデータをオフライン時に使用します。
サーバと連動しないコンテンツに向いており、実際のコンテンツ以外でもテンプレートファイルなどの「動的なコンテンツの静的な部分」にも使用されます。
読み込みデータのマージ
これは「読み込みデータのキャッシュ」と大きくは変わりませんが、「読み込みデータのキャッシュ」がオンラインになった際に毎回キャッシュを読みなおすのに比べて、「読み込みデータのマージ」はオンラインになった際にサーバ上で更新があった内容のみダウンロードを行います。
更新があった内容のみダウンロードを行うため、帯域の使用量が少なくなる利点がありますが、サーバとクライアントでデータの更新タイミングを管理する必要があるため、「読み込みキャッシュ」に比べて実装が複雑になります。
「多数の画像ファイルの内、更新があったもののみを再取得する」など、やりとりするデータ量が多いコンテンツに使用されます。
書き込みデータのキャッシュ
書き込み時に回線状況を確認して、オフラインの場合オンラインになるまでデータの送信を遅延させる方法です。
また、オフラインだった時だけではなく、サーバへの通信に失敗した場合、常にデータを再送する形で実装されることもあります。
実装方法としては独自に定義した通信用のコードを使用するほかに、XMLHttpRequestを乗っ取る方法や、使用しているライブラリ(jQuery)などのajaxメソッドをラッピングする方法などがあります。
「投稿」や「保存」などのサーバへの更新処理に使われますが、ゲーム等のタイミングが重要な通信や、「購入」等のサーバ側との連動が、重要な通信では使用されません。
書き込みデータのマージ
これは「読み込みデータのマージ」に書き込みが行われることを想定されたものですが、「書き込みの時系列を保持するか」、「同じデータを同時に編集していた場合にどう整合性を保つか」等によって実装難易度は大きく変わります。
(時系列を保持せず、同時編集時は常に最後の修正が反映されれば良い場合、書き込みデータのキャッシュ実装と大きな差はありません)
これに関してはサーバ側の実装も大きく関わってくるためクライアント側だけで設計するのは難しいですが、もしクライアント側だけで実装したい場合にはGoogle Drive Realtime APIなどの外部サービスの使用も検討してみてください。
オフライン動作
これは単純なデータのキャッシュではなく「ブラウザがオフラインの状態でも動作させる」方法になります。
現状、ブラウザ上でオフライン時にコンテンツを提供するには、基本的にApplicationCacheを使用します。
サーバ側の処理が重要な場合オフライン動作の必要性は高くありませんが、クライアントサイドで完結する要素が多いモバイル向けのツールやアプリケーションなどでは必要性が高くなります。
ここからは実際にキャッシュを実装する際によく使用される機能について紹介します。
Web Storage(localStorage、sessionStorage)
IE8以降の主要なブラウザで使用できる同期型の保存領域です。
多くのブラウザは5MB程度のデータを保持することが可能です。
実装も容易なためよく使われますが、同期型で実装されているためパフォーマンス的な問題も指摘されています。
ローカルストレージに簡単な解決策はない | Mozilla Developer Street (modest)では具体的な問題点や代替案が書かれています。
Cookie
古くから実装されており、サポートブラウザも広いことから、長い間クライアントキャッシュの保存領域としても使用されてきました。
ただ、Cookieに保存した内容はサーバへも送信されてしまうことから、Cookieをキャッシュとして使用する場合、以後の通信すべてで不要なデータをサーバに送ることになるため、大きなデータをキャッシュすることには向いていません。
location.hash
URLの # 以降に文字列を設定し、クライアントキャッシュとして使用する方法です。
サーバサイドへも送信されず、古いブラウザでも動作可能ですが、「ブラウザの履歴に残る」、「URLを共有した際にキャッシュも共有される」、「ブックマーク等にキャッシュも保存される」、「#を使った画面遷移時にキャッシュが破棄される」といった問題があるため、キャッシュとして使用する場合は注意が必要です。
ブラウザキャッシュ
JavaScriptから直接操作するAPIは提供されていませんが、ブラウザキャッシュもクライアントサイドでは重要なキャッシュになります。
具体的には「非表示のimg要素を作成し、img.srcへ画像のURLを設定して先読みさせる」、「見えないiframeで次に読み込むページを先読みする」といった方法で使用されます。
Web SQL Database
ブラウザ内に内蔵されたSQLiteに対して、JavaScriptからAPI経由でSQLを発行することでデータの保存、取得を行うのがWeb SQL Database APIです。
もともとWebKit系のブラウザで実装されており、W3Cでも標準仕様として提案されていましたが、SQLiteへの依存などの問題によりW3C上では廃案になりました。
(ブラウザ上のデータベースに関して – JavaScriptで遊ぶよ – g:javascriptでは廃案になった経緯等がまとめられています)
ただ、モバイル環境では使える環境が多いため、モバイル環境に限ったウェブアプリケーションなどでは使用されることもあります。
Indexed Database(Indexed DB)
標準仕様としての策定が中止されたWeb SQL Databaseに代わり登場したクライアントサイドDBです。
Web SQL DatabaseがあくまでもSQLiteの仕様に準拠しているのに比べて、Indexed DatabaseはJavaScriptからの使用に特化しておりシンプルなObjectやFile objectなどをそのまま格納できます。
Chrome、Firefox、IE10でサポートされるなど、デスクトップブラウザではサポートが広まっていますが、Mobile Safari、Android標準ブラウザでのサポートが行われていないためモバイル環境での使用は広まっていません。
Filesystem API
ブラウザ上にディレクトリなどのFilesystemを操作するAPIを提供するのが、Filesystem APIです。
DOMFileSystem objectやDirectoryEntry objectなどを使って、File objectを階層構造で管理することができます。
このAPIはinput[type=”file”]やD&Dで渡されたファイルを処理する際に使用する「File API」とは違うAPIであることに注意してください。
(Filesystem APIでも単一のファイルを操作する際はFile APIで提供される機能を使用しますが、File APIはあくまでもFilesystem APIから独立した仕様です)
主にChromeで実装されていますが、Chrome以外のブラウザでは実装が進んでいないため、一般的なウェブ環境ではあまり使用されていません。
Firefox に FileSystem API が無いのはなぜか? | Mozilla Developer Street (modest)ではMozillaのFileSystem APIに対するスタンスが書かれています。
ApplicationCache
ApplicationCacheはcache manifestと呼ばれるキャッシュ対象のURLを記述したテキストファイルをhtmlから参照することで、記述されているURLの内容をローカルキャッシュとして使用する仕様です。
ApplicationCacheには以下の様な機能が含まれます。
- ブラウザがオフラインでも、事前にキャッシュした内容を表示する
- ネットワーク通信なしに、cache manifestに書かれたURLのファイルにアクセスする
- ブラウザがオフライン時のみ、代替内容を表示する
- JavaScriptからキャッシュの取得、更新イベントを受け取る
現在、ブラウザがオフライン時でもコンテンツを提供する場合ApplicationCacheが主に使用されますが、ApplicationCacheは「設計に失敗したAPI」と呼ばれるほど使い勝手が悪く、さまざまな問題を抱えています。
ApplicationCacheの問題点
ApplicationCacheに関しての紹介で「さまざまな問題を抱えています」と書きましたが、ここではそのうちの主なものについていくつか紹介したいと思います。
読み込み元htmlがキャッシュされる
ApplicationCacheを使用する場合、仕様上ApplicationCacheを使用しているhtmlもキャッシュされてしまうため、動的なコンテンツを出力してもすぐに反映されません。
そのため、ApplicationCacheを使用する場合は基本的に1枚の静的なhtmlを用意し、動的な部分はすべてJavaScriptを使用して構築することになります。
これはWebサイト構築の当初から想定している場合は問題ありませんが、サーバサイドで動的にコンテンツを生成している既存のWebサイトに対して、あとからApplicationCacheを導入する場合には大きな障害になります。
キャッシュのクリアが困難
ブラウザによってはApplicationCacheをクリアするUIを提供していないものもあり、開発中も含めて一度キャッシュされたApplicationCacheをクリアするのが困難な場合があります。
特にiOSの場合、アプリが独自に使用しているUIWebViewのApplicationCacheをクリアするには、アプリを削除して入れなおす以外の方法がありません。
(アプリが独自のUIを提供している場合を除く)
JavaScriptからキャッシュを操作することができない
JavaScriptからApplicationCacheに関しては「キャッシュ状態の確認」、「サーバ上のデータ確認」、「キャッシュの破棄」を行うことができます。
ただし、キャッシュ操作は全キャッシュに対しての操作のみで、個別のファイルに対しての操作は提供されていません。
そのため、「高速化のために出来るだけキャッシュするファイルを多くしたいが、キャッシュするファイルが多いとキャッシュの更新に時間がかかる」という状況になります。
キャッシュの更新は自動的に行われるため基本的にブラウザ側で操作を行う必要はありませんが、逆に「一部のキャッシュのみ更新したい」、「更新順に優先度を付けたい」、「指定したキャッシュが更新されたらJavaScriptからアクセスしたい」と言った内容は実現できません。
それでも使われるApplicationCache
問題点の多いApplicationCacheですが、現状ブラウザがオフライン時にコンテンツを提供するためにはもっとも現実的な方法であるため、問題点があることを前提で使用されている状況です。
もしApplicationCacheを導入する場合、以下の点に注意してください。
最初に導入するか決断する
ApplicationCacheは仕様的にシングルページアプリケーション(1枚のhtmlをベースにJavaScriptでコンテンツを構築する形式)を想定しており、サーバサイドで毎回htmlを組み立てて出力する形式は想定されていません。
サーバサイドで毎回htmlを組み立てて出力するコンテンツの場合でもiframeを経由するなどして活用することは可能です。しかし、iframe内のApplicationCacheに対する状態管理とiframeと、本体間のキャッシュデータをやりとりするコードが必要になるため、実装は複雑になります。
最悪の事態を想定する
ApplicationCacheは使い方によっては完全にサーバから独立して動く事が可能ですが、場合によってはサーバからの変更を一切受け付けなくなってしまう状態に陥る場合があります。
もしサイト上でApplicationCacheを使用する場合、常にネットワーク上からダウンロードするJavaScriptを読み込ませておき、最悪の場合でもJavaScriptからリダイレクトの指示を出せるようにしておくことをおすすめします。
ここまでキャッシュの設計やAPIに関して紹介してきましたが、ユーザから見た場合キャッシュとは「最良の通信やパフォーマンスが提供されない場合の代替手段」であり、本来存在しないことが求められる技術でもあります。
そのため、もしキャッシュを使用する場合、「ユーザの意図に反した動作にならないか」、「最良の状態でないことをユーザが理解できるか」といったことを意識して使用してください。