Agusa Lab. > Webware Project > GrapeWeb
 

Ajax - Asynchronous JavaScript + XML

概要

AjaxとはJavaScriptの持つ非同期通信を中心として、 JavaScript, DHTML, XMLなどの既存技術を組み合わせる手法を指します。 本チュートリアルでは、Ajaxの利用場面、基本構造を説明した後、 サンプルアプリケーションを紹介します。

Ajaxとは

Ajaxという名前は2005年2月に登場したGoogle Mapsがきっかけで誕生しました。 しかしそれ以前にも、 Ajaxと同等の手法を利用していたアプリケーションは存在しました。重要なのは、 その手法を"Ajax"と名付けられた事実で、 それにより世間の注目を集めるようになりました。

どのようなことができるか?

「Ajaxって何?」という疑問を持つ人には、説明より先にまず体感してもらう のが良いでしょう。 有名ものに、Google Maps , Google サジェスト があります。また、既存のサービスを利用している 画面遷移なしのAmazon検索 も良い例です。
このようにJavaScriptの持つ 非同期通信, DHTML を利用して、これまでよりも ユーザビリティの高いユーザインターフェースをもつWebサイトを 作成する事ができます。

非同期通信とは?

Ajaxの肝となるのが、XMLHttpRequestを使ったJavaScriptによる非同期通信です。 XMLHttpRequestはJavaScriptの組み込みオブジェクトで、XMLRequestを使えば スクリプト中でHTTP通信を行なう事ができます。ここでの「非同期」の意味は、 「ブラウザの制御を奪う事なしにHTTP通信できる」、という意味です。つまり、 XMLRequestによるHTTP通信を行なっている間も、ユーザはブラウザ上で他の作業を 行なう事ができます。Google Mapsではドラッグされた時、Googleサジェストでは テキストボックスに文字がタイプされた時などに非同期通信を行なっています。
XMLHttpRequestにより通信するには、 JavaScriptソースコード内で例えば以下のようにします。

// XMLHttpRequestオブジェクトを取得(:の後ろはIEの為のもの)
var xmlhttp = this.XMLHttpRequest ?
  new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
// 指定したURLに接続
// 3つ目の引数をtrueとすることで非同期モードで通信します
xmlhttp.open("GET", "/hoge/hoge.cgi", true);
// HTTPリクエストを送信
xmlhttp.send(null);
// 通信結果のXMLオブジェクトを取得
var response = xmlhttp.responseXML;
        
Ajaxの構成

DHTMLとは?

DHTMLとは、JavaScript, CSS, DOM の3つの技術を組み合わせて、ブラウザに表示 されている表示を動的に書き換えるという技術です。JavaScriptはDOMによりHTML, XMLを操作する為の各種APIが用意されています。Ajaxでブラウザの表示を更新する際、 JavaScriptによりCSSの値を変更してるか、DOMを操作しているかになります。
DHTMLによる表示の更新は例えば以下のようにします。ボタンを押すことでIDが textである要素の文字列が置き換わります。

<html>
<head>
<script type="text/javascript">
function push() {
  document.getElementById("text").innerHTML = "Pushed!!";
}
</script>
</head>
<body>

<div id="text">hoge</div> 
<button onClick="push();">push</button>

</body>
</html>
        

Ajaxの問題点

これまで述べてきたXMLHttpRequestによるHTTP通信には大きな欠点が存在していて、 ブラウザのセキュリティによる制限から、 異なるドメイン間では通信する事ができません。 正確には、XMLHttpRequestのopenメソッドでURLを指定する引数に http:// から始まる URLを指定できません。他のドメインと通信を行なうには、通信を行なう為のAPIを持った CGIを用意する必要があります。
JavaScriptとCSSが本質的にもつ問題でクロスブラウザ問題があります。 Ajaxを使ったアプリケーションを開発する際は、複数のブラウザでテストを行なう 必要があります。
また、画面遷移がなくURLが変化しないため、 ブラウザの表示をブックマークで保存することができません。

サンプルアプリケーション

Ajaxを使ったチャットシステムのサンプルアプリケーションを紹介します。 同期通信を用いてHTMLでのチャットをリロードすることなく実現します。

AjaxChatスクリーンショット

構成

構成するファイルを以下に示します。

ajaxchat.html
メインのHTMLファイルです。Ajaxを用いたJavaScriptコードを含みます。
write.cgi
Ajaxから会話ログファイルに書き込む為のAPIを提供します。
log.txt
会話ログファイルです。
AjaxChat構成

ソースコード

ajaxchat.htmlのソースコードを以下に示します。 ajaxchat.htmlは以下の機能を持ちます。

  • 会話入力フォームの表示
  • 会話ログの表示
  • フォームデータをPOSTメソッドでwrite.cgiに送信
  • 周期的にlog.txtの差分を取得
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift-JIS">
<title>Ajax Chat</title>
<link rel="stylesheet" href="chat.css" type="text/css" />
</head>

<body onload="document.f.n.focus()">
<h3>Ajax Chat</h3>
<div id="log">
<div id="d"></div>
</div>
<span class="status" id="status"></span>
<table class="input"><tr><td><center>
<form name="f" action="../cgi-bin/write.cgi" method="POST" onsubmit="post(this);return false">
  Name:<input class="inp" type="text" name="n">
  Comment:<input class="inp" type="text" name="c">
