Ajaxを用いたウエブページ プログラム演習7

前回のJavaScriptの説明をふまえて、ウエブAPIを用いたCGI版のアプリケーションと同様の機能を持つajaxの仕組みを利用したウエブ検索アプリケーションを作成してみましょう。

クロスドメインの制約

JavaScriptAjaxを用いて外部のリソースを取得する場合、その「外部リソースは同じドメインでなければいけない」というクロスドメインの制約があります。クロスドメイン制約を回避する方法としてiframeやJSONPを用いる方法がありますが、今回はその制約の元、アプリケーションを作成してみましょう。
といってもヤフーAPIは外部ドメインであるため、Proxyの役割をもつ中継サーバをアプリケーションを動作させるドメインと同じ場所にたてる必要があります。イメージ的には

|APIサーバ(外部ドメイン)|<---PHPなど--->|Proxyサーバ(同ドメイン)|<---ajax--->|JavaScriptアプリ|

という形です。Proxyサーバ演習6で利用したアプリケーションを拡張して代用させましょう。

演習6のアプリケーションをProxyとして拡張する

演習6のアプリケーションのURL仕様はqueryにキーワードをactionにはウエブ(web)か画像(image)かの選択をしてHTMLをoutputするものでした。URL例:

http://localhost:8888/prog6/index.php?query=dvd&action=web

これを拡張して outputのパラメータを追加しこれがxmlであればXMLとして応答を返すように拡張しましょう。すなわち

http://localhost:8888/prog6/index.php?query=dvd&action=web&output=xml

とすると同内容のコンテンツがXML形式で応答させます。
プログラムの変更箇所は
http://trac.assembla.com/cX0noKTPir3A7Oab7jnrAJ/changeset/11
になります。肝の部分は Viewを管理する以下のクラスです。

bash-3.2$ cat prog6/VIEW/ViewManager.class.inc 
<?php
    class ViewManager {
        public static function viewSmarty($template, $params = array(), $output = 'html') {
            if($output == "html" || $output =="") {
                require_once('Smarty/Smarty.class.php');
                $SMARTY_TEMPLATE_DIR = './VIEW/';

                $smarty = new Smarty();
                $smarty->template_dir = $SMARTY_TEMPLATE_DIR;
                $smarty->compile_dir  = $SMARTY_TEMPLATE_DIR . 'cache/';
    
                $smarty->assign('p', $params);
                header("Content-type: text/html; charset=utf-8");
                $smarty->display($template);

            } elseif($output == "xml") {
                require_once('XML/Serializer.php');
                $serializer = new XML_Serializer(array(
                                                       "rootName" => "myXML",
                                                        XML_SERIALIZER_OPTION_DEFAULT_TAG => 'item'
                                                    ));
                if($serializer->serialize($params)) {
                    header("Content-type: text/xml; charset=utf-8");
                    print $serializer->getSerializedData();
                }
            }
        }
    }
?>

$outputの引数を追加しそれがxmlであった場合はSmartyでのoutputではなくXML_Serializerを用いて$paramをそのままXML化し出力させています。
これで準備が完了です。

ajaxを用いた検索アプリケーション (ライブラリを用いない場合)

では、JavaScriptのアプリを作ってみましょう.前回、JavaScriptライブラリを用いると簡易に開発できることを紹介しましたが、まずはこれを用いない場合にどのようになるかを体験するために、ライブラリは用いずに作成することにします
基本となるHTMLは

bash-3.2$ cat prog7/index-1.html 
<html>
<head>
<title>Sample Program 7 JavaScript-1</title>
<meta http-equiv="Content-Type" content="text/html" charset="utf-8">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<script src="./prog7-1.js"></script>
</head>
<body>

<form>
<input id="searchQuery" type="text">
<select id="searchOpt">
    <option>web</option>
    <option>image</option>
</select>
<input id="searchBtn" type="button" value="search">

<div id="searchResult"></div>

</body>
</html>

JavaScriptで操作する各要素にidを付与しています。またmetaにjavascriptを追加。script srcに 今回作成する jsファイルを読み込むようにしています。では javascriptファイル prog7-1.jsを説明します

