唐突にAJAX入門

UTMCの五月祭反省部誌に寄稿したチュートリアルソースコードのテストは一切やってない.

はじめに

AJAX: Asynchronous JAvascript & Xml とは,画面遷移のないWEBアプリケーションを実現する技術です.
ブラウザは通常,HTMLファイル全体をサーバに要求した後,得たファイル全体を描画します.しかし画面の中で変更したい部分に関するデータだけを非同期にGETし,その後,JavaScriptによって変更したい部分だけを更新することもできます.すると画面遷移が不必要になるのです*1.非同期なリクエストにはいくつかの手法がありますが,ここでは狭義のAJAXとして XMLHttpRequest によってXMLでデータを得る方法を説明します.

必要なもの

IE, Mozillaもしくは匹敵する機能を持つブラウザとか,Apache等のwebサーバとか,それを走らせる鯖とかは,前提条件とさせてもらって...

  1. ブラウザに開かせるためのUI担当のHTMLファイル (index.html)
  2. HTMLファイルから読み込むUI制御ロジック担当のJavaScriptファイル (ajax.js)
  3. UTF-8XMLを吐くデータ処理担当のCGIスクリプト (backend.cgi)

バックエンドのCGI

まず問題なのは「どんなWEBアプリケーションを実装するか」ですが,ここではCGI講座,データベース講座まで踏み込むつもりはないので「とりあえずXMLを返す」だけの適当なものでお茶を濁すことにします.以下のソースを(UTF-8で)backend.cgi として保存し,実行属性を付けるなどしてCGIとして機能するようにして下さい.

#! /bin/bash

echo "Content-Type: application/xml; charset=utf-8\n\n"
echo <<EOS
<?xml version="1.0"?>
<ksms>
    <name>はずむ</name>
    <name>とまり</name>
    <name>やす菜</name>
</ksms>
EOS

UIの大枠を提供するHTMLファイル

次はUIの大元となる index.html を書きましょう.やはり手を抜いて,まず以下のようにします(保存はUTF-8で).

<html lang="ja">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>AJAXのサンプル</title>
    <meta http-equiv="Content-Script-Type" content="text/javascript">
    <script type="text/javascript" src="ajax.js"></script>
</head>
<body>
    <h1>AJAXのサンプル</h1>
    <p>メンバー一覧:</p>
    <div id="displayDiv"></div>
    <form action="./">
        <input type="button" value="Do AJAX!">
    </form>
</body>
</html>

ブラウザには以下のような殺風景な画面が表示されるはずです."Do AJAX!" と書かれたボタンをクリックすると,グループのメンバーの情報をサーバ側のCGIから取得し,一覧表示する... というものを意図しています.

ただし,中身が空の div 要素に注意してください.以降ではJavaScriptを使い,この内部にあれこれ書き加えていくことになります.そのための選択用目印として id="displayDiv" としました.

本題: JavaScriptスクリプト

とうとう本題です.以降ではそれなりにプログラミングを行うので,シートベルトを締めて付いてきてください.
まずは鍵となる XMLHttpRequest オブジェクトを用意する関数を ajax.js で定義します*2.これはIE系とMozilla系とでその辺りの実装に非互換性があるためで,いずれ統一されればシンプルになるでしょうから,何も考えずにコピペすれば十分です.Operaがどうとか心配な方はご自分でググって調べて下さい.

function xmlHttpInitialize() {
    try {
        return new ActiveXObject("Msxml2.XMLHTTP");
    }
    catch (e) {
        try {
            return new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch (e) {
            if (typeof XMLHttpRequest != 'undefined') {
                return new XMLHttpRequest();
            }
        }
    }
    return false;
}

次にこれを使って XMLHttpRequest を生成しましょう.なお,生成は非同期リクエストのたびに行う必要があります(1回生成した XMLHttpRequest で複数回のリクエストを投げることはできません).2008年4月6日追記: これは誤り.複数回のリクエストを投げることはできる.openを呼んだ時点でリセットが行われる(だからイベント・ハンドラの設定はopenの後にすること).よってHTML内のボタンの onclick イベントに対してイベントハンドラを定義し,その内部で生成することになります.まず index.html を書き換え,

    <input type="button" value="Do AJAX!">

    <input type="button" value="Do AJAX!" onclick="ButtonOnClick()">

次にイベントハンドラ ButtonOnClick の定義に入ります.下記のソースは,以降でどんどん書き換えてゆくのでこのままファイルに写さないようにして下さい.

var xhr;
function ButtonOnClick() {
    xhr = xmlHttpInitialize();
    xhr.open('GET', 'backend.cgi', false);
    xhr.send(null);
}

なお,スコープの都合で xhr をグローバルに宣言しました*3
メソッド open では,リクエストに関する指定を行います.

第1引数
文字列 'GET' または 'POST' を指定.意味は読んで字の如し.
第2引数
リクエストを送るURLを文字列で指定*4
第3引数
リクエストを非同期にするかどうか(リクエスト結果を待つか否か)について: false なら非同期(待たない),true なら同期(待つ).

メソッド send の実行時点で,実際のリクエストが送られます.ここでは GET リクエストなので引数に null を指定しましたが((Operaだと null ではエラーになり,空文字列 '' でないと駄目だとか? よく知らない.)),POST ならその中身を文字列で指定します.
ここまでで「"Do AJAX!" ボタンのクリックに合わせて非同期なリクエストを投げる」ことは実現できました.しかし無論これだけでは意味はなく,リクエストの結果を使って画面を変化させなければなりません.ここから,非同期ならではのややこしい点が出てきます.非同期なリクエストが終了したタイミングでないと,リクエストでCGIから得たXMLデータを使った実質的な処理が実行できないのです.そこでイベントハンドラの出番です.XMLHttpRequest には,自分の処理工程に何らかの進行が見られたときに,登録されたイベントハンドラを呼び出す機能があります.登録する関数名を OnReadyStateChange1 とするとき,登録にはこうします:

    ...
    xhr.send(null);

    ...
    xhr.onreadystatechange = OnReadyStateChange1;
    xhr.send(null);

イベントハンドラ OnReadyStateChange1 の実体は,今回は以下のように定義します(省略部分は以降で書き加えます).

function OnReadyStateChange1() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        var xml = xhr.responseXML;  // var txt = xhr.responseText;

       (省略; この部分は後述)
    }
}