<input class="btn" type="submit" value="Send">
</form>
</center></td></tr></table>
</body>

<script>
// ログファイルサイズ
var size = 0;
// HTML内の会話を挿入する箇所
var insert = getById("d");
// 更新周期
var refresh = 1000;
//
var read = 1000;
// 表示行数
var line_num = 20;
// 開始
head();

function createXMLHttp(){
    var xmlhttp;
    try {
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    } catch (e) {
        xmlhttp = new XMLHttpRequest()
    }
    return xmlhttp;
}
// idからエレメントを取得
function getById(id){
    return document.getElementById(id)
}
// タイマー
function timer(){
    setTimeout("reload()", refresh);
}
// 表示
function write(text){
    var lines = text.split("\n");
    for (var i = 0; i < lines.length; i++) {
        if(!lines[i]){continue}
        var tmp = lines[i].split("<>");
        var name = document.createElement("div");
        name.setAttribute("class", "name");
        name.innerHTML = tmp[0];
        var comment = document.createElement("div");
        comment.setAttribute("class", "comment");
        comment.innerHTML = tmp[1];
        getById("log").insertBefore(name, insert);
        getById("log").insertBefore(comment, insert);
    }
    // line_numを超過した分 行を削除
    var line_now_length = getById("log").childNodes.length;
    if (getById("log").childNodes.length > line_num) {
        for (var i = 0; i < line_now_length - line_num; i++) {
            getById("log").removeChild(getById("log").firstChild);
        }
    }
}
// 初回サイズを調べる
function head(){
    var xmlhttp = createXMLHttp();
    xmlhttp.onreadystatechange = function(){
        if(xmlhttp.readyState == 4){
            var len = xmlhttp.getResponseHeader("Content-Length");
            var last = xmlhttp.getResponseHeader("Last-Modified");
            size = len-0;
            modify = last;
            get();
        }
    };
    xmlhttp.open("HEAD", "log.txt", true);
    xmlhttp.send(null);
}
// 初回取得
function get(){
    var xmlhttp = createXMLHttp();
    xmlhttp.onreadystatechange = function(){
        if(xmlhttp.readyState == 4){
            var len = xmlhttp.getResponseHeader("Content-Length");
            var text = xmlhttp.responseText;
//            write(text); *ログイン(?)時に会話ログを表示する場合はコメントを外す
            timer();
        }
    };
    xmlhttp.open("GET", "log.txt", true);
    if(size >= read){
        xmlhttp.setRequestHeader("Range", ["bytes=",size-read,"-"].join(""));
    }
    xmlhttp.send(null);
}
// 差分取得
function reload(opt){
    getById("status").innerHTML = "<img src=\"image/progress.gif\" />";
    var xmlhttp = createXMLHttp();
    xmlhttp.onreadystatechange = function(){
        if(xmlhttp.readyState == 4){
            var len = xmlhttp.getResponseHeader("Content-Length");
            var ran = xmlhttp.getResponseHeader("Content-Range");
            if (ran) {size = (ran.match(/\/(\d+)$/))[1]}
            getById("status").innerHTML = "";
            var text = xmlhttp.responseText;
            if (text) {
                write(text);
            }
            if(opt != "notimer"){timer()}
        }
    };
    xmlhttp.open("GET", "log.txt", true);
    xmlhttp.setRequestHeader("Range", ["bytes=",size-1,"-"].join(""));
    xmlhttp.send(null);
}
// 送信
function post(form){
    if(form.c.value == ""){return}
    var d = "n="+encodeURIComponent(form.n.value)+"&c="+encodeURIComponent(form.c.value);
    form.c.value = "";
    form.c.focus();
    var xmlhttp = createXMLHttp();
    xmlhttp.onreadystatechange = function(){
        if(xmlhttp.readyState == 4){
            reload("notimer");
        }
    };
    xmlhttp.open("POST", "../cgi-bin/write.cgi", true);
    xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xmlhttp.send(d);
}
</script>
        

write.cgiのソースを以下に示します。 このCGIはPOSTで受け取ったデータを、log.txtに書き込みます。

#!/usr/bin/perl

# read FORM DATA
read(STDIN, $post, $ENV{'CONTENT_LENGTH'});
foreach (split(/&/, $post)) {
    ($name, $value) = split(/=/);
    $value =~ tr/+/ /;
    $value =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C", hex($1))/eg;
    $value =~ s/&/&amp;/g;
    $value =~ s/</&lt;/g;
    $value =~ s/>/&gt;/g;
    $value =~ s/″/&quot;/g;
    $FORM{$name} = $value;
}

# insert FILE
if ($FORM{'c'} ne "") {
    $msg = "$FORM{'n'}<>$FORM{'c'}\n";
    open(OUT, ">> ../ajax/log.txt") || ErrorExit("can't open Log File");
    print OUT $msg || ErrorExit("can't write Log File");
    close(OUT) || ErrorExit("can't close Log File");
}

print "Content-type: text/plain\n\n";
	

おわりに

Ajaxは技術的には目新しいものではないですが、その応用はまだ未知なる可能性を 秘めており、この先Ajaxを用いた画期的なサービスが登場すると思います。 Ajaxは身近な既存の技術の組み合わせなので敷居が低く、 アイデア次第で全く新しいアプリケーションを実現できるので、 皆さんも興味を持ったら是非挑戦してみて下さい。

参考

WEB+DB PRESS Vol.27 P.111-142

by Kyohei Ando