こんにちは。まっつんです。これまでの記事 では、ポテトバーガー注文アプリケーションを題材にした RESTful な Web アプリケーションを Ruby on Rails (以下、Rails) で実装しました。
ポテトバーガー注文アプリケーションが RESTful であれば、サーバの実装を変更することなくクライアントの実装が行えるはずです。このことを確認するために今回は JavaScript によるクライアントを実装したいと思います。
JavaScript クライアントの設計
まず、実装するクライアントの仕様を考えます。1 から実装するのは大変なので、基本的にユーザインタフェースはこれまでに実装したものを流用し、最後の確認を JavaScript の confirm 関数で行うことにします。具体的な仕様は以下のとおりです。
- ユーザインタフェースはポテトバーガー注文アプリケーションの HTML を流用する
- 確認は JavaScript の confirm 関数を使用する
- 各メニューの表現として マイクロフォーマット を使用する
- JavaScript のライブラリとして jQuery を使用する
ファイル構成は以下のとおりです。
| ファイル | 説明 |
|---|---|
| public/order.html | ユーザインタフェースとなる HTML ファイル |
| public/javascripts/jquery/jquery-1.3.2.js | JavaScript のライブラリ |
| public/javascripts/jquery/jquery.order.js | ポテトバーガー注文アプリケーションと Ajax による通信を行う jQuery プラグイン |
| public/javascripts/jquery/jquery.hproduct.js | マイクロフォーマット hProduct を操作するための jQuery プラグイン |
マイクロフォーマット
これまでに実装したビューにはメニューの ID や名称に対して明確なマークアップがなされておらず、このままではクライアントからの操作は困難です。この問題を解決するために マイクロフォーマット を使うことができます。
マイクロフォーマット とは人が読んで理解できる情報にマークアップを付加する手段です。マイクロフォーマット を使用することにより、ドキュメントに埋め込まれたデータに対するソフトウェアからの操作がより簡単になります。
カレンダーや住所など扱う情報の種類毎に マイクロフォーマット の仕様が策定されています。今回はメインメニューとサイドメニューの商品を表現するために、製品情報用の マイクロフォーマット である hProduct を採用することにします。
それでは hProduct に従って、メインメニュー、サイドメニュー、確認ページのテンプレートを変更しましょう。
app/views/orders/main_menu.html.erb
<span id="content">
<h3>ご注文は何になさいますか?</h3>
<%= error_messages_for :order %>
<ul class="hlisting">
<li class="hproduct">
<div class="fn" style="display: none">ジャーマンポテトバーガー</div>
<div class="price" style="display: none">650</div>
<div class="identifier" style="display: none">
<span class="type">model</span>
<span class="value">1</span>
</div>
<%= link_to 'ジャーマンポテトバーガー (650円)', "#{orders_path}/order/side_menu?main=1", :class => 'url' %>
</li>
<li class="hproduct">
<div class="fn" style="display: none">ポテトコロッケバーガー</div>
<div class="price" style="display: none">600</div>
<div class="identifier" style="display: none">
<span class="type">model</span>
<span class="value">2</span>
</div>
<%= link_to 'ポテトコロッケバーガー (600円)', "#{orders_path}/order/side_menu?main=2", :class => 'url' %>
</li>
<li class="hproduct">
<div class="fn" style="display: none">肉じゃがバーガー</div>
<div class="price" style="display: none">700</div>
<div class="identifier" style="display: none">
<span class="type">model</span>
<span class="value">3</span>
</div>
<%= link_to '肉じゃがバーガー (700円)', "#{orders_path}/order/side_menu?main=3", :class => 'url' %>
</li>
</ul>
</span>
app/views/orders/side_menu.html.erb
<span id="content">
<h3>サイドメニューは何になさいますか?</h3>
<%= error_messages_for :order %>
<ul class="hlisting">
<li class="hproduct">
<div class="fn" style="display: none">フライドポテト</div>
<div class="identifier" style="display: none">
<span class="type">model</span>
<span class="value">1</span>
</div>
<%= link_to 'フライドポテト', "#{orders_path}/order/confirmation?main=#{@order.main}&side=1", :class => 'url' %>
</li>
<li class="hproduct">
<div class="fn" style="display: none">ポテトサラダ</div>
<div class="identifier" style="display: none">
<span class="type">model</span>
<span class="value">2</span>
</div>
<%= link_to 'ポテトサラダ', "#{orders_path}/order/confirmation?main=#{@order.main}&side=2", :class => 'url' %>
</li>
<li class="hproduct">
<div class="fn" style="display: none">スイートポテト</div>
<div class="identifier" style="display: none">
<span class="type">model</span>
<span class="value">3</span>
</div>
<%= link_to 'スイートポテト', "#{orders_path}/order/confirmation?main=#{@order.main}&side=3", :class => 'url' %>
</li>
</ul>
</span>
app/views/orders/confirmation.html.erb
<h3>注文内容は以上で宜しいですか?</h3>
<ul class="hlisting">
<li id="main" class="hproduct">
<div class="fn"><%=h @main %></div>
<div class="identifier" style="display: none">
<span class="type">model</span>
<span class="value"><%=h @order.main %></span>
</div>
</li>
<li id="side" class="hproduct">
<div class="fn"><%=h @side %></div>
<div class="identifier" style="display: none">
<span class="type">model</span>
<span class="value"><%=h @order.side %></span>
</div>
</li>
</ul>
<div>小計: <%=h @price %>円</div>
<% form_for(@order) do |f| %>
<%= f.hidden_field :main %>
<%= f.hidden_field :side %>
<%= link_to_function 'OK', "document.forms[0].submit()" %>
<%= link_to '選びなおす', "#{orders_path}/order/main_menu" %>
<% end %>
各 HTML 要素に追加された class 属性によって商品の名称や価格が定義されているのがおわかりいただけると思います。これらのテンプレートに加えられた変更はポテトバーガー注文アプリケーションのビューに何ら影響を与えていないことに注意してください。
クライアントの実装
続いて、クライアントを実装しましょう。今回は public/order.html に直接実装を行うことにします。
public/order.html
<!DOCTYPE html "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>ポテトバーガー注文アプリケーション</title>
<script type="text/javascript" src="javascripts/jquery/jquery-1.3.2.js"></script>
<script type="text/javascript" src="javascripts/jquery/jquery.order.js"></script>
<script type="text/javascript" src="javascripts/jquery/jquery.hproduct.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$.ajaxSetup({
error: function(xhr) {
if (xhr.status != '400') {
alert('予期しないエラーが発生しました');
}
show(xhr.responseText);
}
});
$.getMainMenu({
url: '/orders/order/main_menu',
success: show
});
});
function show(html) {
$('#content').remove();
$(html).appendTo('body');
$('.hlisting .hproduct .url').each(function() {
$(this).click(selectMenu);
});
$('a').each(function() {
$(this).click(function() {
if (this.href.match(/.+main_menu/)) {
location.reload();
return false;
}
});
});
}
function selectMenu() {
if (this.href.match(/.+side_menu.+/)) {
$.getSideMenu({
url: this.href,
success: show
});
} else if (this.href.match(/.+confirmation.+/)) {
$.isValid({
url: this.href,
success: confirmOrder
});
}
return false;
}
function confirmOrder(html) {
if (confirm("注文内容は以上で宜しいですか?\n" +
"メインメニュー : " + $(html).find('#main').getProductName() + "\n" +
"サイドメニュー : " + $(html).find('#side').getProductName()
)) {
$.order({
authenticityToken: $(html).getAuthenticityToken(),
main: $(html).find('#main').getProductId(),
side: $(html).find('#side').getProductId(),
success: show
});
}
}
</script>
</head>
<body>
</body>
</html>
Ajax による通信や hProduct で表現された情報にアクセスする部分は jQuery のプラグインとして実装しているため、order.html にはページで実行されるコードのみを配置しています。
最初に order.html がロードされると $(document).ready が実行されます。ここで Ajax 通信のエラー処理を設定し、メインメニューを取得しています。
show 関数はそれぞれのビューを正常に取得できた時に実行されます。取得した HTML を表示し、各商品のリンクがクリックされたときに実行されるイベントをフックします。今回はユーザインタフェースを流用しているので、このような処理を行っていますが、クライアントを 1 から実装する場合には独自のリンクやボタンを用意することになるでしょう。
selectMenu 関数はメインメニュー、サイドメニューが選択されたときに実行される関数です。リンクの URI から次に行う処理を判断しています。
confirmOrder 関数は confirm 関数による最終確認と注文のリクエストを送信しています。
次に jQuery を使ったサーバとの通信部分を見てみましょう。この部分は jQuery のプラグインとして実装しています。
public/javascripts/jquery/jquery.order.js
getHTML = function(config) {
config = $.extend({
url: null,
success: null
}, config);
$.ajax({
url: config.url,
type: 'GET',
dataType: 'html',
success: config.success
});
}
jQuery.extend({
getMainMenu: getHTML,
getSideMenu: getHTML,
isValid: getHTML,
order: function(config) {
config = $.extend({
authenticityToken: null,
main: null,
side: null,
success: null
}, config);
$.ajax({
url: '/orders',
type: 'POST',
data: {
'authenticity_token' : config.authenticityToken,
'order[main]': config.main,
'order[side]': config.side
},
dataType: 'html',
success: config.success
});
}
});
jQuery.fn.extend({
getAuthenticityToken: function() {
var authenticityToken;
$(this).find('input').each(function() {
if (this.name == 'authenticity_token') {
authenticityToken = this.value;
return false;
}
});
return authenticityToken;
}
});
getMainMenu, getSideMenu, isValid メソッドはすべて getHTML 関数を呼び出しています。order.html から getHTML を直接呼び出しても動作しますが、「何を取得しているのか」という意図を明確にするためにあえてメソッドを用意しました。
最後に hProduct で表現された情報へアクセスする部分を見てみましょう。この部分も jQuery のプラグインとして実装しています。これらのメソッドは自身を起点に製品の ID または名称をそれぞれ取得します。
public/javascripts/jquery/jquery.hproduct.js
jQuery.fn.extend({
getProductId: function() {
return $(this).find('.identifier').find('.value').text();
},
getProductName: function() {
return $(this).find('.fn').text();
}
});
以上で実装は完了です。http://localhost:3000/order.html にアクセスして、正常に動作することを確認しましょう。また、http://localhost:3000/orders/order/main_menu にアクセスして、これまでの実装も変わりなく動作することも確認しましょう。
Rails の CSRF 対策
CSRF (Cross Site Request Forgeries) は、正規のアカウントを保持するユーザに、仕掛けを埋め込んだ Web ページを閲覧させ、掲示板の書き込みやショッピングサイトでの購入といったユーザが意図しない処理を実行させる攻撃手法です。CSRF に対して、Rails はどのような対策を行っているのでしょうか?
Rails のドキュメント「Ruby On Rails Security Guide」の「3.1 CSRF Countermeasures」の最初の一文には W3C が提案する CSRF 対策が書かれています。
-- First, as is required by the W3C, use GET and POST appropriately. Secondly, a security token in non-GET requests will protect your application from CSRF.
これによると、HTTP メソッドの適切に利用し、GET (情報の取得) 以外の処理ではセキュリティトークン (ワンタイムトークン) を利用することが推奨されています。
HTTP メソッドの適切な利用については REST アーキテクチャスタイルに従って実装することで対応できるため、ここではセキュリティトークン (ワンタイムトークン) について考えてみたいと思います。
Rails は、自動的にセキュリティトークン (ワンタイムトークン) の発行・処理を行います。以下は確認ページのテンプレートのフォームと実際にブラウザで表示されたフォームです。
app/views/orders/confirmation.html.erb
...
<% form_for(@order) do |f| %>
<%= f.hidden_field :main %>
<%= f.hidden_field :side %>
<%= link_to_function 'OK', "document.forms[0].submit()" %>
<%= link_to '選びなおす', "#{orders_path}/order/main_menu" %>
<% end %>
ブラウザで表示されたフォーム
...
<form method="post" id="new_order" class="new_order" action="/orders">
<div style="margin: 0pt; padding: 0pt;">
<input type="hidden" value="ppxvv0wTBg0UZXerjs0nm30yeLmMuhObFhbHZImiGMI=" name="authenticity_token"/>
</div>
<input type="hidden" value="2" name="order[main]" id="order_main"/>
<input type="hidden" value="3" name="order[side]" id="order_side"/>
<a onclick="document.forms[0].submit(); return false;" href="#">OK</a>
<a href="/orders/order/main_menu">選びなおす</a>
</form>
...
authenticity_token フィールドがランダムな値で自動的に生成されていることがわかります。他のフィールドといっしょにこの値が POST されなければ、Rails は以下のエラーを出力します。
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
REST の観点から見た場合、このような対策は問題ないでしょうか?
セキュリティトークン (ワンタイムトークン) は複数のリクエストにまたがって管理されなければならないためステートレスな振る舞いとはいえません。Rails はこの点について、セッションをクッキーに保存することで折り合いを付けているように思えます。リクエストごとにクッキーに保存されたセッションが送信されるため、各リクエストは独立している、つまりステートレスであるというわけです。
セッションの保存先を ActiveRecord にした場合はこの理屈は通らなくなりますが、Rails のソリューションは CSRF 対策と REST との間で適切なものだと思います。
Rails を使ってみて
今回はポテトバーガー注文アプリケーションのクライアントを JavaScript で実装しました。ユーザインタフェースを流用したため、テンプレートは変更しましたが、コントローラやモデルは一切変更する必要がありませんでした。従って、ポテトバーガー注文アプリケーションは RESTful な Web アプリケーションとして実装できたのではないかと思います。
最後に Rails に対する筆者の感想をまとめます。
わかりやすい規約
Convention Over Configuration (CoC) による規約は違和感のない非常にわかりやすいものでした。始めて使うフレームワークなので、どういったメソッドを使えば良いかという点ではいろいろ悩みましたが、意図が断絶していてわかりにくい部分はありませんでした。
データベースマイグレーションは便利
アプリケーションの開発・運用においてデータベースの変更は避けては通れません。データベースに対して行った履歴を保存することができ、変更をロールバックできるこの機能はプロジェクトに従事する者に心強い味方になるでしょう。
Scaffolding は便利
初学者にとって Scaffolding は非常に便利な機能でした。Scaffolding が作成するコードは実際に動作するリファレンスとしても価値があると思います。
「設計の可視化」という観点がない
「設計の可視化」は主に IDE がカバーする領域ですが、フレームワーク側にこれを意識した実装があることが前提となります。Rails の IDE としては RadRails が有名ですが、残念ながら Rails にも RadRails にも「設計の可視化」には重点がおかれていないようです。
今回で Rails のチャレンジは終了です。Rails を中心に REST や マイクロフォーマット などさまざまな技術を見ることができ、今までのチャレンジの中で一番濃い内容になったのではないかと思います。
使用したソフトウェアのバージョン
| Ruby on Rails | 2.3.2 |
|---|---|
| jQuery | 1.3.2 |
参考文献
トラックバック(0)
- このブログ記事のトラックバックURL:
