デベロッパー2006年9月26日 11:00
文字サイズ文字サイズ小文字サイズ中文字サイズ大

AJAXとDojoとStrutsを組み合わせWebページを高速化する

この記事のURLhttp://japan.internet.com/developer/20060926/26.html
著者:Doug Tillman
海外internet.com発の記事

はじめに

 Webページの開発に携わっている人なら、きっとAJAX(Asynchronous JavaScript and XML)について聞いたことがあるでしょう。AJAXコールを慎重に利用すると、サイトの操作性を向上させ、人目を引くようなブラウザクライアントアプリケーションを作成することができます。本稿では、AJAXを使ってStrutsアクションを呼び出す方法と、Tilesを使って呼び出し側ページの更新応答を簡単に作成する方法について説明します。StrutsやTiles、JavaScriptを使用した経験がない人は、本稿の最後で紹介しているチュートリアルや文献に目を通して、背景知識を把握しておくとよいでしょう。JavaScriptの書き方を十分に理解していて、StrutsやTilesを使用した経験がある人は、このまま読み進めてください。

前提条件
  • Dojo(Webサイトから無料でダウンロードできます)
  • JavaScriptおよびXML構文解析の知識など、AJAXの基本事項の理解
  • TomcatやJBoss(Servlet 2.4以降)など、JavaのWebコンテナ
  • Jakarta Struts Webフレームワーク、Tiles Webフレームワーク、それらに関連するtaglibについての知識

 実は、AJAXは新しいテクノロジではありません。XML-RPCコールは何年も前からありました。また、しばらくはSOAPを使ったWebサービスが大いに注目を集めていましたが、さまざまな理由からAJAXほど大々的に取り上げられることはありませんでした。開発コミュニティにこれほどの活気をもたらしたAJAXの利点を1つ1つ取り上げて正確に説明することは困難です。ただし、サービス指向アーキテクチャの利点が認められつつあることや、AJAXによってユーザーインターフェイスが著しく改善されていることがAJAXの利点と関係しているのは確かです。

 従来のWebの「要求-応答」モデルでは、ユーザーが閲覧しているデータを変更するにはWebフォームの送信とページ全体のリロードが必要でした。一方、AJAXには、ページの一部分をその場で修正する仕組みが用意されています。この処理は高速に行われるため、ユーザーにコンテキストの中断を意識させません。最近のブラウザはページ上のすべての要素を解析木(Document Object Model:DOM)という形で整理しており、AJAXの応答処理はこのDOMの一部分を選択的に更新できるため、サーバーからページ全体をリロードするというコストのかかる処理を行わずに済むのです。Dojoツールキットは、リモートリソース(本稿の場合はURL)を呼び出すための強力なJavaScript APIであり、堅牢性を備えた補助的なJavaScriptユーティリティクラス群も提供しています。

 ページ全体をリロードせずにリモートリソースを呼び出せるということは、ユーザーがページ上のデータとのインタラクションを段階的に行えることを意味しています。一般的にWebページは、formタグを使って、サーバーに再送信すべきページの一部を区別しています。このformタグのアクション属性で指定されたリソースに対して、フォームの各フィールドがポストされます。Dojoはページ上のデータの更新/置き換えを行うための「フォームノード」の送信をサポートしていますが、これは必須ではありません(後で実際の例を紹介します)。さらにHTMLの仕様には、formタグ以外で区切られたノードの境界をページ上で定めるためのdivタグが用意されています。divタグで囲まれたデータは、通常、サーバーからAJAXの応答を受け取ると更新または置き換えが行われます。

 Dojoの設定はとても簡単です。メインのJavaScriptリソースファイル「dojo.js」をWebサーバー上に置き、ページ内からそのファイルを参照するだけです。次のようなJavaScriptのブロックを含めることで、Dojoに「自分自身の居場所」を伝えます。

var djConfig = {
    baseRelativePath: "js/dojo",
    isDebug: true,
    preventBackButtonFix: false
};

 isDebugフラグをtrueに設定すると、読み込み対象となるページに有益なデバッグ情報が出力されます。「/src」ディレクトリはこのベースパスの指す場所にあるので、これにより、Dojoはスクリプト内で参照されているライブラリに確実にアクセスできるようになります。

 「tiles-def.xml」には、Dojoや他のJavaScriptのリソースファイルを再利用可能な形で指定するためのベーステンプレートを定義します。他のタイルは、拡張によってこの「ベースタイル」を再利用できます。以下に、ベースタイルの定義を示します。

<!-- Foundation Template Definitions -->
<definition name="basic" path="/templates/basic.jsp">
    <putList name="reusableJavascripts">
</definition>

 この「basic.jsp」タイル(本稿のサンプルコードを参照)には、Strutsのlogicタグが含まれています。このタグによって、「tiles-def.xml」のputList要素からタイルに渡される引数を読み取ります。

<tiles:importAttribute  scope="page"/>
<tiles:useAttribute id="tileDefinedJS"
                    name="reusableJavascripts" scope="page"/>

<logic:present name="tileDefinedJS">
    <logic:iterate id="tileDefinedJS" name="reusableJavascripts">
        <script language="javascript"
                src="<bean:write name="tileDefinedJS"/>"
                type="text/javascript"></script>
    </logic:iterate>
</logic:present>

 このベースタイルを拡張する他のタイルは、「tiles-def.xml」内の自身のタイル定義のreusableJavascriptsリストに目的のスクリプト名を追加で指定することで、適切なDojoのJavaScriptリソース参照を自動的にインクルードするようになります。

<definition name="famousPeople" extends="jstemplate" >
    <put name="body" value="/tiles/famousPeopleList.jsp"/>
    <putList name="reusableJavascripts">
        <add value="js/dojo/dojo.js"/>
    </putList>
</definition>

 Tilesを使ってJavaScriptリソースファイルの参照を追加すると、タイプミスや、不注意で間違った場所を指定する可能性が低くなります。またTilesによって、アプリケーションの保守性も高まります。Tilesはこれらの定義を集中管理してくれるため、開発者は個々のJavaScriptページを見て.jsリソースが利用されているかどうかを判断せずに済みます。

 余談ですが、Dojoを(Webサーバーを使わない)ローカル開発環境向けに設定する場合は、少し違ったアプローチが必要になります。ローカルのDojoリソース(dojo.js)のURI表記がブラウザ依存であることに注意してください(私は苦心の末にこのことに気付きました)。詳しくは、以下のマークアップを見てください。

<!-- this form recognized by both IE and Firefox -->
<script src="file:///c:/dojo-0.2.2-ajax/dojo.js"
        type="text/javascript" language="JavaScript1.5" ></script>

<!-- this form recognized only by IE - not Firefox -->
<script src="/dojo-0.2.2-ajax/dojo.js" type="text/javascript"
        language="JavaScript1.5" ></script>

ソートの例

 DojoがどのようにStrutsやTilesと連携するのかを理解するため、具体的な問題に取り組んでみましょう。付録のサンプルコードでは、まず有名人のリストを図1のような形式でユーザーに表示します。

図1 最初のページ読み込み時の画面
図1 最初のページ読み込み時の画面

 このデータを、最初のページ読み込み時と異なる順序でソートして表示するには、2つの方法があります。1つ目は、従来どおりの、フォーム全体をサーバーに送り返してデータを並べ替え、ページ全体をリロードするという方法です。2つ目は、もうお分かりでしょうが、AJAXを利用する方法です。このAJAXによる方法を実装するには、ページのマークアップを編集して、divブロック内部にソート可能なリストを含め、ページ上のどこかのボタンクリックまたはその他のクライアントサイドのイベントから、図2のようにデータを並べ替えるためのAJAXコールを呼び出すようにします。

図2 ドロップダウンリストから[Ascending(昇順)]を選択すると、サーバーにアクセスすることなく、データの再ソートを行うAJAXコールが呼び出され、リストがすばやく並べ替えられる
図2 ドロップダウンリストから[Ascending(昇順)]を選択すると、サーバーにアクセスすることなく、データの再ソートを行うAJAXコールが呼び出され、リストがすばやく並べ替えられる

 具体的には、次のようなdivタグを記述することが考えられます。

<div id="famousPeople">
    <table>
        <tr>
            <td>
                <ul>
                    <li>Dave Thomas</li>
                    <li>Ronald McDonald</li>
                    <li>George Washington</li>
                </ul>
            </td>
        </tr>
    </table>
</div>

 ただし、上記のdivマークアップは手で書いたものではなく、生成されたHTMLです。このような単純なコードを繰り返し書くよりも、Tilesを使用して、ブロック全体を再利用しやすく、エラーの起こりにくいものにした方が得策です。そこで、次のようにします。

<div id="famousPeople">
    <tiles:insert definition="famousPeopleList" flush="true">
        <tiles:put name="listbean" beanName="famousFolkForm"
                   beanProperty="famousFolkList" />
    </tiles:insert>
</div>

 このサンプルコードでは、有名人の名前のキャッシュリストを「famousFolks」というセッション属性で定義しています。このリストは、Dojoが次のようなURLに対する標準的なStrutsマッピングを使って呼び出すサーブレットによって再ソートされます。

<action path="/famousPeopleSort"
        name="famousFolkForm"
        scope="request"
        validate="false"
        type="com.tillman.dojo.action.FamousPersonageSort">
<forward name="success.sort" path="/sortUpdateXml.jsp"/>
</action>

 新しくソートされたリストは「sortUpdateXml.jsp」に渡されます。このJSPには、<tiles: insert>タグと、AJAX(Dojo)の応答において返されるマークアップを作成する際に名前のリストを受け取るTilesコンテキストが含まれています。

 それでは、もう少し機能を追加してみましょう。このAJAXコールを含んでいるページには、sortByという名前のドロップダウンリストも含まれています。このドロップダウンリストのonchangeイベントから、JavaScriptメソッドを呼び出すようにします。sortByドロップダウンのマークアップは次のようになります(コード全体についてはダウンロードサンプルを参照してください)。

<select name="sortBy" styleId="sortBy"
        onchange="javascript(doSort(this.value));">
    <option name="person" value="George Washington">George Washington
...
</select>

 新しいソートパラメータが選択されると、<head>タグで定義された(またはJavaScript(.js)リソースファイルからインクルードされた)doSort()というJavaScriptメソッドが呼び出されます。こうした事情を念頭に置きながら、今度はDojoコールのためのコードを調べてみましょう。

 Dojoには堅牢なAPIが多数用意されており、パッケージ化の構造もおおむね直感的です。そうは言っても、AJAXコールのセットアップにはdojo.ioパッケージのdojo.io.bind()メソッドを使用するのだということを私が理解するまでには少し時間がかかりました(この「io」はフォームに対する入出力を意味するに違いありません。個人的には、Javaにならって.net.packageにした方がいいのではないかと思うのですが)。

