レベル: 中級 Jack Herrington (jherr@pobox.com), Editor-in-Chief, Code Generation Network
2007年 3月 20日 間違ったやり方を理解することで、逆に正しいやり方の多くを学ぶことになるものです。Ajax (Asynchronous JavaScript™+ XML) アプリケーションにも当然、誤った作成方法と正しい作成方法があります。そこで今回の記事では、避けなければならない一般的なコーディングの慣習について説明します。
みんながみんな、すべてのことを最初から上手くできたとしたら、世界はまったく違ったものになっていたでしょう。それは Ajax でも同じことです。私は、コーディングや、執筆、会話、そして自分自身を含めたAjax 開発者の支援をたくさん行ってきた中で、Ajax での正しいやり方と間違ったやり方の多くを学びました。前回の記事「Ajax に共通の 5 つのデザイン・パターン: 今日から使える便利な Ajax デザイン・パターン」では、Ajax アプリケーションを正しく作成するための 5 つのパターンを紹介しましたが、今回は Ajax コードでよく見かける 5 つのアンチパターンを紹介します。
アンチパターンとは何なのかというと、誰もが用心しなければならないほどよく見られるアプリケーション設計の不具合です。ここで説明するのは、レベルの高い誤りについてであり、構文エラーやリンカーの問題は別とします。
たいていの開発者が耳にしたことがあるアンチパターンの代表例は、構造化照会言語 (SQL) ライブラリーを誤って使用したために Web サイトがSQL インジェクションによる攻撃を受けたというものです。このアンチパターンによって今まで数々の会社が収益を損ない、カスタマーの記録が公開されてしまうという結果になりました。不幸なことに、これはどのプログラミング言語でも起こり得るアンチパターンです。そのため、このアンチパターンに至る経緯と理由、そして回避方法を理解することは意義があります。
Ajax のアンチパターンの場合でも同じことが言えます。これらのアンチパターンによって会社が膨大な収益の損失を被るというわけではありませんが、サーバーをクラッシュさせたり、カスタマーのエクスペリエンスを損ねる原因にはなり得ます。いずれの場合にしても苛立たしくて費用がかかるものです。
何が間違っているかを理解できれば、そこから学べることはたくさんあります。Ajax はページがロードされた後にサーバーから XML をフェッチするだけの手段だと思われがちですが、それは極めて枠にはまった考えです。誤って適用されたAjax は、アプリケーションのパフォーマンス問題の原因になる可能性さえあります。そこでこの記事では、間違っている理由とそれを修正する方法を併せて説明することにします。
タイマーによる不必要なポーリングは行わないこと
よく目にする Ajax 問題の多くは、JavaScript 言語に組み込まれたタイマーの機能を誤って使用していることに関係しています。ここで鍵となるメソッドは、window.setInterval() です。このメソッドを見かけたら常にちょっとした警戒心を持って、タイマーが使用されている理由を考えなければなりません。もちろんタイマーにはそれなりの目的があります。動画はその一例です。
window.setInterval() メソッドは、ページに特定の間隔、例えば 1 秒ごとに特定の関数をコールバックするように指示するメソッドです。ほとんどのブラウザーはこれらのタイマーとうまく連動するという触れ込みですが、その通りになることはめったにありません。第一の理由は、JavaScript言語は単一のスレッドになっているからです。1 秒を要求すると、1 秒または 1.2 秒、あるいは 9 秒などの間隔でコールバックが行われることがあります。
タイマーが確実に不要なのは、例えば Ajax 要求の完了を監視する目的で使用されている場合です。リスト 1 の例を見てください。
リスト 1. Antipat1a_polling.html
<html><script>
var req = null;
function loadUrl( url ) {
if(window.XMLHttpRequest) {
try { req = new XMLHttpRequest();
} catch(e) { req = false; }
} else if(window.ActiveXObject) {
try { req = new ActiveXObject('Msxml2.XMLHTTP');
} catch(e) {
try { req = new ActiveXObject('Microsoft.XMLHTTP');
} catch(e) { req = false; }
} }
if(req) {
req.open('GET', url, true);
req.send('');
}
}
window.setInterval( function watchReq() {
if ( req != null && req.readyState == 4 && req.status == 200 ) {
var dobj = document.getElementById( 'htmlDiv' );
dobj.innerHTML = req.responseText;
req = null;
}
}, 1000 );
var url = window.location.toString();
url = url.replace( /antipat1a_polling.html/, 'antipat1_content.html' );
loadUrl( url );
</script><body>
Dynamic content is shown between here:<br/>
<div id="htmlDiv" style="border:1px solid black;padding:10px;">
</div>And here.</body></html>
|
すべて順調に見えるのは、setInterval を呼び出すところまでです。この呼び出しは、要求のステータスを監視するタイマーを設定してから、ダウンロードした材料でページのコンテンツを設定します。
まもなく要求が完了するということを知るためにポーリングしなければならないという問題に対処する、より良いソリューションをこのあと紹介しますが、その前にページが要求しているファイルをリスト 2 に記載します。
リスト 2. Antipat1_content.html
図 1 は、ブラウザーに表示されるページです。
図 1. HTML 文書に配置されたコンテンツ
おそらくこう思っていることでしょう。「うまく機能しているじゃないか。問題もないのに、どうして修正する必要があるのだろう」。でも実は問題があるのです。なぜなら表示されるまでに非常に時間がかかるからです。タイマーを1 秒間隔に設定すると、タイマーが満了する頃には要求がとっくに完了しています。そのため、最初のページが空のボックスで表示された後、1 秒経ってからやっとコンテンツが表示されることになります。
それでは、これに対するソリューションは何でしょう。Ajax はその性質上、非同期です。つまり、ポーリング・ループで要求が完了したかどうかを調べる必要はないということです。
このソリューションは大げさなものではないことがわかると思います。リスト 3 に示すように XMLHTTPRequest オブジェクトが指定しているのは唯一、onreadystatechange というコールバック・メカニズムだけです (VAX、PDP-11 を連想させる素敵な名前ではありませんか)。
リスト 3. Antipat1a_fixed.html
<html><script>
var req = null;
function processReqChange() {
if (req.readyState == 4 && req.status == 200 ) {
var dobj = document.getElementById( 'htmlDiv' );
dobj.innerHTML = req.responseText;
}
}
function loadUrl( url ) {
...
if(req) {
req.onreadystatechange = processReqChange;
req.open('GET', url, true);
req.send('');
}
}
var url = window.location.toString();
url = url.replace( /antipat1a_fixed.html/, 'antipat1_content.html' );
loadUrl( url );
</script>
...
|
上記の新しいコードは、要求オブジェクトがこの onreadystatechange コールバックに応答して変更されたかどうかを確認するだけで、それが完了したらページを更新します。
この結果、電光石火のごとく素早くページがロードされます。ページが表示されるとほとんど同時に、ボックスに新しいコンテンツが表示されます。それはなぜかというと、要求の完了と同時にコードが呼び出されてページに情報が入力されるためです。役立たずのタイマーを扱う必要はまるでありません。
ポーリングに関するアンチパターンのもう 1 つの形として、要求は変更されていないのに、ページがその要求を何度もサーバーに送ってしまうというものがあります。リスト 4 の検索ページを見てください。
リスト 4. Antipat1b_polling.html
<html><script>
var req = null;
function processReqChange() {
if (req.readyState == 4 && req.status == 200 ) {
var dobj = document.getElementById( 'htmlDiv' );
dobj.innerHTML = req.responseText;
}
}
function loadUrl( url ) {
...
}
window.setInterval( function watchSearch() {
var url = window.location.toString();
var searchUrl = 'antipat1_content.html?s='+searchText.value;
url = url.replace( /antipat1b_polling.html/, searchUrl );
loadUrl( url );
}, 1000 );
</script><body><form>
Search <input id="searchText" type="text">:<br/>
<div id="htmlDiv" style="border:1px solid black;padding:10px;">
</div></form></body></html>
|
ブラウザーでの実際のページは、 図 2 のように表示されます。
図 2. 動的応答エリアを持つ検索エリア
なかなかの見映えです。外観を見る限り、このページは十分道理にかなっていて、検索テキストを変更すると結果エリアが新しい検索基準に基づいて変更されます(実際はそのとおりにはなりませんが、要求に本物の検索エンジンを使ったとしたらの場合です)。
問題は、JavaScript コードが window.setInterval を使用しているため、検索フィールドの内容が変更されていないとしても要求が何度も繰り返されるという点です。要求の繰り返しにより、ネットワーク帯域幅もサーバーの時間も消費されます。この2 つがこのように消費されるとなると、人気の高いサイトでは致命的です。
これに対するソリューションは、リスト 5 に示すように検索ボックスでイベント・コールバックを使用することです。
リスト 5. Antipat1b_fixed.html
<html><script>
var req = null;
function processReqChange() { ... }
function loadUrl( url ) { ... }
var seachTimer = null;
function runSearch()
{
if ( seachTimer != null )
window.clearTimeout( seachTimer );
seachTimer = window.setTimeout( function watchSearch() {
var url = window.location.toString();
var searchUrl = 'antipat1_content.html?s='+searchText.value;
url = url.replace( /antipat1b_fixed.html/, searchUrl );
loadUrl( url );
seachTimer = null;
}, 1000 );
}
</script><body><form>
Search <input id="searchText" type="text" onkeyup="runSearch()">:<br/>
<div id="htmlDiv" style="border:1px solid black;padding:10px;">
</div></form></body></html>
|
上記では、runSearch() 関数を検索ボックスの onkeyup()メソッドにフックしています。このようにして、ユーザーが検索ボックスに何かを入力した時点でコールバックが行われるようにしています。
runSearch() はかなり気の効いた役目を果します。この関数は、1 秒に 1 回タイムアウトが発生し、このタイムアウトによりサーバーが呼び出されて実際に検索が実行されます。タイムアウトを設定する時点で前のタイムアウトがまだ満了していなければ、前のタイムアウトをクリアします。このようにタイムアウトをクリアすることで、ユーザーは長いテキストを入力できるからです。そしてユーザーが最後のキーを押した1 秒後に、検索が実行されます。この方法では、ユーザーが始終ちらつく画面に煩わされることがなくなります。
コールバックの呼び出し結果は検査すること
多くの Ajax アンチパターンは、XMLHTTPRequest オブジェクトのメカニズムを誤解していることに起因します。よく目にする誤解は、ユーザーがコールバックでオブジェクトの readyStateまたは status フィールドを検査しないというものです。リスト 6 を見れば、私が何を言っているのかがわかるはずです。
リスト 6. Antipat2_nocheck.html
<html><script>
var req = null;
function processReqChange() {
var dobj = document.getElementById( 'htmlDiv' );
dobj.innerHTML = req.responseText;
}
...
|
上記には何も問題がないように見えます。実際、ちょっとした要求や一部のブラウザーではおそらく問題なく機能するでしょう。ただし要求の多くはちょっとしたものではなく、要求が完了するまでにはonreadystatechange ハンドラーを何度も呼び出します。そのような要求となると、上記のコードではコールバックで不完全なデータを操作してしまう可能性があります。
正しい方法は、リスト 7 のようになります。
リスト 7. Antipat2_fixed.html
<html><script>
var req = null;
function processReqChange() {
if (req.readyState == 4 && req.status == 200 ) {
var dobj = document.getElementById( 'htmlDiv' );
dobj.innerHTML = req.responseText;
}
}
...
|
コードはそれほど増えていませんが、すべてのブラウザーで機能します。
私が気付いた点として、Windows® Internet Explorer® 7 ではこの問題は他のブラウザーに比べて深刻になります。InternetExplorer 7 は onreadystatechangeを何度も呼び出すためです。その回数は、小さな要求でさえも無視できないほどなので、ハンドラーを正しく作成することが重要になります。
HTML のほうが適切な場合は複雑な XML を渡さないこと
私が協力したある会社では、話題は「ネットワーク・エッジにインテリジェンスを」持たせることに尽きていました。ありきたりなフレーズですが、デスクトップで処理するときは、サーバーだけに頼らず、ブラウザーの機能を使用してください。
ただし、ページにさまざまなインテリジェンスを持たせるということは、すなわち多くの JavaScrip コードを組み込むということです。これにはブラウザーの互換性という大きな弱点があります。JavaScriptコードの特異な行はすべて、よく使われているすべてのブラウザーでテストしなければなりません。あるいは少なくともカスタマーが使用する可能性の高いブラウザーでテストすることが必須となりますが、そうなるとかなりの作業となります。リスト 8 に、複雑な Ajax コードの例を記載します。
リスト 8. Antipat3_complex.html
<html><head><script>
var req = null;
function processReqChange() {
if (req.readyState == 4 && req.status == 200 && req.responseXML ) {
var dtable = document.getElementById( 'dataBody' );
var nl = req.responseXML.getElementsByTagName( 'movie' );
for( var i = 0; i < nl.length; i++ ) {
var nli = nl.item( i );
var elYear = nli.getElementsByTagName( 'year' );
var year = elYear.item(0).firstChild.nodeValue;
var elTitle = nli.getElementsByTagName( 'title' );
var title = elTitle.item(0).firstChild.nodeValue;
var elTr = dtable.insertRow( -1 );
var elYearTd = elTr.insertCell( -1 );
elYearTd.innerHTML = year;
var elTitleTd = elTr.insertCell( -1 );
elTitleTd.innerHTML = title;
} } }
function loadXMLDoc( url ) {
if(window.XMLHttpRequest) {
try { req = new XMLHttpRequest();
} catch(e) { req = false; }
} else if(window.ActiveXObject) {
try { req = new ActiveXObject('Msxml2.XMLHTTP');
} catch(e) {
try { req = new ActiveXObject('Microsoft.XMLHTTP');
} catch(e) { req = false; }
} }
if(req) {
req.onreadystatechange = processReqChange;
req.open('GET', url, true);
req.send('');
}
}
var url = window.location.toString();
url = url.replace( /antipat3_complex.html/, 'antipat3_data.xml' );
loadXMLDoc( url );
</script></head><body>
<table cellspacing="0" cellpadding="3" width="100%"><tbody id="dataBody">
<tr>
<th width="20%">Year</th>
<th width="80%">Title</th>
</tr>
</tbody></table></body></html>
|
 | |
