October 2011
Volume 26 Number 10
HTML5 - JavaScript でビジネス指向 Web アプリケーションを作成する
Frank Prößdorf | October 2011
マイクロソフトは HTML5 と JavaScript を Windows の開発者にとって重要なものとして普及させることに精力的に取り組んでおり、運用可能なアプリケーションの構築に利用できる高品質のライブラリとフレームワークを数多く用意しています。この記事では、基本的なビジネス指向アプリケーションの作成を通して、既存の機能についてより深く知るきっかけを提供し、JavaScript でどれほど楽しく効率的なコーディングが可能かを体感していただきます。
ここでは製品の一覧を作成し、カテゴリに分類するアプリケーションを作成します。製品とカテゴリはどちらも、作成、読み取り、更新、および削除 (CRUD) が可能です (図 1 参照)。一般的な CRUD 操作に加え、このアプリケーションでは国際化、入力検証、キーボード管理などの標準的作業にも対処します。このアプリケーションの最も重要な側面の 1 つは、オフラインでの編集を可能にするため HTML5 のローカル ストレージを使用することです。ここでは、そのすべてを詳しく説明することはしませんが、codeplex (英語) からこのサンプル アプリケーションの完全なコードを入手できます。
図 1 製品の概要一覧
はじめに
さて、JavaScript でビジネス指向アプリケーションを作成するには、何から始めればよいのでしょう。既に確立されている方法の 1 つが、モデル - ビュー - コントローラー (MVC: Model-View-Controller) 構造を使用する方法です。この方法は、Ruby on Rails、Django、ASP.NET MVC などのフレームワークで採用され、成功を収めています。MVC は、厳密な構造化が可能なうえ、ビューやビジネス ロジックといったアプリケーションの懸案事項をそれぞれ分離できます。JavaScript では、すべてを 1 か所で処理しようとして、コードを複雑にしてしまいがちなので、上記のような MVC の特長が特に重要になります。複雑にせず、明確でわかりやすく、再利用可能なコードを作成することを、いつも心がけてください。MVC 構造向けに構築されたフレームワークはいくつかありますが、特に注目に値するのは Backbone.js (英語)、Eyeballs.js (英語)、および Sammy.js (英語) です。
今回のサンプル アプリケーションでは Sammy.js を使用します。既に知っているからというのが主な理由ですが、小規模で、ていねいに作成されているうえ、テストも完了していて、アプリケーションの作成を始めるのに必要がものがすべて含まれているためでもあります。Sammy.js では自動的に MVC 構造が提供されるわけではありませんが、これを基盤として簡単に構築できます。現在 Sammy.js と依存関係があるのは jQuery (英語) だけで、いずれにしてもここで DOM の操作にライブラリとして使用します。次のようなディレクトリ構造から始めます。
- public
- js
app.js
+ controllers
+ models
+ helpers
+ views
+ templates
- vendor
- sammy
sammy.js
- jquery
jquery.js
JavaScript コードでレンダリングされるテンプレート ファイルはすべてテンプレート ディレクトリに、テンプレート ファイルのレンダリングに関連する JavaScript コードはすべてビュー ディレクトリに格納します。
アプリケーション ファイル
実際の Sammy.js アプリケーションは app.js に作成します。app.js でコントローラーを読み込み、ルートを初期化します (図 2 参照)。多くの場合、作成する変数 (コントローラー、モデルなど) をすべて名前空間に含めます。ここでは、この名前空間を karhu と呼ぶことにします。一覧を作成する製品の会社名です。
図 2 Karhu.app
karhu.app = $.sammy(function() {
this.element_selector = '#main';
this.use(Sammy.Mustache, 'mustache');
this.use(Sammy.NestedParams);
this.use(Sammy.JSON);
this.helpers(karhu.ApplicationHelper);
this.helpers({ store: karhu.config.store });
karhu.Products(this);
});
$(function() {
karhu.app.run('#/products');
});
最初に行うのはプラグインの読み込みです。プラグインには、テンプレート レンダリング エンジンの Mustache (英語) などがあります。次に、ヘルパー (karhu.ApplicationHelper) とコントローラー (karhu.Products) を初期化します。アプリケーションが定義され、DOM 要素がすべて読み込まれたら、アプリケーションを実行し、最初のルート (全製品のインデックス) にリダイレクトします。
テストの作成
製品のコントローラーがどのように動作し、全製品を表示するかを示す前に、テストによって JavaScript で作成するアプリケーションの品質がどれほど向上するかを簡単に説明します。このサンプル アプリケーションの開発に取りかかる際、主要な手順の前に、まずコードが実際に動作することを確認する受け入れテストを作成しました。これにより回帰の問題を避け、以前作成したコードがすべて正常に機能することも保証されます。コードがより複雑な場合は、単体テストを作成し、コードの実行時に起こり得るほとんどのケースを網羅するよう試みます。受け入れテストを作成する最も簡単でわかりやすい方法は、Selenium が組み込まれた Capybara (英語) を使用することです。Capybara のドライバーとして PhantomJS (英語) などのヘッドレス ブラウザーが利用可能なら、Selenium よりはるかに高速なので、代わりに使用するのも理にかなっているでしょう。
図 3 に示す最初のシナリオで、製品の一覧が表示されるかテストします。
図 3 テスト シナリオ
Feature: Products
In order to know which products I have
As a user
I want to see a list of products
Scenario: list products
Given a category "Trees" with the description "Plants"
And a product "Oak" with the description "Brown"
and the price "232.00€"
that is valid to "12/20/2027"
and belongs to the category "Trees"
And a product "Birch" with the description "White"
and the price "115.75€"
that is valid to "03/01/2019"
and belongs to the category "Trees"
When I go to the start page
Then I should see "Trees"
And I should see "Oak"
And I should see "Brown"
And I should see "232.00€"
And I should see "12/20/2027"
And I should see "Birch"
単体テストには、さまざまな選択肢があります。過去に使用したことのあった Ruby の RSpec と似ていたため、以前はよく JSpec を使用していました。現在では JSpec は廃止され Jasmine (英語) に置き換わっているので、ここでは Jasmine を使用します。Jasmine の機能は非常に優れており、受け入れテストと平行して簡単に実行できる Rake タスクが備わっています。サンプル アプリケーションの単体テストの一例を次に示します。
describe("Product", function() {
describe("attachCategory", function() {
it("should assign itself its category", function() {
var categories = [{id: 1, name: 'Papiere'}, {id: 2, name: 'Baeume'}];
var attributes = {id: 1, name: 'Fichte', category_id: 2};
var product = new karhu.Product(attributes, categories);
expect(product.category.name).toEqual('Baeume');
});
});
});
コントローラーの定義
テスト シナリオが完了したら、コントローラーの作成に着手します。次に示すように、これに取りかかるのは非常に簡単です。
karhu.Products = function(app) {
app.get('#/products', function(context) {
context.get('/categories', {}, function(categories) {
context.get('/products', {}, function(products) {
products = products.map(function(product) { return new karhu.Product(
product, categories); });
context.partial('templates/products/index.mustache', {products: products});
});
});
});
};
この時点では、定義済みのルートは #/products ルートを取得する GET だけです。URL のロケーション ハッシュが /products に変わると、コールバックが実行されます。そのため URL にルートを追加すると (たとえば、http://localhost:4567/index.html#/products)、アタッチされたコールバックが実行されます。app.js で最初のパスが同じルートを指定するよう定義したため、アプリケーションの開始時にも同様にコールバックが実行されます。
このルートでは、バックエンドに基本的な AJAX GET 要求のみを実行するヘルパーから、カテゴリと製品のデータを取得します。データを取得したら JavaScript オブジェクトにマップし、次にそのオブジェクトを index.mustache テンプレートにレンダリングします。index.mustache テンプレートは、app.js ファイルでルート element_selector に定義された <div id="main"> HTML タグにオブジェクトをレンダリングします。
モデルの定義
製品を該当するカテゴリに関連付け、製品の横にカテゴリ名をレンダリングするため、JavaScript オブジェクトにデータをマップする必要があります。次のようなコードになります。
karhu.Product = function(attributes, categories) {
_.extend(this, attributes);
attachCategory(this, categories);
function attachCategory(product, categories) {
product.category = _.find(categories, function(category) {
return parseInt(category.id, 10) === parseInt(product.category_id, 10);
});
}
};
オブジェクトを拡張して製品のすべての属性を含めてから、オブジェクトに製品のカテゴリをアタッチします。attachCategory をクロージャに格納し、プライベート関数にします。このコードでは、Underscore.js (英語) から提供されている Underscore 関数を使用していることに注意してください。Underscore.js ライブラリは Enumerable のヘルパーを定義し、わかりやすく簡潔なコードの作成に役立ちます。
図 4 は、ユーザーにモデルがどのように表示されるかを示しています。
図 4 Web アプリケーションの製品モデルへのアクセス
テンプレートのレンダリング
すぐ上で示したモデルでは、ビュー層にオブジェクトを追加する必要はありません。これはレンダリングのロジックが非常に基本的で、作成した製品のオブジェクトを反復処理し、事前にアタッチしたカテゴリ名を含め、それぞれのオブジェクトの属性を表示するだけだからです。レンダリングされる、ロジックを必要としない Mustache テンプレートは、図 5 のようになります。
図 5 Mustache テンプレート
<h2>Products</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Price</th>
<th>Valid To</th>
<th>Category</th>
</tr>
</thead>
<tbody>
{{#products}}
<tr>
<td>{{name}}</td>
<td>{{description}}</td>
<td>{{unit_price}}</td>
<td>{{valid_to}}</td>
<td>{{#category}}{{name}}{{/category}}</td>
</tr>
{{/products}}
</tbody>
</table>
図 6 に示すのは、レンダリングされた出力です。
図 6 Mustache テンプレートからレンダリングされた HTML 形式の出力
コントローラーのモデル固有のコードをモデルに移行
コントローラーにどれだけの役割を与え、モデルのコードにどれだけリファクタリングできるようにするかは好みの問題です。図 5 で示したコードをよりモデル中心の方法で作成すると、たとえば図 7 のようになります。
図 7 モデル中心のアプローチ コントローラー
karhu.Products = function(app) {
app.get('#/products', function(context) {
karhu.Product.all(function(products) {
context.partial('templates/products/index.mustache', {products: products});
});
});
};
モデル
karhu.Product.all = function(callback) {
karhu.backend.get('/categories', {}, function(categories) {
karhu.backend.get('/products', function(products) {
products = products.map(function(product) { return new karhu.Product(product, categories); });
callback(products);
});
});
};
標準的作業
ほとんどの Web 開発に共通する作業が数多く存在し、JavaScript でアプリケーションを作成する際にもそれに取り組む必要があります。そうした作業に取り組む方法と問題に直面したときの解決法を説明します。例によって、これにも 1 つの問題に対して複数のアプローチがあります。
認証: この記事の例を含むほとんどのアプリケーションは、ユーザーのログイン機能により基本的なセキュリティを追加します。HTTP はステートレスなので、要求のたびに認証を再送信する必要があります。ユーザーが初めてログインしたときにトークンを保存し、その後の要求のたびに使用することで、この問題に対処できます。ここでは、ユーザーが正常にログインしたときにトークンをローカル ストレージに保存し、そのトークンを XMLHttpRequest にアタッチされたヘッダーとして送信する方法を選択します。図 8 に、これを実行するコードを示します。このコードは、前述したヘルパーに使用するバックエンド モデルに格納します。
図 8 ユーザーのログイン時にトークンを保存
this.get = function(url, data, success, error) {
sendRequest('get', url, data, success, error);
};
function authenticate(xhr) {
var token = '';
if(karhu.token) {
token = karhu.token;
} else if(karhu.user && karhu.password) {
token = SHA256(karhu.user + karhu.password);
}
karhu.token = token;
xhr.setRequestHeader("X-Karhu-Authentication", 'user="' + karhu.user + '", token="' + karhu.token + '"');
};
function sendRequest(verb, url, data, success, error) {
$.ajax({
url: url,
data: data,
type: verb,
beforeSend: function(xhr) {
authenticate(xhr);
},
success: function(result) {
success(result);
}
});
}
図 9 は、保存されたユーザーのトークンを示しています。
図 9 HTTP 要求に含まれた X-Karhu-Authentication
ユーザーが初めてログインした場合は、ユーザー名とパスワードを入手します。ユーザーが以前にログインしている場合は、保存したトークンを使用します。どちらにせよ、トークンかユーザー名とパスワードの組み合わせをヘッダーとしてアタッチすると、要求が成功した場合、ユーザーが正常に認証されたことがわかります。要求が失敗した場合は、バックエンドは単にエラーを返します。この方法は比較的実装が容易で、唯一の問題はコードがいくぶん複雑でわかりにくくなることです。これを改善するには、ヘルパーを別のモデルにリファクタリングします。バックエンド モデルに要求をまとめる方法は非常に一般的で、たとえば Backbone.js ライブラリではこれがライブラリの主要部分になっています。認証コードはアプリケーションごとに異なる場合が多く、バックエンドおよびフロントエンドが何を送信すると想定しているかによって常に異なります。
国際化 (I18n): 国際化は Web アプリケーションでは一般的な作業で、これを実現するため、JavaScript でアプリケーションを作成するときには jquery.global.js (英語) をよく使用します。jquery.global.js は、数と日付の書式を設定するメソッドを提供し、現在のロケールに対応した辞書で文字列を翻訳できるようにします。辞書はキーと翻訳された値を格納する簡単な JavaScript オブジェクトで、その辞書の読み込みを完了すると、残るのは数と日付の書式設定だけです。この書式設定は、オブジェクトをテンプレートにレンダリングする前のモデルに作成するのが適切です。この製品モデルでは、たとえば次のようになります。
var valid_to = Date.parse(product.valid_to);
product.valid_to = $.global.format(valid_to, "d");
図 10 は、表示言語をドイツ語に設定した画面です。
図 10 表示言語をドイツ語に変更
検証: JavaScript で開発する利点の 1 つは、ユーザーにフィードバックをリアルタイムに提供できることです。この利点を活かして、バックエンドに送信する前にデータを検証するのは理にかなっています。バックエンドでも同様にデータを検証する必要があることに注意してください。これは、フロントエンドを使用しない要求もあり得るためです。検証には jQuery ライブラリの jquery.validate.js (英語) がよく使用されます。jquery.validate.js はフォームについての一連の規則を提供し、入力内容が規則に違反している場合、該当の入力フィールドにエラーを表示します。作成したモデルに入力規則を構造化し、すべてのモデルに入力規則を返す検証関数を含めるのは適切な方法です。ここでのカテゴリ モデルの検証は、次のようになります。
karhu.Category = function() {
this.validations = function() {
return {
rules: {
'category[name]': {
required: true,
maxlength: 100
}
}
};
};
};
図 11 は、エラーがどのように表示されるかを示しています。
図 11 カテゴリの新規作成時に発生した検証エラー
これ以外にも検証が行われます。データを送信せずにフォームから移動することはできません。ユーザーは実際に有効なデータを送信するか、移動する前に入力したデータを取り消す必要があります (図 12 参照)。
図 12 ユーザーがフォームのデータを送信していないために表示された赤色に点滅する警告メッセージ
オフライン編集のためのオブジェクトのキャッシュ: ユーザーが、オフラインで作業する必要が生じることがあります。これを可能にするのは、このアプリケーションの最も重要で複雑な部分です。アプリケーションがオフラインのときにオブジェクトが正確に分類、修正、およびフィルター処理されるには、すべてのオブジェクトが事前にキャッシュされている必要があります。オブジェクトがキャッシュされるとすぐにオブジェクトにアクションが適用されるように、オブジェクトのキャッシュの前に、実行するすべてのアクションのキューが必要です。オンラインに戻ったときにオフラインで行ったことがすべてバックエンドに適用されるように、実際にオフラインになるときに設定する 2 つ目のキューも必要です。図 13 は、オフライン状態のアプリケーションを示しています。
図 13 オフライン状態のアプリケーションの応答
キャッシュとキューのプロセスは既に複雑になっていますが、まだ取り組む必要のある課題がたくさんあります。たとえば、今のコードのままではオブジェクトに ID が与えられないため、オフラインで作成したオブジェクトの更新や削除ができません。ここでは単にオフラインで作成したオブジェクトのアクションを禁止することで、この問題を回避します。同じ理由で、オフラインで作成したカテゴリは製品の作成に使用できません。ここでは単に製品の作成に使用可能なカテゴリの一覧に、それらのカテゴリを表示しないようにします。この種の問題は、一時的に ID を使用したり、オフラインのキューを再構成したりすることで解決できるでしょう。
さらに、利用可能なパーシャルとテンプレートもキャッシュする必要があります。これには、対象のブラウザー グループでサポートされている場合は HTML5 で定義されているキャッシュ マニフェストを使用することもできますし、単にパーシャルを読み込み、ローカル ストレージに格納することもできます。Sammy.js を使用すると、次のような非常に簡単なコードで実現できます。
context.load('templates/products/index.mustache', {cache: true});
Windows への統合
HTML5 アプリケーションを実行するには Internet Explorer 9 が最適です。さらに Internet Explorer 9 では、Web アプリケーションが Windows 7 のタスク バーにネイティブに統合され、通知の表示、ナビゲーションの統合、およびジャンプ リストのサポート機能によりアプリケーションを強化できます。ジャンプ リストの統合は簡単で、最もシンプルな方法だとメタ タグ属性を宣言するだけです。Karhu でも、ユーザーが必要なものに容易にアクセスできるよう、まさにこのアプローチを採用します。ジャンプ リストのタスクから製品の追加、カテゴリの追加、製品の概要、およびカテゴリの概要のビューに移動するようにできます (図 14 参照)。次に示すようなコードで、簡単なジャンプ リストのタスクを宣言できます。
<meta name="msapplication-task"
content="name=Products;
action-uri=#/products;
icon-uri=images/karhu.ico" />
図 14 ジャンプ リストのタスク
"ピン留め" および Windows 7 の統合の詳細については、Build My Pinned Site を参照してください。JavaScript を使用して、ブラウザーの通知や動的なジャンプ リストなどを Web アプリケーションに追加する方法が説明されています。わずかな労力で、Web アプリケーションにより多くの機能を簡単に追加できます。また、MSDN JavaScript 言語リファレンス (英語) およびこの記事で言及したライブラリとフレームワークのドキュメントも、この点についてより深く理解するきっかけになるでしょう。
まとめ
これで、このサンプル アプリケーションに必要な基本的な機能はすべて実装しました。時間をかけさえすれば、言及した問題に対処することもできるでしょう。認証、国際化、ビジネス ロジックの処理などの作業は、フレームワークやライブラリとは独立してコーディングする必要があり、これらはまだ出発点にすぎません。
必ずテストを作成し、アプリケーションの構造に気を配れば、運用可能でさらに改良を重ねられるアプリケーションを JavaScript で作成することは、単に可能というだけにとどまらず、投資するだけの価値が十分にあります。取りかかるだけなら簡単ですが、重要なのは常にコード ベースをわかりやすく保ち、必要に応じてリファクタリングすることです。これさえ守れば、JavaScript を使用して洗練された管理しやすいアプリケーションを作成できます。
Frank Prößdorf はフリーランスの Web 開発者であると同時に NotJustHosting (ドイツ語) の共同設立者です。彼は Ruby と JavaScript を愛用しており、新たなテクノロジを発見し試すことに情熱を注いでいます。コードやアイデアに貢献し共有できる機会に魅力を感じているため、オープン ソース ソフトウェアを継続的にサポートしています。仕事だけでなく、旅行、セーリング、およびテニスも楽しんでいます。彼について知りたい方は、github (英語) のプロフィールや彼の Web サイト (英語) を参照してください。
Dariusz Parys は、マイクロソフトのドイツ法人に所属する開発者エバンジェリストで、最新のテクノロジを重視し Web 開発に取り組んでいます。最近は、JavaScript や HTML5 を使用して数多くの開発を行っています。downtocode.net (英語) から彼のブログをご覧いただけます。
この記事のレビューに協力してくれた技術スタッフの Aaron Quint に心より感謝いたします。