function doSort(sortTerm) {

dojo.io.bind( {
    url: "famousPeopleSort.shtml",
    content: {sortedBy: sortTerm},
    method: "POST",
    mimetype: "text/html",
    load: function(type, value, evt) {
        processReturnValue(value);
    },
    backButton: function(){ //for maintaining Dojo back button stack
        dojo.io.bind({
            url: "famousPeopleSort.shtml",
            content: {sortedBy: previousSortTerm},
            method: "POST",
            mimetype: "text/html",
            load: function(type, value, evt) {
                resetDropDown(previousSortTerm);
                processAjaxResponse(value);
            },
            error: function(type, error) { alert("Error: " + error); }
        });
    },
    error: function(type, error) { alert("Error: " + error.reason); }
});

 このメソッド呼び出しでは、HTTP PostのURL引数に、「struts-config.xml」(サンプルコードを参照)内で「famousPeopleSort.shtml」にマッピングされているサーブレットを割り当てます。content引数は、ドロップダウンリストのonchangeイベントからこの関数に渡されるsortTerm引数を保持しており、これをPostの要求パラメータとして設定します。method引数はHTMLのformタグのmethod属性に似ており、mimetype引数はAJAXの応答時に期待されるデータの型を指定します。load引数は、AJAXの応答が返されたときに呼び出される関数を定義しています。

 上記のJavaScriptメソッドから、Dojoがブラウザの[戻る]ボタンの動作を管理できることがお分かりになったかもしれません。Dojoは独自のコールスタックを作成することでこれを行っています。これが便利なのは、もちろん、多くのユーザーは[戻る]ボタンを使って前のページに戻ろうとするからです。こうした状況では、ページ全体がリロードされず、Dojoによる処理が失われるため、この動作は混乱を招くかもしれません。個々のDojoコールは直前のコールの上にスタックされており、ユーザーが[戻る]ボタンをクリックすると、一番上のバインドされた(つまり、io.bindメソッドによって呼び出された)コールがスタックからポップされて使われます。

 このスタックは、「dojo.js」と同じディレクトリにあるHTMLファイル「iframe_history.html」の助けを得て管理されています。私の場合はDojoの[戻る]ボタン管理機能をうまく実装できましたが、他の開発者からはうまくいかなかったという報告も聞いています。問題に遭遇したときには、私の事例が少しでも参考になれば幸いです。

 [戻る]ボタンをプログラムで管理するのは素晴らしいことですが、仕事の関係者がDojoの[戻る]ボタンの動作に不慣れな場合は、開発に入る前に、彼らのためにプロトタイプやデモを作成すべきです。AJAXコールは、「想定されるもの」とはかけ離れたナビゲーションやページフローのパラダイムを作り上げます。さらに、サイトの反応を鈍らせる何か未知の問題(または目に見えない問題)が起こっていると、大抵はユーザーに嫌な思いをさせることになり、アプリケーションは失敗に終わる可能性が高くなります。そのため、Dojoコールの実行開始時に起動し、完了時には消えるプログレスバーなどのビジュアルキュー(視覚的な合図)の追加を検討したくなるかもしれません。

 返されたデータがどのように処理されるかを説明する前に、そのデータを保持する応答ドキュメントを見てみましょう。今回の有名人ソートのための応答ドキュメントはXML形式になっており、これが、前述のdiv内のタイル挿入ステートメントで参照されるJSPタイルのコンテナとして働きます。

<%@page contentType="text/xml" %>
<%@taglib prefix="tiles" uri="/WEB-INF/tiles.tld" %>
<%@taglib prefix="html" uri="/WEB-INF/struts-html.tld" %>
<%@taglib prefix="bean" uri="/WEB-INF/struts-bean.tld" %>

<ajax-response>
    <field id="famousFolk1" attribute="innerHTML">
        <![CDATA[
            <tiles:insert definition="famousPeople" flush="true">
                <tiles:put name="famousFolkList" beanName="famousFolkForm"
                    beanProperty="famousFolkList"/>
            </tiles:insert>
        ]]>
    </field>
</ajax-response>

 tiles:insertはサーバー上で処理されるので、ソート操作に関するDojoコールはリストを再び処理し、その結果をタイルによって作成されたHTMLに組み込みます。AJAXコールが呼び出されると、動的に生成されたこのHTMLが、bind()メソッドで指定したJavaScript関数の処理のためにajax-responseというXML要素内にネストされます。

function processResponse(response) {

    if (djConfig["isDebug"]) {
        dojo.debug("ajax response: " + response);
    }
    //if the XML element ?ajax-response?
    //isn’t found then write out what was returned
    if  (response.toLowerCase().indexOf("<ajax-response>") < 0) {
        document.write(response);
        document.close();
        return;
    }

    //parse the AJAX XML response into a document.
    var xmlDoc = dojo.dom.createDocumentFromText(response);

    //refer to the XML response above ? the ?field? element is inside
    //the response document.
    var fields = xmlDoc.getElementsByTagName("field");

    for (var i = 0; i < fields.length; i++) {
        var id = fields[i].getAttribute("id");
        var attribute = fields[i].getAttribute("attribute");
        var value = null;

        if (fields[i].hasChildNodes()) {
            for (var j=0; j<fields[i].childNodes.length; j++) {
                var currentNode = fields[i].childNodes[j];
                if(currentNode.nodeName.toLowerCase()==
                    "#cdata-section") {
                    value = currentNode.nodeValue;
                }
            }
        }
        //Dynamically builds a function
        //called replaceValue and then invokes it
        eval("window.replaceValue = function(value) { dojo.byId(’"
 + id + "’)." + attribute + " = value;}");
        replaceValue(value);
    }
}

 この応答の処理は、返されるXMLドキュメントの構文解析の途中で行われます。上記のコードは、この応答内にあるフィールド要素を探し、その内容を取り出します。続いて、ページ上のdivをこの応答の指定された#cdata-sectionの値で置き換えます。もちろん、この置き換えはDOMの更新に対応しており、ブラウザはコンテンツの変更を反映するために表示の更新を行います。他にもきっとあるでしょうが、これは私自身が機能させることができた唯一の、ブラウザに依存しない形の構文解析でした。

 ここ数年、StrutsとTilesは企業でのJava開発における重要なフレームワークになっています。また、AJAXとDojoツールキットは、問題を分離し、コードおよび表示要素の双方の再利用を促進する大まかなサービス指向の方法でStrutsとTilesを利用するための優れた組み合わせです。こうした強力なフレームワークどうしを統合するためには習熟が必要ですが、その努力に見合うだけの結果が得られます。

最前線で利用されるDojoの問題
 私はDojoをとても気に入っていますが、[戻る]ボタンや一部の不親切なデバッグステートメントに難点があると感じました。最初にインストールしたDojo 0.2.2には、dojo.byIdの呼び出しにいくつか問題がありました。Strutsはidプロパティをhtml:hiddenタグのプロパティとしてサポートしていません。IEの場合は、dojo.byId("field")の呼び出し途中でこのプロパティが見当たらなくても、うまくやり過ごしてそのフィールドの名前を参照します。一方、Firefoxの場合は、エラーをスローして、このフィールドがそのようなプロパティ(この場合はidプロパティ)をサポートしていないことを知らせます。私にとって幸運だったのは、同僚の1人が、html:hiddenタグのstyleldフィールドがJSPのレンダリング時にidプロパティに変換されるんだよとヒントを与えてくれたことでした。さらに運が良かったのは、そのときの会話でもう1人の同僚が、[戻る]ボタンに類似した機能を実行するためにはchangeURLパラメータをdojo.bind()メソッドに追加しなければならない、と教えてくれたことです。
 これで問題の全容が見えてきたのではないでしょうか。私もそうでした。私の同僚たちは、彼らが[戻る]ボタンを動作させるために行ったさまざまなハッキングに関する知識を惜しみなく与えてくれました。しかし彼らのヒントがあったにもかかわらず、これらの問題や次から次へと現れる新たな問題への対処方法を思い出すには、思ったよりも時間がかかりました。本稿の発表前にこれらすべての問題の原因を探り当て、あらゆる問題を解決したサンプルコードを紹介できればよかったのですが、残念ながらそれはできていません。また、インストールされているFirefox拡張機能の数など、ある種の特異な環境上の問題も、不安定な動作につながる可能性があります。
 これらの問題のいくつかに遭遇した後、自分のローカル環境にあったDojoツールキットのアップグレードを行ったため、現在、私のDojoに関する不完全ながらも増えつつある知識は、バージョン0.2.2と0.3.1の両方に対応しています。ちょっとした調整:Webを検索してみたところ、「bootstrap1.js」ファイルでは、djConfigpreventBackButtonFixのデフォルト値がtrueに設定されていることが分かりました。それまで私のコードではこれをfalseに設定していたのですが、この「bootstrap1.js」の値を同じように変えると、驚いたことに、Firefoxだけに見られたページ読み込みのエラーが消えたのです。普通、[戻る]ボタンはFirefoxと連動するのですが、バージョン0.2.2でうまくいったのと同じテクニックを0.3.1で使うと、フィールドの一部の更新がスムーズに行われませんでした。
 また、私のサンプルをOperaで読み込んだところ、[戻る/進む]ボタンの機能はOperaではサポートされていません、というメッセージが表示されましたが、それ以外のコードは正常に動作していました。奇妙なことに、ある同僚がバージョン0.2.2を使ってテストした際にはOperaでも正常に動作したそうです。要するに、Dojoにはまだ何らかの問題が残っているので、正しい方向に向かってDojoの開発が活発に進められている限り、当面は困難に遭遇する覚悟が必要と言えそうです。

参考文献

著者紹介

Doug Tillman(Doug Tillman)
Grainger.comで活躍するベテランのJavaおよびPython開発者。開発者の役に立つツールを作成することに強い関心を持ち、積極的に取り組んでいる。

Copyright 2008 Jupitermedia Corporation All Rights Reserved.http://www.internet.com/