bash-3.2$ cat prog7/prog7-1.js 
function createHttpRequest() {
    if(window.ActiveXObject) {
        try {
            return new ActiveXObject("Msxml2.XMLHTTP");
        } catch (e) {
            try {
                return new ActiveXObject("Microsoft.XMLHTTP");
            } catch (e2) {
                return null
            }
        }
    } else if(window.XMLHttpRequest) {
        return new XMLHttpRequest();
    } else {
        return null
    }
}

function getAction() {
    var selectList = document.getElementById("searchOpt");
    for(var i=0; i<selectList.options.length;i++) {
        if(selectList.options[i].selected) {
            return selectList.options[i].value;
        }
    }
}

function doSearch() {
    var query = document.getElementById("searchQuery").value;
    var action = getAction();

    var httpObj = createHttpRequest();
    httpObj.open("GET", "/prog6/index.php?query="+query+"&action="+action+"&output=xml", true);
    httpObj.onreadystatechange = function () {
        if(httpObj.readyState == 4) {
            displayResult(httpObj.responseXML);
        }
    }
    httpObj.send("");
}

function displayResult(data) {
    var action = data.getElementsByTagName("action")[0].firstChild.nodeValue;
    if(action == "web") {
        displayWebResult(data.getElementsByTagName("resultSet")[0]);
    } else if (action == "image") {
        displayImageResult(data);
    }
}

function displayWebResult(data) {
    var resultSet = data.getElementsByTagName("item");
    var resultCount = resultSet.length;
    var resultHtml = "";
    for(var i=0; i<resultCount; i++) {
        resultHtml += '<li><a href="';
        resultHtml += resultSet[i].getElementsByTagName("ClickUrl")[0].firstChild.nodeValue;
        resultHtml += '">';
        resultHtml += resultSet[i].getElementsByTagName("Title")[0].firstChild.nodeValue;
        resultHtml += '</a>';
    }
    document.getElementById("searchResult").innerHTML = resultHtml;
}

function displayImageResult(data) {
    var resultSet = data.getElementsByTagName("item");
    var resultCount = resultSet.length;
    var resultHtml = "";
    for(var i=0; i<resultCount; i++) {
        resultHtml += '<a href="';
        resultHtml += resultSet[i].getElementsByTagName("ClickUrl")[0].firstChild.nodeValue;
        resultHtml += '"><img src="';
        resultHtml += resultSet[i].getElementsByTagName("ThumbnailUrl")[0].firstChild.nodeValue;
        resultHtml += '"></a>';
    }
    document.getElementById("searchResult").innerHTML = resultHtml;
}

function addEvent(element, eventName, func) {
    if(element.attachEvent) {
        element.attachEvent("on"+eventName, func);
    } else {
        element.addEventListener(eventName, func, false);
    }
}

function init() {
    addEvent(document.getElementById("searchBtn"), "click", doSearch);
}

addEvent(window, "load", init);

各関数は以下の機能を持ちます

createHttpRequest ajaxで通信するためのリクエストクラスを作成します.ブラウザの違いにより、そのオブジェクトの生成方法が異なるため、クロスブラウザの処理が含まれています
getAction web imageのselectがどちらを選択しているかを取得します。(この部分IEに対応してません.修正予定)
doSearch 検索ボタンを押されたときの動作です.createHttpRequestでリクエストオブジェクトを生成してajax通信を行います
displayResult ajaxで受信したData(XMLObject)を元に検索結果を描画します(実際はweb,imageの判別を行い、それぞれの各描画関数にdataを渡します)
displayWebResult ウエブの検索結果を描画します
displayImageResult 画像の検索結果を描画します
addEvent ブラウザのイベントを登録します. ここもブラウザ毎に登録方法の差異があるため、クロスブラウザの処理があります
init HTMLが読み込まれた時点で一度読み込まれる初期化処理を記述しています。実際には検索ボタンをクリックしたときにdoSearch関数をコールするように登録しています