このコードはリスト 9 の XML ファイルからデータを読み取り、そのデータを表の形式にします。
リスト 9. Antipat3_data.xml
<movies>
<movie>
<year>1993</year>
<title>Jurassic Park</title>
</movie>
<movie>
<year>1997</year>
<title>The Lost World: Jurassic Park</title>
</movie>
<movie>
<year>2001</year>
<title>Jurassic Park III</title>
</movie>
</movies>
|
結果は図 3のようになります。
図 3. 複雑な映画の一覧ページ
このコードはしっかり機能を果しています。ただ、比較的単純なタスクを実行するには大げさすぎるコードです。結果のページはまったく複雑なものではなく、クライアント・サイドではソートも検索もできません。実は、XMLと HTML との間のこの複雑な変換を行う理由はほとんどありません。
リスト 10 のようにサーバーに XML ではなく HTML を返させるとしたら、もっと簡潔にできるかもしれません。
リスト 10. Antipat3_fixed.html
<html><script>
var req = null;
function processReqChange() {
if (req.readyState == 4 && req.status == 200 ) {
var dobj = document.getElementById( 'tableDiv' );
dobj.innerHTML = req.responseText;
}
}
function loadUrl( url ) { ... }
var url = window.location.toString();
url = url.replace( /antipat3_fixed.html/, 'antipat3_content.html' );
loadUrl( url );
</script><body><div id="tableDiv"></div></body></html>
|
上記は実に簡潔になっています。複雑な表の行とセルを作成するコードが、ページ上の <div>タグに含まれる単一の innerHTML セットに置き換えられているためです。
リスト 11 は、サーバーから返される HTML です。
リスト 11. Antipat3_content.html
<table cellspacing="0" cellpadding="3" width="100%">
<tbody id="dataBody">
<tr>
<th width="20%">Year</th>
<th width="80%">Title</th>
</tr>
<tr>
<td>1993</td>
<td>Jurassic Park</td>
</tr>
<tr>
<td>1997</td>
<td>The Lost World: Jurassic Park</td>
</tr>
<tr>
<td>2001</td>
<td>Jurassic Park III</td>
</tr>
</tbody>
</table>
|
すべてについて言えることですが、サーバー上で処理するか、それともクライアント上で処理するかの選択は、そのジョブの必要性によって決まります。この例の場合は、映画の表を配置するという比較的単純なジョブです。ジョブがそれより複雑なもので、例えばソート、検索、追加または削除、あるいは映画をクリックすると詳細が表示されるという動的操作を伴う場合には、これよりも複雑なクライアント・サイドのコードで対処できるはずです。この記事の最後では、実際にクライアント・サイドでソートする例を用いてサーバーに負担をかけ過ぎないようにすることについて説明します。
以上のことを見事に説明する例は、おそらく Google Map です。Google Maps はリッチなクライアント・サイドのコードをサーバー・サイドのインテリジェント・マッピング・エンジンに適合させるという高度なジョブを行います。私はどの処理をどこで行うかを決定する方法の例として、このサービスを使用しています。
JavaScript コードを渡さなければならない場合には XML を渡さないこと
Web ブラウザーに XML データ・ソースを読み取らせて動的にレンダリングさせるという大げさな宣伝から、XML だけが使用できる唯一の手段だと思っているかもしれませんが、そうではありません。極めて賢いエンジニアたちは、Ajaxトランスポート技術を使って XML ではなく JavaScript コードを送信しています。リスト 12の映画表の場合の例を見てください。
リスト 12. Antipat4_fixed.html
<html><head><script>
var req = null;
function processReqChange() {
if (req.readyState == 4 && req.status == 200 ) {
var dtable = document.getElementById( 'dataBody' );
var movies = eval( req.responseText );
for( var i = 0; i < movies.length; i++ ) {
var elTr = dtable.insertRow( -1 );
var elYearTd = elTr.insertCell( -1 );
elYearTd.innerHTML = movies[i].year;
var elTitleTd = elTr.insertCell( -1 );
elTitleTd.innerHTML = movies[i].name;
} } }
function loadXMLDoc( url ) { ... }
var url = window.location.toString();
url = url.replace( /antipat4_fixed.html/, 'antipat4_data.js' );
loadXMLDoc( url );
</script></head><body>
<table cellspacing="0" cellpadding="3" width="100%">
<tbody id="dataBody"><tr>
<th width="20%">Year</th>
<th width="80%">Title</th>
</tr></tbody></table></body></html>
|
上記では、サーバーから XML を読み取る代わりに JavaScript コードを読み取っています。このコードは次に JavaScript コードでeval() 関数を使用して、表の作成に簡単に使用できるデータを取得しています。
リスト 13 に、この JavaScript データを示します。
リスト 13. Antipat4_data.js
[ { year: 1993, name: 'Jurassic Park' },
{ year: 1997, name: 'The Lost World: Jurassic Park' },
{ year: 2001, name: 'Jurassic Park III' } ] |
この機能を使うには、サーバーを JavaScript 言語に対応させる必要がありますが、通常は大した問題ではありません。よく使われている Web言語のほとんどは、すでに JSON (JavaScript Object Notation) 出力をサポートしているからです。
利点は明らかです。この例では、JavaScript 言語を使うことによって、クライアントにダウンロードされるデータ・サイズが 52% 縮小されています。さらにパフォーマンスも向上していて、JavaScriptバージョンを読み取る場合、処理速度は 9% 増加します。9% はそれほど大きい値には思えないかもしれませんが、これはかなり初歩的な例にすぎないことを忘れないでください。データ・ブロックがこれより大きかったり、あるいは構造が複雑になるにつれ、必要となるXML 解析コードは増えていきますが、JavaScript コードの場合はそのまま変わりません。
サーバーに負担をかけ過ぎないこと
サーバーでの処理が少なすぎることの反対は、サーバーに負担をかけ過ぎてしまうことです。前にも言ったように、これはバランスの問題ですが、サーバーの負担を軽くする方法の一例として、クライアント・サイドで映画の表をソートする方法を紹介したいと思います。
リスト 14 は、ソート可能な映画の表です。
リスト 14. Antipat5_sort.html
<html><head><script>
var req = null;
var movies = null;
function processReqChange() {
if (req.readyState == 4 && req.status == 200 ) {
movies = eval( req.responseText );
runSort( 'year' );
} }
function runSort( key )
{
if ( key == 'name' )
movies.sort( function( a, b ) {
if ( a.name < b.name ) return -1;
if ( a.name > b.name ) return 1;
return 0;
} );
else
movies.sort( function( a, b ) {
if ( a.year < b.year ) return -1;
if ( a.year > b.year ) return 1;
return 0;
} );
var dtable = document.getElementById( 'dataBody' );
while( dtable.rows.length > 1 ) dtable.deleteRow( 1 );
for( var i = 0; i < movies.length; i++ ) {
var elTr = dtable.insertRow( -1 );
var elYearTd = elTr.insertCell( -1 );
elYearTd.innerHTML = movies[i].year;
var elTitleTd = elTr.insertCell( -1 );
elTitleTd.innerHTML = movies[i].name;
}
}
function loadXMLDoc( url ) { ... }
var url = window.location.toString();
url = url.replace( /antipat5_sort.html/, 'antipat4_data.js' );
loadXMLDoc( url );
</script></head><body>
<table cellspacing="0" cellpadding="3" width="100%">
<tbody id="dataBody"><tr>
<th width="20%"><a href="javascript: void runSort('year')">Year</a></th>
<th width="80%"><a href="javascript: void runSort('name')">Title</a></th>
</tr></tbody></table></body></html> |
 | |
