高度なRialsキャッシング
通常、ユーザーに関連するコンテンツによってページ・キャッシングは無効になりますが、これは各ユーザーのコンテンツが少しずつ異なるためです。JavaScript をクッキーと組み合わせて使うと、たとえカスタムのユーザー・データを表示する場合であってもページ・キャッシングを使うことができます。この記事では、 Ruby on Rails での高度なページ・キャッシングについて説明します。
ページ・キャッシングでは Rails がまったく関係しないことを思い出してください。ある意味で、これは良いことです。なぜなら素晴らしいパフォーマンスが得られるからです。Rails は一度 HTML ページを作成してそれをディレクトリーに置き、そしてそれを忘れてしまいます。それ以後アプリケーション・サーバーは、1 サイクルも使うことなく、そのページを提供します。パフォーマンスの観点から見ると、ページ・キャッシングは素晴らしいものです。
私はページ・キャッシングを好んで使用します。そして Rails はページ・キャッシングを単純かつ簡潔にしてくれます。たった 1 行のコードでキャッシュを有効にすることができます。さらに数行のコードを追加すれば、単純にファイルを削除することで、あるいは Rails の上位レベルの API を使うことで、キャッシュを失効させることができます。しかし、ここで問題があります。すべてのサイトにページ・キャッシングが使えるわけではありません。誰が見るかによって変化するデータがページにある場合には、ページ・キャッシングをすることはできません。また、いつページを失効させるかの判断が難しい場合には、ページ・キャッシングは非常に面倒になるかもしれません。
例えば、ChangingThePresent.org (囲み記事を参照) のほとんどすべてのページには、現在ログインしているユーザーに基づいて変化する何らかのユーザー・データがあります。図 1 は、私達の最新のホームページの 1 つのセクションを示しています。(これはまだ未完成であり、変更される可能性があります。) このページは比較的単純な問題を提示しています。ユーザーがログインしたかどうかを判断できれば、Flash や JavaScript、DHTML、あるいは他のブラウザー・ベースのコードを使って即座にビューをカスタマイズできるのです。この図を見ると、ログインしたユーザーは、ログアウトするか、あるいは自分のプロファイルを見ることができ、またログアウトしたユーザーは、サインアップするか、あるいはログインできることがわかります。
図 1. ChangingThePresent.org のログイン・ビューとログアウト・ビュー

単純な Rails アプリケーションの作成に役立つ記事は、何十というほどあります。このシリーズでは、単純なブログを作成するという基礎を越え、すべての Rails サイトが解決すべき問題に入り込みます。ここでは Rails の最適化方法や、サイトをより安定にするための方法について学びます。また、Rails の基本的な制約を、プラグインを追加することで回避する方法も学びます。このシリーズの各記事を読み終わるごとに、現実の世界で Rails サイトを動作させるための方法について、さらにもう少し知ることができるはずです。
図 2 は、ユーザーのデータを、もう少し高度なビューで示したものです。私達はこのビューをサイト全体で使っています。図 2 の 2 つのビューは大きく異なっています。ページ・キャッシングを処理するためには、このような違いのすべてに対応する必要があります。ログインした各ユーザーに対して、ページのログアウト部分を、ログインしたユーザーのログイン ID と写真を表示する部分で置き換える必要があります。各ユーザーはそれぞれ異なるデータを持つため、このコンテンツ部分をキャッシングしようとすると、もう 1 つ別のレベルの課題が提起されることになります。
図 2. 2 つの明確に異なるビュー