これで変数 xml にリクエスト結果のXMLデータが代入されます.XMLでない,単なるベタのテキストとしてリクエスト結果を得るようなシステムの場合では,コメント部分で置き換えてください.なお if 文の意味は,「リクエストが成功裡に終了したタイミングで」という意味です*5
さてこうして得たXMLデータを使って元々のHTMLファイルに変更を加えます.ここからはいわゆる「ダイナミックHTML」の話です.ただし,せっかくなので格好つけてモダーンなDOM: Document Object Modelに沿った方法を使うことにします.DOMとは,XMLデータを処理するためのスタイルと言うか手法を定めた一般的なAPIです*6.ここでは必要最低限を説明します(詳細はググって規格書に当たって下さい).
まず返って来たXMLデータから,ルートである ksms 要素を選択します.これには getElementsByTagName を使います.このAPIは同名のタグの配列を返すため,ksms がルートと分かっている今回はその返り値の配列の先頭だけを選び出します.さらにその子要素の配列を,childNodes によって取得します.

(省略部の中身)

        var ksms = xml.getElementsByTagName('ksms')[0];
        var names = ksms.childNodes();

また表示用の領域として選んだ div 要素には,固有の id 属性を振っておいたのでした.id 属性から要素を選択するAPIgetElementById です(こちらの返り値は配列ではありません).

(省略部続き)

        var dd = document.getElementById('displayDiv');

ここまで来れば,後は「names の各要素の中身を dd の(HTMLとしての)子要素として追加する」だけです.ただ,これは直接HTMLを書くのに比べると少し面倒な作業です.

(さらに続き)

        for (var i=0; i < names.length; ++i) {
            var name = names[i], theName = name.nodeValue;

           (省略; この部分は後述)
        }

変数 name には(独自スキーマの)XMLの要素(と言うか部分木)が入っていますが,変数 theName の方にはその中身が文字列として入っています.HTMLには name などと言う要素はないので,中身の文字列をいったん取り出したという訳です.この文字列をどう使うか? ということになります.単に "<p>" + theName + "<p>" などとしてもよいのですが,やはりDOMを使ってみましょう.

(省略部の中身)

        var t = document.createTextNode(theName);
        var p = document.createElement("p");
        p.appendChild(t);
        dd.appendChild(p);

変数 theName に入っている文字列を p タグでくくってできた要素を,変数 dd の指す div 要素の子にしました((ここでオブジェクト document のメソッド createElement を使わないと「HTMLの」p ではない,別の名前空間p 要素になってしまうので注意.)).DOMでは不完全なツリー構造を持つHTMLを扱うために "text node" というちょっと微妙な概念を使っており,そこでメソッド createTextNode が使われているのですが,まぁ慣れて下さい.ここは for 文の内部なので,その操作を配列 names の要素毎に繰り返すことになります.これが実行されると,元々のHTMLは以下のような変更を受けることになります.

    <div id="displayDiv"></div>

    <div id="displayDiv">
        <p>はずむ</p>
        <p>とまり</p>
        <p>やす菜</p>
    </div>

これでブラウザでは以下のように表示されることになります.

これが画面遷移なしで行われるのです.立派なAJAXですね!
なお今回の実装では,"Do AJAX!" ボタンを2度押すと一覧が繰り返し表示されてしまうんですが,それはご愛嬌ということで... もちろんDOMには追加した要素を削除するAPIなどもあるので,他に様々な処理ができます.興味を持たれた方は色々調べてみて下さい.

言い訳

始めは部誌に寄稿する気などなかったのだけど,「〆切が製本関係のアクシデントで延びた」と聞いて書く気になって,しかも延びた〆切のさらに2日後に提出したものであって,決してやるべきことをほったらかして遊んでいる訳ではございません.気分転換てやつね.どうかご勘弁を.

*1:これには利点と欠点とがあります.ここではユーザビリティ論には踏み込みませんが,AJAXは適切な場所で使うようにしましょう.「金槌を持つと全てが釘に見える」ようなことにならないように...

*2:Factoryパターンと言ってもよいですが,まぁデザパタなんて飾りですよ.

*3:グローバルにするなら,本当はファイル先頭で宣言した方がよいです.以下で追加する関数のソースは,これより後に書き加えてください.

*4:なんかセキュリティの都合で同一ドメインでないといけない制限があるとか何とか? まぁ自分で遊びに使う分には気になりません.

*5:このイベントハンドラはリクエスト終了以外のタイミングでも呼び出されるので,ここで区別しています.

*6:JavaScriptの他,JavaPerl, Ruby, Python, C++等のいくつもの言語に対し,DOMを実装したライブラリが存在しています.DOMではXMLをツリーと考え,ツリーの探索や加工のためのAPIを定義しています.これに対しツリーの探索におけるイベントドリヴンなモデルに基づくSAXというものもあります.