上記はかなり単純な例です。このコードは、通常ページに表示されるような長い映画のリストには機能しません。しかし、ページの更新もせず、厄介でつまらないソート作業をサーバーで行うこともなく、素早くソートを実行する表を簡単に組み立てられることを実証するものです。
まとめ
私は Ajax を話題にした多数の記事を書き、Ajax をさんざん扱ってきました。IBM developerWorks では Ajax フォーラムのモデレーターも務めているため、Ajax ついての多少の知識と正しい使用方法、誤った使用方法が身についています。開発者が Ajax を単に XML、JavaScript、またはHTML コードをブラウザーに送信するだけの手段と考え、その複雑さを過小評価していることは珍しくありません。ですが私が考える Ajax プラットフォームはブラウザー全体、つまりよく使われているブラウザーすべてを1 つにまとめたものです。なぜなら、Ajax ではこれらのブラウザーすべての特徴を知る必要があるからです。
要は、Ajax について学ぶべきことはたくさんあり、その学習過程で経験する過ちもたくさんあるということです。この記事が、Ajax のいくつかの落し穴に陥らないための手助け、あるいはそこに陥ってしまった場合に抜け出すための手助けとなることを願います。いずれにしても、成功から学べることは多くありますが、たいていの場合、失敗から学べることはもっとたくさんあります。
参考文献 学ぶために
議論するために
著者について  | |  | Jack D. Herringtonは、20年以上の経験を持つシニア・ソフトウェア・エンジニアです。著者には、「Code Generation in Action」、「Podcasting Hacks」、そして近々刊行予定の「PHP Hacks」の3冊があります。彼は30本以上の技術記事も執筆しています。 |
記事の評価
|