この動作は ChangingThePresent.org に独特のものではありません。ユーザーのエクスペリエンスをパーソナライズし始めた瞬間から、変更されることがない Rails ページに対するキャッシングの使い方が制限されます。しかし少しカスタマイズすれば、実際にはこれらのページを非常に容易にキャッシュすることができるのです。
これらの問題を解決するための方法はいくつもあります。私にとって最も魅力的な方法は次の通りです。
- Rails フレームワークの制約の中で処理を行い、ページ・キャッシングの代わりにフラグメント・キャッシングを使います。
- ページの大部分をロードし、次に JavaScript と Ajax を使って、そのページの一部の動的な部分をロードします。サーバー・サイド・コードはユーザーがログインしているかどうかを検出し、そして適当な部分を Ajax で描画します。
- 何らかのユーザー状態 (例えばユーザーがログインしているかどうか、など) を、クライアント・サイドのクッキーに保存します。次にクッキーの内容に基づいて、JavaScript でページの表示を動的に変更します。
この 3 つの方法の中で、私は文句なしに 3 番目の方法を好みます。その理由は、最初の方法と 2 番目の方法では、いやでも Rails アプリケーションが入り込んでくるからです。究極的なスケーラビリティーを望む場合には、可能な限り、静的なコンテンツを処理したいものです。この記事では、3 番目の方法に焦点を絞ることにします。ただしこの方法を、失っては困る非常に機密性が高いもの (ICBM (大陸間弾道ミサイル) の発射コードやクレジットカード番号など) を保存するためには使わないでください。私達の限定されたデータ・セットに関しては、この解決方法で問題はありません。
show-and-tell か hide-and-seek か
私はホームページのキャッシングを初めて試したとき、単純にリンクを JavaScript で置き換えてしまうという決断をすることもできました。この方法は、show-and-tell と考えることができます。ログインしたユーザーに関して知っていることに基づいて、JavaScript を使って Web ページの一部を選択的に置き換えたり、あるいは選択的に挿入したりすることで、そのユーザーに対して適切な話をすることができるのです。これをさらに分解すると、次のようになります。
- すべてのユーザーに共通な要素のみを持つ Web ページを作成します。
- ユーザーがログインしたら、そのユーザーに関するいくつかのデータ (ログインなどのデータ) をクッキーに置きます。
- 次に、クッキーの内容に基づいて、JavaScript を使って HTML フラグメントを挿入することでページの残り部分を描画します。
ChangingThePresent のホームページの場合、ログインしたユーザーに基づいて表示すべきリンクのセットは 2 つしかなかったため、show-and-tell の方法では過剰でした。そこで私は、私が hide-and-seek と呼ぶ、2 番目の方法を選びました。この方法では、すべてのユーザーに共通のページ要素をすべて表示し、そして変化する表示部分として考えられる全データを、隠しバージョンとして持ちます。これが hide-and-seek の hide の部分です。次に、ユーザーのロールに基づいて、JavaScript を使って表示用の文書の中でそのユーザー用の表示部分を見つけます。これが seek の部分です。考えられる全バージョンのデータを用意しておくのは過剰と思われるかもしれませんが、さまざまなセキュリティー・ロールに応じてさまざまな機能を選択的に有効にしたい場合には、これは実は非常に一般的です。そして hide-and-seek は ChangingThePresent のホームページには最適なのです。この方法を実現するためには次のことを行います。
- すべてのユーザーに共通の要素のみを持つ Web ページを作成します。
- ユーザー群をタイプごとに分けます。各ユーザー・タイプに対してコンテンツのバージョンを追加します。この場合では、ChangingThePresent のホームページに関するユーザー・タイプは、ログインしたユーザーとログアウトしたユーザーです。最初はこのコンテンツを隠します。
- ユーザーがログインしたら、一群のユーザーを区別する何らかのデータ (ユーザー・ロールやログイン状態など) をクッキーに置きます。
- ユーザーがページにアクセスしたら、そのユーザー・タイプに応じたバージョンのコンテンツを選択的に表示します。
hide-and-seek を実装する
ChangingThePresent のホームページの場合、hide-and-seek の実装は驚くほど簡単です。図 1 で、このホームページがユーザーのアカウントに関連するいくつかのリンクを示す部分を持っていたことを思い出してください。これらのリンクは、ユーザーがログインしているかどうかによって変化します。最初の仕事は、このページのすべての共通コンテンツを作成することです。ここではその説明を省略します。2 番目の仕事は、ユーザーがログインしているかどうかによらず、すべてのユーザーに対するすべての動的コンテンツを表示することです。
- リスト 1. 動的コンテンツの全バージョンを 1 つのビューの中に作成する
<div id='logged_out'> <%= link_to "login", :controller => 'members', :action => 'login' %> <br /> <%= link_to "register", :controller => 'members', :action => 'signup' %> </div> <div id='logged_in' style="display: none;"> <%= link_to "your profile", :controller => 'profiles', :action => 'show' %> <%= link_to "logout" , :controller => "members", :action => "logout" %> </div>
my profile リンクに注目してください。このリンクは最初、あるユーザー固有のプロファイルを指していました。しかしそれでは私達のホームページのキャッシングが壊されてしまいます。そこでこのリンクを、ユーザー ID を持たない索引アクションを指すようにします。そうするとこの索引アクションは、このユーザーを正しいプロファイル・ページにリダイレクトします。
- リスト 2. ユーザーを正しいプロファイル・ページにリダイレクトする
def index redirect_to my_profile_url end
リスト 2 の my_profile_url は、ユーザーのタイプに基づいて適切なプロファイル URL を判断するメソッドです (ユーザー・タイプには、celebrity、advisor、member があります)。ユーザー・タイプごとに別々のプロファイル・ページがあります。この時点でアプリケーションは完全に機能しますが、ここには 4 つのリンク (logged_in 用と logged_out 用それぞれに 2 つのリンク) があります。
- login
- register
- your profile
- logout
次のステップは、現在のユーザー・タイプを保持するクッキーを取得することです。ChangingThePresent の場合には、現在のログイン ID を持つクッキーを、ログイン時に作成します。そしてログアウト時にクッキーを破棄します。
- リスト 3. ログイン時とログアウト時にクッキーを作成し、破棄する
def login if request.post? self.current_user = User.authenticate(params['user_login'], params['user_password']) ... if logged_in? set_cookies ... end end def logout end private def set_cookies cookies[:login] = current_user.login cookies[:image] = find_thumb(current_user.member_image) end def logout cookies.delete :login cookies.delete :image ... end
リスト 3 の logged_in? は、カレント・ユーザーがログインしていると真を返すプライベート・メソッドです。上記の Rails メソッドは、ユーザーがログインすると 3 つのクッキーを作成し、ログアウトするとそれらを削除します。データについて心配する必要はありません。まだデータは必要ないのです。ここでは単に、あるユーザーがログインしているかどうかを、Rails フレームワークを呼び出さずに判断できるようになったことを理解してください。ただし、クッキーの有効期限はサイトの有効期限のポリシーと必ず一致する必要があります。この場合は一致しているため、ページ・キャシングの準備は完了です。
次のステップは、ユーザーのクッキーに基づいて適切なエントリーを選択的に表示したり隠したりすることです。ここでは public/javascripts/application.js に次の JavaScript を追加しました。
- リスト 4. login の div を表示したり隠したりする JavaScript サポート
function readCookie(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i=0;i < ca.length;i++) { var c = ca[i]; while (c.charAt(0)==' ') c = c.substring(1,c.length); if (c.indexOf(nameEQ) == 0) { return c.substring(nameEQ.length,c.length); } } return null; } function handle_cached_user() { var login_cookie = readCookie('login'); var logged_in = document.getElementById('logged_in'); var logged_out = document.getElementById('logged_out'); if(login_cookie == null) { logged_in.style.display = 'none'; logged_out.style.display = 'block'; } else { logged_out.style.display = 'none'; logged_in.style.display = 'block'; } }
最初の関数は JavaScript からクッキーの値を読み取り、2 番目の関数は DOM を操作します。Prototype ライブラリーを使うと、このコードを単純にすることができますが、ここではすべての読者に明確にわかるように基本的な DOM 参照を含めています。最後のステップは、ページがロードされた時に JavaScript 関数を呼び出すことです。そこでレイアウトに下記を追加します。
- リスト 5. ページをロードする際に JavaScript 関数を呼び出す
<script type="text/javascript"> window.onload = function() { handle_cached_user(); <%= render_nifty_corners_javascript %> <%= yield :javascript_window_onload %> } </script>
これは単純な JavaScript です。ページをロードする際に handle_cached_user 関数をロードすると、今度はこの関数が適切なビットを表示したり隠したりします。これでコントローラーに下記を追加すれば、安全にページ・キャッシングを有効にすることができます。
caches_page :index
そしてこれは完璧に動作します。何らかの理由でフロント・ページを失効させたい場合には、やはり定期的にキャッシュからそのページを削除する必要があります。そのためには単純に、定期的に public/index.html を削除します。hide-and-seek の方法は、いくつかのユーザー分類を持つページに対しては有効ですが、図 2 に示すユーザー部分には使えません。この部分については、hide-and-seek の方法と show-and-tell の方法の両方を組み合わせて使う必要があります。
show-and-tell を実装する
図 2 をもう 1 度見てください。hide-and-seek を使って、(ユーザーがログインしているかどうかによって) その部分の適切なバージョンを選択し、そして次に show-and-tell の方法を使って、(先ほどリスト 3 の 4 行目と 5 行目で作成したクッキーの内容に基づいて) ページの動的部分にデータを追加します。show-and-tell では、1 人のユーザーに合致するようにわざわざページの要素を変更していることを思い出してください。
まず、ログアウトしたユーザー用の部分とログインしたユーザー用の部分という各部分を描画する、静的なコンテンツがあります。ここではユーザーがログアウトしているとします。そこで、display: none というスタイルを付加することで logged_in の div を隠します。後で、それらを必要に応じて JavaScript を使って表示したり隠したりすることができます。各 div の識別に logged_in と logged_out という先ほどと同じ 2 つの名前を使っているため、ホームページ用に作成した JavaScript を変更する必要がないことに注意してください。
- リスト 6. ログイン部分とログアウト部分の両方を描画する
<div class="boxRight sideColumnColor"> <div id='logged_in'> <%= render :partial => 'common/logged_in' style="display: none; %> </div> <div id='logged_out'> <%= render :partial => 'common/logged_out' %> </div> </div>
次に、下記は logged_in 部分のコンテンツです。動的コンテンツを含む各 HTML コンポーネントが ID を持っていることに注意してください。そのため、後で JavaScript を使って各コンポーネントを見つけ、置き換えることができます。
- リスト 7. logged_in 部分を表示する
<div id='logged_in' style="display: none;"> <%= link_to %(<span class="mainBodyDark">Hi, </span>) + %(<span class="textLarge mainBodyDark"><b id='bold_link'>) + "my_login" + %(</b></span>), {:controller => 'profiles', :action => 'show', :id => 'my_login'}, {:id => 'profile_link'} %> <br/> <div id='picture_and_link'> <a href="http://member/my_login" id='link_for_member_thumbnail'> <img id='member_thumbnail' alt="Def_member_thumbnail" src="/images/default/def_member_thumbnail.gif" /></a> </div> <div id="not_mine">Not my_login?</div> <br/> <%= image_button "logout", :controller => "members", :action => "logout" %>
Rails をよく知っている人であれば、いくつかのカスタム・ヘルパー関数に気付いたと思います。これを見ると、ページがロードされるごとに JavaScript を使って置き換えなければならない明確な動的コンテンツが 4 つあることがわかります (つまり 3 ヵ所にログインがあり、1 ヵ所にメンバーの画像があります)。この JavaScript コードは、handle_cached_user 関数に対する変更と、動的ユーザーに対するページの更新を処理するための新しいメソッドで構成されています。私はこの記事のために、コードを少し単純にしました。下記の関数を application.js ファイルに追加します。
- リスト 8. ユーザー部分の要素を置き換える
function handle_user_partial() { var login_cookie = readCookie('login'); var image_cookie = readCookie('image');
var profileLink = document.getElementById('profile_link'); profileLink.href = '/member/' + login_cookie; document.getElementById('bold_link').firstChild.nodeValue=login_cookie; document.getElementById('not_mine').firstChild.nodeValue="Not " + login_cookie + "?"; document.getElementById('link_for_member_thumbnail').href="/member/" + login_cookie; document.getElementById('member_thumbnail').src=image_cookie.replace(/%2[Ff]/g,"/"); document.getElementById('member_thumbnail').alt=login_cookie; }
リスト 8 の JavaScript 関数は、まずクッキーを読み取り、そして DOM ツリーの 1 つの部分 (profile_link という、カレント・ユーザーのプロファイルへのリンク) を取得します。次に、handle_user_partial は下記を行います。
- ログインしたユーザーの名前 (login_cookie に保存されています) を my_login で置き換え、そのユーザーのプロファイル・ページに対する正しい URL を作成します。
- ログインしたユーザーの名前を、ログインしたユーザーを表す太字のテキストを含む DOM 要素の中に挿入します。
- 「Not login?」という単純な文を、login 部分に logout キャプションを含む DOM 要素の中に挿入します。
- メンバーの画像を含む dom 要素を見つけ、汎用の画像のための画像 URL を、(image_cookie の中にある) メンバーの画像の URL で置き換えます。
- また、万が一画像が表示されない場合に備えて、画像の alt タグを login 名で置き換えます。
DOM をナビゲートする際には、直接 DOM 要素に行く必要がある場合や、 (例えばテキストを処理している場合など)、その要素の特定の子が必要になる場合があります。ここでは、DOM 項目の中で見つけたい最初の子要素を、firstChild 関数を使って見つけています。Prototype ライブラリーを使うと、わかりやすい構文を使って少し容易に特定の DOM 要素を処理することができます。しかしこの点はこの記事の対象範囲外なので、触れないことにします。
既にすべてのクッキーを作成したので、最後のステップは既存の handle_cached_user 関数から JavaScript を呼び出すことです。この関数が public/javascripts/application.js の中にあることを思い出してください。
- リスト 9. handle_cached_user に handle_user_partial 関数を追加する
function handle_cached_user() { var login_cookie = readCookie('login'); var logged_in = document.getElementById('logged_in'); var logged_out = document.getElementById('logged_out'); if(login_cookie == null) { logged_in.style.display = 'none'; logged_out.style.display = 'block'; } else { handle_user_partial(); logged_out.style.display = 'none'; logged_in.style.display = 'block'; } }
handle_cached_user の else 条件に追加されている追加の行に注意してください。この行は logged_in という DOM 要素を見えるようにする前に、適切な置き換えを行います。あとは、今回の記事と先月の記事で説明した、ページ全体をキャッシュするためのページ・キャッシング・ディレクティブを使うだけです。
まとめ
今回説明した高度な方法によって、多くの扉が開かれます。ChangingThePresent.org では、非常に単純な時間ベースのスイーパーを使ってページの75% 以上をキャッシュできると見積もっています。それよりもほんの少しだけ高度なスイープ手法を使えば、90% を軽く越える、あるいはそれ以上のページ・ヒットをキャッシュできるはずです。私達の積極的な画像キャッシング計画を考慮に入れれば、アプリケーション・サーバーにアクセスしに行くのは、すべての Web リクエストの 1% から 3% に過ぎないでしょう。
ただし、欠点も忘れないでください。このシステムは大幅に複雑になりました。以前よりもずっと複雑な HTML コードを維持する必要があり、HTML と JavaScript を必ず同期させる必要があります。しかし良い点として、より高いパフォーマンスが本当に必要な場合には、最も単純で最も効果的なキャッシング方法を使うことができます。ぜひ皆さんも試しに ChangingThePresent.org のサイトから、そのホームページをロードしてください。次に最上位レベルの各メニューをロードしてください。選択できる最上位レベルのメニューが 6 つある中で、私達が 4 つをページ・キャッシングしていることに気付くと思います。アカウントを作成し、それぞれをリロードしてください。どのページがキャッシュされているかわかるでしょうか。
添付ファイル