jQuery に注目したこのシリーズではこれまで、読者の皆さんが JavaScript ベースの Web アプリケーションを作成できるようになるよう手ほどきをしてきました。このシリーズを読む前は jQuery について聞いたことがなかった方でも、今では jQuery を使って優れた Web アプリケーションを作成するためのスキルと経験が備わっているはずです。しかし優れたアプリケーションでは十分ではなく、卓越した Web アプリケーションが必要になる場合もあります。そのためには、作成したアプリケーションが大規模なアプリケーションのなかでも小規模なアプリケーションのなかでも円滑に動作し、あらゆるユーザーに適切に表示されることを確実にするためのステップをアプリケーションに追加しなければなりません。これらの最終的な仕上げのステップによって、Web アプリケーションに磨きがかかります。
この記事では、コードのパフォーマンスの改善を中心に説明しますが、jQuery ライブラリーの内容についても取り上げます。これらの内容は、一部の人々は重要視していないかもしれませんが、複雑なアプリケーションでは重要になります。具体的には、誰のアプリケーションにとっても欠かせないプラグイン、そして Web アプリケーションのコードを作成しやすくする設計上のヒントについて説明します。そして最後のセクションでは、最近リリースされてライブラリーにいくつかの新しい機能を追加した jQuery 1.3の新機能についても紹介します。
この記事で紹介するほとんどのヒントは、記事に付属のサンプル・アプリケーション (「ダウンロード」を参照) に含まれています。このサンプル・アプリケーションは単純な E メール Web アプリケーションです。これはシリーズの最初の記事で紹介したアプリケーションと同じなので、読者の皆さんにとって見慣れたものかもしれませんが、最初の記事からどれだけ変わっているか、どれだけパフォーマンスが改善されているか、そして最後の仕上げによって、いかに卓越した Web アプリケーションに変身しているかを注意深く見てください。
図 1. サンプル・アプリケーション
このアプリケーションの Events モジュールには bind() と unbind() という関数が含まれています。この2 つの関数は一見すると、他のすべてのイベント・メソッドの機能をそっくり真似ているように思えます。結局のところ、click() メソッドをページ要素に単純にアタッチできるのであれば、ページ要素でわざわざ bind("click") を呼び出す必要があるのでしょうか。bind("click") を呼び出すのは、単にキーを無駄に叩いているだけにならないのでしょうか。しかし、この 2 つの関数は特定の状況で役に立ちます。そして適切に使用すれば、アプリケーションのパフォーマンスが大幅に改善されます。これらの関数では、もちろん、Events モジュールに含まれる多くのイベント・メソッドと同じく、イベントを特定のページ要素にアタッチすることができますが、それだけではなく、ページ要素にアタッチしたイベントをデタッチすることもできます。しかしなぜ、ページ要素からイベントをデタッチする機能が必要なのでしょう。サンプル Web アプリケーションを例に、この機能がどのように使われるのかを調べてみましょう。
図 2. bind/unbind の例
リスト 1 に、上記の機能を実装する前の、改善前のオリジナルのコードを示します。
リスト 1. 最適化前のウィジェット
$(document).ready(function(){
// cache this query since it's a search by CLASS
selectable = $(":checked.selectable");
// when the select/deselect all is clicked, do this function
$("#selectall").click(selectAll);
// whenever any individual checkbox is checked, change the text
// describing how many are checked
selectable.click(changeNumFilters);
// calculate how many are initially checked
changeNumFilters();
});
var selectable;
function changeNumFilters()
{
// this needs to be checked on every call
// since the length can change with every click
var size = $(":checked.selectable").length;
if (size > 0)
$("#selectedCount").html(size);
else
$("#selectedCount").html("0");
}
// handles the select/deselect of all checkboxes
function selectAll()
{
var checked = $("#selectall").attr("checked");
selectable.each(function(){
var subChecked = $(this).attr("checked");
if (subChecked != checked)
{
$(this).click();
}
});
changeNumFilters();
} |
このコードは、これまで何回かの記事で扱ってきたウィジェットとほとんど同じなので、比較的容易に理解できるはずです。「すべてを選択/すべての選択を解除 (Select All/Deselect All)」ウィジェットについては、シリーズ最初の記事で取り上げたときに、その基本となる形を記載しました。またパフォーマンスに関する記事では、セレクターによる検索結果をキャッシュに入れ、クラスによる検索をできるだけ使わないようにすることによって、このウィジェットのパフォーマンスを向上させる方法を説明しました。しかし、それでもまだ問題があります。100 行からなる表で「すべてを選択/すべての選択を解除」チェック・ボックスがクリックされた場合には、パフォーマンスがかなり低下するからです。実際、私が使用しているブラウザーでこのコードを使ってみると、「すべてを選択」を完了するまでの平均時間は 3.4 秒もかかりました。これでは応答性が良いとは言えません。この時点までに行ったすべての最適化をもってしてでも、このように、まだ許容できない問題が残っています。
ここでアルゴリズムの内容に一歩踏み込んで、何か間違っていることがないか調べてみましょう。このアルゴリズムでは、ページ上のすべてのチェック・ボックスをループして、現在の checked ステータスが「すべてを選択/すべての選択を解除」チェック・ボックスと同じであるかどうかを調べます。ステータスが同じでない場合、そのチェック・ボックスで click を呼び出し、「すべてを選択/すべての選択を解除」チェック・ボックスの状態と一致させます。しかし、ちょっと待ってください。これらのチェック・ボックスには関数もアタッチしてあるので、click を呼び出すたびに、changeNumFilters() 関数が呼び出されることになります。詳しく調べてみると、changeNumFilters() をおそらく 101 回は呼び出すアルゴリズムにしてしまったことがわかりました。どおりでパフォーマンスがこれほど悪かったわけです。クリックするごとに選択済みメッセージのカウントを更新する必要は当然ありません。プロセスの終わりに 1 回更新すればよいだけです。そこで、チェック・ボックスをクリックしても、このメソッドが呼び出されないようにする必要があります。それには、どうすればよいのでしょうか。
ここで活躍するのは、もちろん unbind() メソッドです。皆さんも予測していたと思いますが、チェック・ボックスがクリックされた状態にする前に unbind() を呼び出すことで、click() の呼び出しによって changeNumFilter() メソッドが呼び出されることがなくなります。これは理想的な方法で、望んでいた通りの動作です。これでメソッドが 101 回呼び出されることはなくなりますが、効果があるのは 1 回だけなので、チェック・ボックスで click の呼び出しが完了した後に、bind メソッドを使用してチェック・ボックスごとに click メソッドを再びアタッチする必要があります。リスト 2 の更新バージョンを見てください。
リスト 2. 最適化後のウィジェット
// handles the selection/unselection of all checkboxes
function selectAll()
{
var checked = $("#selectall").attr("checked");
selectable.unbind("click", changeNumFilters);
selectable.each(function(){
var subChecked = $(this).attr("checked");
if (subChecked != checked)
{
$(this).click();
}
});
selectable.bind("click", changeNumFilters);
changeNumFilters();
} |
上記の最適化を行った後は、約 900 ミリ秒で「すべてを選択」を実行できるようになります。これはかなりのパフォーマンスの改善です。この改善は、一歩下がって最適化前のアルゴリズムでは具体的に何が行われているのか、そしてそのアルゴリズムに加える措置がどのようにコード全体に作用するかを考えただけで実現されました。最適化後のウィジェットでは、changeNumFilter() 関数は 100 回呼び出されるのではなく、1 回しか呼び出されません。このシリーズの多くの記事でこの selectAll() 関数を検討してきた末に、ついに最大限の速度と効率性を達成しました。しかし実は、これで終わりではありません。今まで隠していましたが、この同じことを超高速で実行できるアルゴリズムがあります。今までこの方法に触れなかったのは、高速なアルゴリズムをすぐに明かしてしまったら、この関数をこのシリーズの教訓として使えなかったからです。この状況を作ったことによって、読者の皆さんがコードの中で bind/unbind 機能を使用するメリット (もちろん、他に近道がなかったらの話です) に目覚めてくれたことを願います。
教訓: bind/unbind は、デフォルトのイベントを起動させたくない場合、あるいはイベントをページ要素に一時的にアタッチ、またはデタッチする方法として使用すること。
リスト 3 に、「すべてを選択/すべての選択を解除」ウィジェットがコードに含まれる場合に、「すべてを選択」を超高速で実行するアルゴリズムに基づき作成したコードを示します。この場合、この関数は 40 ミリ秒で実行され、他に試したどの方法に比べても劇的に高速になります。
リスト 3. 超高速アルゴリズムを使用したウィジェット
function selectAll()
{
var checked = $("#selectall").attr("checked");
selectable.each(function(){
$(this).attr("checked", checked);
});
changeNumFilters();
} |
jQuery のバージョン 1.3 に追加された素晴らしい新機能としては、live() 関数と die() 関数が挙げられます。優れた設計の Web アプリケーションのなかでこの 2 つの関数が果たす役割を理解するには、例を引用すれば一目瞭然です。例えば表内のすべてのセルにダブルクリック・イベントをアタッチするとします。読者の皆さんは経験を積んだ jQuery のベテランとして、この実装を行う場所が document.ready() 関数であることはわかっているはずです (リスト 4 を参照)。
リスト 4. ダブルクリックの実装
$("tr.messageRow").dblclick(function() {
if ($(this).hasClass("mail_unread"))
{
$(this).removeClass("mail_unread");
}
}); |
この設計には 1 つだけ問題があります。上記のとおり、このコードは messageRow クラスを使って表内のすべての行にダブルクリック・イベントをアタッチします。しかし、新しい行を表に追加した場合にはどうなるでしょうか。表に新しく追加された行が表示されるのは、例えばページ全体をリロードせずに、Ajax を使用して追加メッセージのみがページにロードされた場合ですが、この場合は問題が生じます。なぜなら、上記のように作成されたコードだと機能しないためです。作成したイベントは、ページをロードする時点で存在していたすべての tr.messageRow 要素にバインドされますが、ページの存続期間中に新しく作成された tr.messageRows にはバインドされません。このようなコードを作成した人々は、自分のコードが思った通りに動作しないことを知ってがっかりするでしょう。初心者の jQuery プログラマーは、jQuery のドキュメントでたまたまこの事実に気付くまで、コードが思った通りの動作をしない理由を突き止めようと何時間もデバッグ作業に頭を悩ませるかもしれません (去年の私がそうでした)。
jQuery 1.3 がリリースされる以前は、この問題を回避する方法は 3 つありましたが、いずれもスマートな方法ではありませんでした (jQuery 1.2.x を引き続き使用している場合は、これらの方法はまだ有効です)。方法の 1 つは再初期化を行う手法で、新しい要素が追加されるたびに、選択されている要素に単にイベントをアタッチし直すというものです。2 つ目の方法では、前のセクションで説明した bind/unbind メソッドを利用します。リスト 5 に、この両方の方法を記載します。
リスト 5. 新規要素にイベントをアタッチするための次善策
// first technique to deal with "hot" page elements, added during the page's
// lifetime
$("#mailtable tr #"+message.id).addClass("messageRow")
.dblclick(function() {
if ($(this).hasClass("mail_unread"))
{
$(this).removeClass("mail_unread");
}
// second technique to deal with "hot" page elements
$("#mailtable tr #"+message.id).addClass("messageRow")
.bind("dblclick", (function() {
if ($(this).hasClass("mail_unread"))
{
$(this).removeClass("mail_unread");
} |
見てのとおり、いずれの方法にしてもそれほどスマートなものではなく、同じコードを何度も繰り返すことになります。さらに、新しいページ要素がページに追加されている可能性のあるあらゆる場所を見つけ出し、その時点で「新しい要素」の問題に対処しなければならなりません。それでは賢いプログラミング方法とは言えません。しかも、これは jQuery です。jQuery はあらゆる作業を驚くほど簡易化するはずです。そして本来なら、あらゆる作業を自動的に行ってくれるはずです。
幸い、これらの問題を解決すると思われるプラグインはありました。LiveQuery という名前のプラグインです。このプラグインでは特定のページ要素をイベントにバインドできる一方、バインドは「ライブで」(動的に) 行われます。そのため、ページの作成時に存在していたページ要素も、ページの存続期間中に (例えば Ajax 呼び出しによって) 作成されたページ要素も含め、すべてのページ要素がイベントをトリガーすることになります。静的ページを扱うのと同じくらい簡単に動的ページを扱えるようにしてくれるこの賢いプラグインは、UI 開発者には非常に重宝されていました。そして Web アプリケーション開発者にとっては、まさに必須プラグインの 1 つでした。
jQuery コア・チームはこのプラグインの重要性を支持し、jQuery の 1.3 リリースに組み込みました。現在、このライブ機能はコア jQuery の一部となっているため、あらゆる開発者がこの機能を利用することができます。このプラグインは、jQuery 1.3 のコアとなるコードにほとんどそのまま再現されていますが、最初の 1.3 リリースには漏れてしまったイベントがいくつかあります。賭けてもいいですが、これらの漏れてしまったイベントは今後の jQuery リリースに必ず登場するはずです。以下のリストで、このプラグインによってどのようにコードを変えられるかを見てください。
リスト 6. ライブ・イベント・モデル
$("tr.messageRow").live("dblclick", function() {
if ($(this).hasClass("mail_unread"))
{
$(this).removeClass("mail_unread");
} |
この簡単な変更をコードに加えると、ページ上にあるすべての tr.messageRow 要素が、ダブルクリックされたときにこのコードを呼び出すようになります。前に説明したように、dblclick() 関数を使用しただけでは、このような振る舞いにはなりません。こうした理由から、ほとんどのイベント・メソッドには live() メソッドの使用を検討することを是非ともお勧めします。実のところ、Ajax を使用するか、ユーザー操作によるかに関わらず、動的にページ要素を作成するページでは、別のイベント・メソッドではなく live() 関数を使用することが必須と言ってもよいくらいです。バグの可能性がある代わりに、これだけコードの作成が容易になるので、あれこれ考えて決定するまでのことはありません。
教訓: イベントを動的ページ要素にアタッチするときには常に、live() メソッドを使用すること。こうすることで、イベントがページ要素と同じく動的になります。
最近では、サーバーに対して Ajax 呼び出しを使用しているかどうかが、Web 2.0 企業が自らを評価する手段となってきています。このシリーズで延々と説明してきたように、jQuery では通常のメソッド呼び出しを行うのと同じくらい簡単に Ajax を使用することができます。つまり、サーバー・サイドの Ajax 関数は、クライアント・サイドの JavaScript 関数を呼び出すように簡単に呼び出せるということです。しかしこの利点には望ましいとは言えない副次作用があります。それは、サーバーに対する Ajax 呼び出しがあまりにも多い場合に表面化してきます。さらに、Web アプリケーションで過剰な Ajax 呼び出しが行われると、問題が発生することは間違いありません。
第 1 に問題となるのは、ブラウザーによってサーバーへのオープン接続の数が制限されていることです。Internet Explorer の現行バージョンでは、サーバーとの同時オープン接続は 2 つしか許可されません。Firefox の場合、許容接続数は 8 ですが、オープン接続の数はさらに制限されることになります。サーバー・サイドの呼び出しに時間がかかる場合はなおさらのこと、制御の利かない Ajax 呼び出しを Web アプリケーションが行っている場合には、2 つのオープン接続だけでは足りなくなるはずです。この問題は、Web アプリケーション設計者による不十分な設計によって起こることも、自分が実行するリクエストを制御できないユーザーが原因で起こることもあります。いずれの場合にしても望ましい状態ではなく、ブラウザーがどの接続を許可し、どの接続を許可しないかを決めるような状況は回避しなければなりません。
その上、呼び出しは非同期なので (Ajax の「A」は非同期を表します)、リクエストを送信した順にサーバーからレスポンスが返される保証はありません。どういう意味かと言うと、2 つの Ajax 呼び出しをほぼ同時に行ったとしても、サーバーからのレスポンスが同じ順に返されるとは限らないということです。したがって、2 番目の呼び出しが、その前の呼び出しが先に完了することに依存している場合、運の悪い結果になる可能性があります。例えば、最初の呼び出しによってデータを取得し、2 番目の呼び出しによってそのデータをクライアント・サイドで操作するというシナリオを考えてみてください。2 番目の呼び出しによるレスポンスが最初の Ajax 呼び出しのレスポンスよりも先に返って来た場合、コードはエラーを発生することになります。レスポンスが返ってくる早さを保証する手段はありません。2 つの接続だけでもこうした問題が起こることを考えると、これが 4 倍になった場合には、直ちに問題になることは明らかです。
jQuery の作成者はこれを潜在する問題として認識していたものの、Web アプリケーションで問題になるのは全体の 1% 程度に過ぎないとも認識していました。しかし、その 1% に当てはまるアプリケーションの開発者たちにとっては、ソリューションが必要です。そこで、jQuery の作成者はこの問題を解決するために使用できるプラグインとして、Ajax Queue、そして Ajax Sync を作成しました。機能の点では、どちらもよく似ています。Ajax Queue は Ajax 呼び出しを一度に 1 つずつ行い、1 つの Ajax 呼び出しに対するレスポンスが返って来てから次の呼び出しを行います。Ajax Sync は複数の Ajax 呼び出しを即時に送信しますが、呼び出し側関数にレスポンスが返ってくるのは、前の呼び出しへのレスポンスが返ってきてからになります。
このように Ajax 呼び出しをクライアント・サイドで制御するとともに、クライアント・サイドのコードにレスポンスを送信する方法を制御および調整することによって、過負荷の問題は解決されます。レスポンスがクライアントに返される順番が確実にわかれば、イベントの順番を予測したコードを作成することができます。リスト 7 の例で、このプラグインがどのように機能するか、そしてこのプラグインを使用する方法を見てみましょう。このコードは、一連の Ajax 呼び出しが行われ、それぞれの Ajax 呼び出しに不可欠な情報はその前の Ajax 呼び出しに依存するという、あの 1% のアプリケーションのために作成されていることに注意してください。リスト 7 の例は、そのようなアプリケーションの 1 つではありませんが、それでもこのコードから、このプラグインの使用方法が読み取れるはずです (このプラグインが必要となる実際の、しかも理解しやすいサンプルを作成するのは困難です)。
リスト 7. Ajax Queue
var newRow = "<tr id='?'>" +
"<td><input type=checkbox value='?'></td>" +
"<td>?</td>" +
"<td>?</td>" +
"<td>?</td>" +
"<td>?</td></tr>";
$("#mailtable").everyTime(30000, "checkForMail", function(){
// by using the Ajax Queue here, we can be sure that we will check for mail
// every 30 seconds, but ONLY if the previous mail check has already returned.
// This actually would be beneficial in a mail application, if one check for
// new mail takes longer than 30 seconds to respond, we wouldn't want the
// next Ajax call to kick off, because it might duplicate messages (depending
// on the server side code).
// So, by using the Ajax Queue plug-in, we can ensure that our Web client
// is only checking for new mail once, and will never overlap itself.
$.ajaxQueue({
url: "check_for_mail.jsp",
success: function(data)
{
var message = eval('(' + data + ')');
if (message.id != 0)
{
var row = newRow.replace("?", message.id);
row = row.replace("?", message.id);
row = row.replace("?", message.to);
row = row.replace("?", message.from);
row = row.replace("?", message.subject);
row = row.replace("?", message.sentTime);
$("#mailtable tbody").prepend(row);
$("#mailtable #"+message.id).addClass("mail_unread").addClass("messageRow");
$("#mailtable #"+message.id+ " td").addClass("mail");
$("#mailtable :checkbox").addClass("selectable");
}
}
}); |
教訓: アプリケーションで、互いの機能が重なる複数の Ajax 呼び出しが行われる場合には、Ajax Queue または Ajax Sync の使用を検討すること。
この記事の残りで取り上げる 3 つの問題に対処するために、ここからは別のウィジェットを使用します。コードの詳細を説明する前に、このウィジェットをお披露目して説明したいと思います。これは以前の記事にも記載したお馴染みの 401k ウィジェットです (記事のリンクについては「参考文献」を参照)。ただし、今回は微妙な違いがあります。それは、ウィジェットを同じページに 2 回組み込んで、2 つの異なる表にアタッチしていることです。これによって、いくつかの興味深い点が持ち上がってきます。図 3 にウィジェットの外観を示します。
図 3. 401k ウィジェット
このウィジェットでは、いくつかのことを行っています。まず 1 つ目は、テキスト・フィールドに入力された数値を合計し、その合計が 100 になるかどうかを判断することです。合計が 100 にならない場合は、ユーザーにエラーがある旨を通知し、ウィジェットを正しく使用していないことを示します。2 つ目は、各選択項目が入力された後に、それぞれの選択項目をソートすることです。このようにして、最大パーセントの出資配分が常に表の一番上に「移動」するようにしています。この結果は図 3 に現れているとおりで、選択項目はパーセント値でソートされています。そしてこのウィジェットでもう 1 つ行われていることは、このウィジェットの見栄えを良くするために 1 行おきに背景色を変えていることです。
このウィジェットを生成する HTML コードは驚くほど簡単なものです。リスト 8 にコード全体を記載します。
リスト 8. 401k ウィジェットの HTML
<p><table width=300 class="percentSort" cellpadding=0 cellspacing=0> <tbody> <tr><td>S&P 500 Index</td> <td><input type=text> %</td></tr> <tr><td>Russell 2000 Index</td> <td><input type=text> %</td></tr> <tr><td>MSCI International Index</td> <td><input type=text> %</td></tr> <tr><td>MSCI Emerging Market Index</td> <td><input type=text> %</td></tr> <tr><td>REIT Index</td> <td><input type=text> %</td></tr> </tbody> <tfoot> </tfoot> </table> |
上記の簡単な HTML はこのセクションでもそのまま使われ、jQuery コードの中にウィジェットと必要なすべてのコードを実装する際にも中心となるコードです。皆さんは、イベントをアタッチする処理や、特定の状況でクラスをアタッチするような単純な処理を実装するのは慣れているはずですが、今回はもう一歩踏み込んで、ウィジェットを実装するコードをすべて jQuery コードのなかに含めることにします。HTML の設計者と JavaScript のコード作成者にそれぞれの仕事をさせるために、役割の分離について毎度お馴染みの訳のわからない説明をすることもできますが、読者の皆さんは聞き飽きていることでしょう。そこで、ここでは多くのプラグイン作成者も使用している「クラス修飾」の概念についてだけ付け足しておきます。リスト 8 の HTML コードを見ると、単に percentSort クラスを表にアタッチすることによって、表の機能と外観をまるごと変換しています。このようにクラスを追加するだけでウィジェットを追加、削除できることは、まさに、皆さんが設計するあらゆるウィジェットの目標とするところです。
以下のリストで、jQuery でウィジェットを実装するために私が行ったステップをひと通り読んでください。これらのステップの説明を読むことで、リスト 9 に現れている設計のパターンが見えてきます。
リスト 9. jQuery ウィジェットのコード
$(document).ready(function() {
// the first step is to find all the tables on the page with
// a class of percentSort. These are all the tables we want to
// convert into our widget.
// After we find them, we need to loop through them and take some
// actions on them
// At the conclusion of this block of code, each table that's going to
// be a percentSort widget will have been transformed
$("table.percentSort").each(function(i){
// each table needs a unique ID, for namespace issues (discussed later)
// we can simply create a uniqueID from the loop counter
$(this).attr("id", "percentSort-"+i);
// within each table, let's highlight every other row in the table, to
// give it that "zebra" look
$(this).find("tbody > tr").filter(":odd").addClass("highlight");
// because each table needs to show the "Total" to the user, let's create a new
// section of the table in the footer. We'll add a row in the table footer
// to display the words "Total" and a span for the updated count.
$("#"+$(this).attr("id") + " tfoot")
.append("<tr><td>Total</td><td>
<span></span> %</td></tr>");
// finally, let's add the CLASS of "percentTotal" to the span we just
// created above. We'll use this information later to display
// the updated totals
$("#"+$(this).attr("id") + " tfoot span").addClass("percentTotal");
});
// now the second step, after we've completed setting up the tables themselves
// is to set up the individual table rows.
// We can similarly sort through each of them, taking the appropriate actions
// on each of them in turn.
// Upon completion of this block of code, each row in each table will be
// transformed for our widget
$("table.percentSort tbody > tr").each(function(i){
// get the namespace (to be discussed in the next section)
var NAMESPACE = $(this).parents("table.percentSort").attr("id");
// attach a unique ID to this row. We can use the loop counter
// to ensure the ID is unique on the page (which is a must on every page)
$(this).attr("id", "row"+i);
// now, within this row of the table, we need to find the text input, because
// we need to attach a class to them. We utilize the namespace, and also
// find the :text within the table row, and then attach the correct class
$("#"+$(this).attr("id") + " :text").addClass("percent");
// Finally, we attach a unique ID to each of the text inputs, and we do this by
// making it a combination of the table name and the row name.
$("#"+$(this).attr("id") + " .percent").
attr("id", NAMESPACE + "-" + $(this).attr("id"));
});
// Finally, because we know we only want numerical inputs, we restrict the text entry
// to just numbers. We must do this now, because up until this point, the page
// contained no elements with the CLASS "percent"
$(".percent").numeric(); |
上記の例からわかるように、jQuery コードに含まれる HTML コードには、存分に機能を組み込むことができます。このタイプの設計の利点は当然のことながら、役割の分離、コードの再利用などの方針に従っていることです。またウィジェット・プラグインにもこのタイプの設計が現れていて、一見したところ単純な HTML マークアップが、プラグインに意図されたあらゆるウィジェットに変換されます。本質的に、このリスト 9 で行っているのも同じことで、単純な表をソート機能と合計機能を備えた表に変換するためのプラグインを作成しています。
教訓: コードはできるだけ jQuery コードで実装し、HTML はできるだけ使用しないこと。
このタイプの設計、そして jQuery 自体に取り組む上で最も難解な点の 1 つは、コードが機能する名前空間を正しく理解することでしょう。このサンプル・アプリケーションが優れている理由は、ここにあります。このアプリケーションは、この問題を目に見える形で表しているからです。ページ上にあるすべての要素の ID とクラスがわかっていれば、jQuery コードの作成は極めて単純明快で容易に行えます。結局のところ、これが本来、jQuery に意図されていたことです。しかし、ページ上にあるクラスの数がわからなかったり、クラスが重複するようになったりした場合はどうなるでしょうか。サンプル・アプリケーションを見れば、この問題をすぐに理解できます。このサンプル・アプリケーションにはまったく同じ 2 つのウィジェットがあり、すべてが重複しています。以下の図のように、名前空間の問題を考慮していなければウィジェットが互いに干渉するようになり、最終的にはまったく正常に機能しなくなってしまいます。
図 4. 名前空間の問題を無視した場合
このような事態になった理由は、コードのなかで具体的にどのウィジェットを更新するべきかを無視して、単に $(".percentTotal") のような呼び出しを行っているからです。同じページに複数の表があれば、ページ上にある percentTotal クラスのインスタンスも複数にはるはずです。当然、ページに 1 つの表しかないのであれば、インスタンスは 1 つしかないという前提に立つことができます。しかしこのような前提は、ページが次第に拡張されてウィジェットが再利用されるようになったら捨て去らなければなりません。「代わりに ID を使うだけではいけないのか」という質問があがってくるかもしれませんが、それでは問題を解決することはできません。結局、どの ID を指定するかがわからないためです。「percentTotal」は、曖昧になるため使用することができず、「percentTotal-1」も、ページ上では何も意味しないため使用することができません (番号はユーザーの独断で作成されるためです)。例えば「percentTotal-percentSort1」のように、そのインスタンスが含まれる表の指定を追加するのも 1 つのソリューションですが、その場合、あまりにも複雑になりすぎます。このような複合的な命名体系を不要にするのが、jQuery の極めて高度ながらもとても簡単に使えるセレクターの構文です。ここで一から作り変えて、jQuery のセレクター・エンジンを使って名前空間の問題を解決します。
このウィジェットの問題の核心にあるのは、概して、どのウィジェットでアクションが起こったかを判断することです。テキスト・フィールドのいずれかに数値を入力するときに、数値が入力されたテキスト・フィールドを jQuery がどうやって認識するのかを考えてみてください。コードではもちろん、「percent」のクラスにイベントをアタッチしたので、コード内ではこのテキスト・フィールドを $(this) で参照することができます。そうなると、次に問題となるのは、jQuery がどのようにしてイベントが発生したウィジェットを認識するかです。該当するウィジェットを認識しなければ、該当する percentTotal フィールドも更新することはできません。しかし、jQuery はイベントが発生したウィジェットを認識しないと思います。少なくとも、簡単には認識できないでしょう。これこそが問題の核心です。ページ上の「percent」クラスを持つすべてのテキスト・フィールドにイベントをアタッチできるという点では、このコードは洗練されていますが、イベントが発生したウィジェットを単純に無視してしまうのであれば、洗練されたコードとは言えません。
この問題を名前空間の問題と呼ぶ理由は、ウィジェットに曖昧な名前を付けていると、それが原因で問題につながる可能性があるからです。jQuery コードが適切に機能するためには、それぞれの名前をその独自の空間、つまり名前空間に明確に定義する必要があります。このようにして、ウィジェットの名前が重複しないようなコードを作成し、各ウィジェットが単独で機能できるようにしなければなりません。同じページ上で同じウィジェットの複数のインスタンスを追加しても、その名前が 1 つも重複することがないようにしなければなりません。基本的に、それぞれのウィジェットはページ上で独立している必要があります。
この名前空間の問題に対処する正しい方法は 1 つに絞られるわけではないので、ここでは私のソリューションを紹介します。このソリューションは、皆さん独自のコードで使用することができます。あるいはこの問題を読んで、私のソリューションよりも優れたソリューションを自分で作成するのも一考です。私がこのコードを気に入っている理由を挙げると、まず簡単に使えること (たった 1 行です)、そして独自の名前空間をある程度制御できることです。リスト 10 を見てください。
リスト 10. 名前空間のソリューション
// our event is attached to EVERY input text field with a CLASS of "percent"
// this makes our code look good, but can lead to namespace issues
$("table.percentSort input.percent").keyup(function(){
// this simple line can establish a namespace for us, by getting the unique
// ID attached to our table (the table is considered the "container" for
// our widget. It could be anything, depending on your specific code.)
// We pass the CLASS of our widget to the parents() function, because
// that effectively encapsulates the widget
var NAMESPACE = "#" + $(this).parents("table.percentSort").attr("id");
// with the namespace established, we can use the jQuery selection
// syntax to use this namespace as our prefix for all of our remaining
// searches. Notice how the ambiguity is removed for our search
// by CLASS of "percent" and "percentTotal"
// This solves our namespace issues
var sum = $(NAMESPACE + " input.percent").sum();
var totalField = $(NAMESPACE + " .percentTotal");
|
このように、1 行のコードを追加するだけで、ウィジェットをカプセル化し、そのウィジェット (あるいは、ID またはクラスに不幸にも同じ名前を使っている他のウィジェット) の他のインスタンスと関数が (大抵は誤って) 重複するという事態を防ぐことができます。プラグインのコードでは、このタイプのコードの作成が極めて一般的です。よく練られたプラグインは名前空間の問題を考慮して作成されている一方、出来の悪いプラグインはこの問題を無視していることがわかります。この例を見るとわかるように、独自のコードで名前空間を使用するのは比較的簡単で、そうすることによって、ページがより複雑になった場合に大幅な時間の節約にもなります。このことから、これからは名前空間の問題を念頭に置いて jQuery コードに取り掛かること、そして独自のコードを作成する際には常に、このソリューションを使用することを推奨します。
教訓: 独自の jQuery コードの作成に取り掛かるときには、常に名前空間のソリューションを念頭に置いてから始めること。上記のソリューションを使用するのでも、独自のソリューションを作成するのでも構いません。それによって、複雑さが問題になることなくコードを拡張できるようになります。
この記事の最後のセクションでは、jQuery の 1.3 リリースに導入された新しい機能について紹介したいと思います。1.3 リリースはパフォーマンスの面では、大々的なリリースでした。この理由 1 つをとっても、皆さん独自のコードは 1.3 リリースに移行するべきです。その一方、コア・コードにも小さいながらもコードを改善する上で役立つ追加機能がいくつかあります。
1.3 リリースのコアに追加された機能として第 1 に挙げられるは、この記事で説明した live()/die() 関数です。セクションをまるごと 1 つ使って説明したこの 2 つの関数は、お察しの通り、最も重要な追加機能だと思います。1.3 リリースのコアで、もう 1 つ主要な追加機能となっているのは jQuery.Event クラスです。このクラスは、ページ上で発生したイベントを Object にカプセル化します。イベント中心のアプリケーションには、この機能が極めて大きなメリットをもたらします。それというのも、このクラスが提供する機能豊富なオブジェクトによって、型、ターゲット、XY 座標、そしてタイムスタンプに至るまで、イベントに関して必要なすべての情報を中継できるためです。これらの情報は 1.3 以前でも利用できましたが、十分なドキュメントがなかっただけでなく、カプセル化も明らかに不十分でした。そして最後に取り上げる 1.3 リリースへの追加機能は、開発者には明白かもしれませんが、それでも触れておきます。このリリースでは、ブラウザーの種類やバージョンを判別して処理を記述するための if 文が必要なくなりました。サポートしなければならないブラウザーのすべてに対処すると、スパゲッティー・コードになってしまうという事態は想像できるはずです。また、常時、新しく登場するブラウザーもあれば、使われなくなるブラウザーもあるなか、ブラウザーの検知はそれぞれの jQuery バージョンに確実な劣化をもたらしていました。けれども 1.3 バージョンではブラウザーの機能を検出する一方、ブラウザーの種類やバージョンにはとらわれません。つまり、新しいブラウザーが登場し、古いブラウザーが消えていっても、jQuery バージョンを更新する必要はないということです。jQuery のバージョンを毎年アップグレードしたり、アップグレードが必要かどうかをテストしたりしたくないサイトにとって、これは願ってもいないことです。
これで、優れた jQuery コードを卓越した jQuery コードに変える方法についてのヒントを紹介するこの記事は終わりです。jQuery は使いやすいため (そして改善されたスタンドアロン JavaScript であるため)、いとも簡単に優れた jQuery コードを作成することができます。大抵の開発者は数分のうちに優れたコードを完成できるでしょう。けれども優れたコードを作成することと、卓越したコードを作成することとは違います。卓越した jQuery コードでは、ページがますます複雑になっていった場合のパフォーマンスが考慮されています。卓越した jQuery コードは、ページの現状ではなく、今後ページが進化する方向を予測し、その予測に合わせたコードを考慮しているということです。このように可能な限り最も複雑なアプリケーションを対象とした卓越した jQuery コードでは、コードに投入される単純な内容に易々と対処することができます。
この記事では、優れた jQuery コードを卓越した jQuery コードにするために役立つ 5 つのヒントを紹介しました。最初に紹介したヒントは、bind()/unbind() メソッドの使用についてです。この 2 つのメソッドは、ページの存続期間をとおしてイベントをコードにアタッチしたくない場合、イベントをページ要素にアタッチしたり、ページ要素からデタッチしたりする上で大いに役立ちます。イベントがページ全体に影響する場合のパフォーマンスにとっては、これらのメソッドを使用することが重要となります。また、特定のユーザー・インターフェースでこれらのメソッドを使用することもできます。2 番目のヒントは、1.3 で新しく導入された関数、live()/die() の使用についてです。これらの関数によって、イベントをページ要素と同じように動的にすることができます。ページ要素が Web アプリケーションに追加されても、コードはこの 2 つの関数を使用することによってページに対応したコードへと拡張できるようになります。これは、以前のリリースでは不可能だったことです。イベント処理は、ページと同じく動的にしてください。3 番目に紹介したヒントは、Ajax Queue/Sync プラグインの使用についてです。このプラグインは、(クライアントの観点から) サーバーに対する Ajax 呼び出しが制御できないほど多くなる可能性がある場合、そして Ajax 呼び出しに対するレスポンスが返ってくる順番が重要な場合に、Ajax 呼び出しを調整および制御するために使用してください。4 番目のヒントとして推奨したのは、ページを実装するコードをできるだけ jQuery コード内に作成することです。そうすることによって、HTML を単純に作成できるようになり、ページを実装する際により柔軟に機能を追加できるようになります。そして最後のヒントでは、ページ上にある複数のウィジェットの機能が重複してエラーが発生することがないようにするため、コード内で名前空間のソリューション (私のソリューション、もしくは皆さんの自作のソリューションのいずれか) を利用する方法を説明しました。それぞれのページ要素とウィジェットを独立させ、ページの他の側面に干渉させないようにするには、このソリューションが効果を発揮します。
以上の 5 つのヒントを実践に移すのは、決して難しいことではありません。事実、このうちの 4 つはコードの 1 行を変更するだけの話です。ただし、これらのソリューションを自分のコードに適用する方法を理解することは重要です。あらゆるソリューションの例に漏れず、これらのソリューションを誤った方法で使用すると、コードの役に立たないだけでなく、コードに害を与える恐れもあります。私がお勧めするのは、ページの jQuery コードを作成し始めるのと同時に、この記事で紹介した 5 つのヒントを適用することです。あらゆる開発者が口を揃えて言うように、途中で機能が変更になることは開発者にとってごく当たり前のことになっています。しかし、当初の決定がまずかったせいで Web アプリケーション全体を設計し直すという状況や、上司の命令で何かを変更しなければならないためにアプリケーション全体を再設計しなければならないという状況には陥りたくないはずです。そのためには、卓越したアプリケーションを作成することを念頭に置いてコードの作成に取り掛かり、この記事での提案を生かして、卓越したアプリケーションを実現してください。
今回の記事が、私の jQuery に関するシリーズ第 2 段の締めくくりとなります。これまで 5 回の記事を通して、皆さんの jQuery のスキルを次のレベルまで伸ばしてきました。今では、このライブラリーを使用して、どんなタイプのアプリケーションでも作成できるようになっているはずです。私の最後の提案として、この記事のコードをいろいろといじって試してみてください。多くのことを学べるだけでなく、Web での新しい名案が生まれるかもしれません。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Zip file with Example | examples.zip | 97KB | HTTP |
学ぶために
- jQuery API ページをひと通り読んで、ライブラリーに用意されたすべての関数を確認してください。
- 1.3 のリリース・ノートを読んで、この最新リリースで行われているすべての変更および改善内容を確認してください。
- W3Schools では、CSS と JavaScript、そしてその他の Web 言語の詳細な背景を網羅しています。
- この著者の jQuery に関する連載の最初の 3 回の記事を読んでください。ライブラリーの導入方法を説明しています。
- 「jQuery を扱う: 第 1 回 ブラウザーでデスクトップ・アプリケーションを実現する」(developerWorks、2008年9月)
- 「jQuery を扱う: 第 2 回 一歩進んだ Web アプリケーションを今すぐ作成する」(developerWorks、2008年9月)
- 「jQuery を扱う: 第 3 回 jQuery と Ajax による RIA: 一歩進んだ Web アプリケーションを今すぐ作成する」(developerWorks、2008年10月)
製品や技術を入手するために
- この記事を書いている時点で最新の安定版、1.3.2 jQuery 最小版をダウンロードして、独自のコードに組み込んでください。