doSearch関数のhttpObj.open で作成したProxyのURLを指定します。受信のXMLはこのProxyからの結果になります。

ajaxを用いた検索アプリケーション (JQueryライブラリを利用)

次にJQueryライブラリを用いて上と同じアプリを作ってみましょう. HTMLの部分は読み込むJQueryのサイトからダウンロードしたライブラリを読み込む部分が追加となるだけであとは同じです

<script src="./jquery-1.2.6.min.js"></script>
<script src="./prog7-2.js"></script>

次にprog7-2.jsのJavaScript部分です JQueryを用いているのでかなりシンプルになります

bash-3.2$ cat prog7/prog7-2.js 
function doSearch() {
    $("#searchResult").html("");
    $("#searchResult").fadeOut("fast");
    var apiParam = "query=" + $("#searchQuery").val();
    apiParam += '&action=' + $("#searchOpt").val();
    apiParam += '&output=xml';

    $.ajax( {
                type: "get",
                url: "/prog6/index.php",
                dataType: "xml",
                data: apiParam,
                success: function(data) {
                    displayResult(data); 
                }
    });
}

function displayResult(data) {
    var action = data.getElementsByTagName("action")[0].firstChild.nodeValue;
    if(action == "web") {
        displayWebResult(data.getElementsByTagName("resultSet")[0]);
    } else if (action == "image") {
        displayImageResult(data);
    }
}

function displayWebResult(data) {
    var resultSet = data.getElementsByTagName("item");
    var resultCount = resultSet.length;
    var resultHtml = "";
    for(var i=0; i<resultCount; i++) {
        resultHtml += '<li><a href="';
        resultHtml += resultSet[i].getElementsByTagName("ClickUrl")[0].firstChild.nodeValue;
        resultHtml += '">';
        resultHtml += resultSet[i].getElementsByTagName("Title")[0].firstChild.nodeValue;
        resultHtml += '</a>';
    }
    $("#searchResult").html(resultHtml);
    $("#searchResult").fadeIn("slow");
}

function displayImageResult(data) {
    var resultSet = data.getElementsByTagName("item");
    var resultCount = resultSet.length;
    var resultHtml = "";
    for(var i=0; i<resultCount; i++) {
        resultHtml += '<a href="';
        resultHtml += resultSet[i].getElementsByTagName("ClickUrl")[0].firstChild.nodeValue;
        resultHtml += '"><img src="';
        resultHtml += resultSet[i].getElementsByTagName("ThumbnailUrl")[0].firstChild.nodeValue;
        resultHtml += '"></a>';
    }
    $("#searchResult").html(resultHtml);
    $("#searchResult").fadeIn("slow");
}

$(window).load(function() {
    $("#searchBtn").click(function() {
        doSearch();
    });
});

ajaxの部分は JQueryライブラリで提供される

$.ajax 

で処理しています document.getElementById("ID") で DOMのノードを特定する処理も JQuery

$("#ID")

で行えます。innerHTMLの処理は

$("#ID").html(挿入したいHTML文字列);

検索ボタンをクリックした場合の処理は

$("#searchBtn").click(function() {ここに処理を書く});

です。クロスブラウザを意識した処理はすべてJQuery内部で行われているので処理が簡潔になります。
JQueryはその他にもエフェクト処理を簡易に行う関数も提供されています。上で一部使用しました

$("#searchResult").fadeIn("fast");
$("#searchResult").fadeIn("slow");

の部分がそれに当たります。これもJQueryを用いない場合はTimerオブジェクトを生成してエレメントのCSS:alpha(透明度)の値を増減させる処理の記述が必要になりますが JQueryを用いると簡単に実現できます。

受信したXMLの解析部分がまだまだスリムとはいえません(XPathを利用する方法もありますが)。この部分は受信データをXMLではなく JSON形式のデータにすればさらに、プログラムが簡易になります。これは次の章で説明することにします。

今回のプログラム一式は
http://trac.assembla.com/cX0noKTPir3A7Oab7jnrAJ/browser/prog7
にありますので、必要に応じて参照してください。