Ajax.Requestクラス(1)

【抜粋】一部省略
Ajax.Request = Class.create();
Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
  initialize: function(url, options) {
    this.transport = Ajax.getTransport();
    this.setOptions(options);
    this.request(url);
  },
(省略)
  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.RequestクラスはAjax.Baseクラスを継承しています。

Eventsプロパティ

【抜粋】
Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Eventsプロパティはprototypeプロパティにではなく、Ajax.Requestクラスに直に定義されています。よって、Ajax.Requestクラスのインスタンスでもthisによりアクセスができません。Ajax.Request.Eventsとしてアクセスする必要があります。リテラルなのでインスタンスごとに作成させたくなかったのでしょう。

initializeメソッド

【抜粋】
  initialize: function(url, options) {
    this.transport = Ajax.getTransport();
    this.setOptions(options);
    this.request(url);
  },

前述Ajax.getTransportメソッドにより、環境に適したXMLHttpRequestオブジェクトをthis.transportに格納します。前述Ajax.Baseクラスから継承したsetOptionsメソッドにより、optionsプロパティ(定義はAjax.Base)のオブジェクトに、引数optionsのプロパティを追加しています。その後、後述requestメソッドを引数urlを使用して呼び出しています。

requestメソッド

【抜粋】
  request: function(url) {
    var parameters = this.options.parameters || '';
    if (parameters.length > 0) parameters += '&_=';

    try {
      this.url = url;
      if (this.options.method == 'get' && parameters.length > 0)
        this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;

      Ajax.Responders.dispatch('onCreate', this, this.transport);

      this.transport.open(this.options.method, this.url,
        this.options.asynchronous);

      if (this.options.asynchronous) {
        this.transport.onreadystatechange = this.onStateChange.bind(this);
        setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
      }

      this.setRequestHeaders();

      var body = this.options.postBody ? this.options.postBody : parameters;
      this.transport.send(this.options.method == 'post' ? body : null);

    } catch (e) {
      this.dispatchException(e);
    }
  },

自オブジェクトthisのoptionsプロパティのオブジェクトが、parametersプロパティを持つ場合、これを取得し、末尾に「&_=」を付加します。未定義や空文字の場合は、パラメータは空文字とします。・・・なんで「_=」を付けているのか分からないのですが。分かったら追記します。

urlプロパティに引数urlを格納します。options.methodが'get'の場合(大文字は不可)、かつパラメータが空文字でない場合にurlプロパティの末尾にパラメータを付加します。すでにurlにパラメータがある場合でも対応できるように、「?」の有無を判定して付加方法を分岐させています。

その後、前述Ajax.Responders.dispatchメソッドにより、Ajax.Responders.respondersプロパティが持つすべてのオブジェクトのonCreateメソッドを実行しています。*1

transportプロパティはXMLHttpRequestオブジェクトです。options.methodプロパティ(get、post等)とurlプロパティとoptions.asynchronousプロパティ(true OR null:非同期 false:同期)を引数としてopenメソッドを呼び出します。

options.asynchronousプロパティがtrue:非同期の場合、onreadystatechangeにonStateChangeメソッドをbindメソッド*2を使用して登録します。次に、setTimeout関数を使用し、0.01秒後にrespondToReadyStateメソッドが1:Loadingを引数として実行されるようセットします。これが前述した、prototype.jsにおいて1:Loadingがインスタンス作成0.01秒後に発生する所以です。(本来の1:Loadingはブロックされています。後述。) イベントへの関数登録時にbindメソッドを使うのはprototype.jsでは常套手段です。

次にsetRequestHeadersメソッドを呼び出しています。

最後にsendします。options.methodが'post'の場合、options.postBodyか、前述でoptions.parametersから作成したパラメータ(postBodyがnullの場合に使用)を引数としてsendします。post時はpostBody、parametersどちらで設定してもよいということになります。'get'等であればnullを引数とします。*3

try catch節内でエラーが発生した場合はエラーオブジェクトを引数としてdispatchExceptionメソッドを呼び出します。

【参考】
Ajax.Requestでのフォームデータ送信サンプル(postBodyへの設定) - Backstage of theater.js

setRequestHeadersメソッド

【抜粋】
  setRequestHeaders: function() {
    var requestHeaders =
      ['X-Requested-With', 'XMLHttpRequest',
       'X-Prototype-Version', Prototype.Version];

    if (this.options.method == 'post') {
      requestHeaders.push('Content-type',
        'application/x-www-form-urlencoded');

      /* Force "Connection: close" for Mozilla browsers to work around
       * a bug where XMLHttpReqeuest sends an incorrect Content-length
       * header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType)
        requestHeaders.push('Connection', 'close');
    }

    if (this.options.requestHeaders)
      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);

    for (var i = 0; i < requestHeaders.length; i += 2)
      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
  },

配列requestHeadersにヘッダ名と値を設定しています。最初がヘッダ名、次がその値、その次に別のヘッダ名・・・という形になっています。ヘッダ名'X-Requested-With'と'X-Prototype-Version'はprototype.js独自の設定です(たぶん。参考URL:http://www.studyinghttp.net/cgi-bin/rfc.cgi?2616#Sec14)。サーバ側プログラムで使用します。

options.methodプロパティが'post'ならContent-typeヘッダ(既存)に'application/x-www-form-urlencoded'(すべてのFORM入力データがURLエンコードされたワンデータ、という意味。参考URL:http://jsgt.org/ajax/ref/test/enctype/test1.htm)を設定します。

次がMozillaブラウザのバグのために入れられているコードです。overrideMimeTypeメソッドはMozillaブラウザのXMLHttpReqeuestオブジェクトにしか存在しません。これがあるかどうかで判定しています。XMLHttpReqeuest送信時にContent-lengthが不正な値となるというバグです。(詳細:246651 - XMLHttpRequest doesn't calculate Content-Length correctly(英語)) ただ、Connectionヘッダを'close'にする(持続的な接続をせず、その都度接続する)ことで対応している理由はちょっと分からないです・・・。

次に、options.requestHeadersプロパティがあれば、requestHeadersに追加しています。applyは第二引数でthis.options.requestHeadersをそのまま使いたいために使用されています。

【参考】
var ary = ["zero", "one"];
ary.push("two", "three");
alert(ary);
//「zero,one,two,three」と表示される
var add = ["four", "five"];
ary.push.apply(ary, add);
alert(ary);
//「zero,one,two,three,four,five」と表示される

その後、requestHeadersのヘッダ名と値のすべてを、transportプロパティ(XMLHttpRequestオブジェクト)のsetRequestHeaderメソッドでヘッダに追加しています。

onStateChangeメソッド

【抜粋】
  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState != 1)
      this.respondToReadyState(this.transport.readyState);
  },

前述requestメソッドにてXMLHttpRequestのonreadystatechangeイベントに登録されているメソッドです。XMLHttpRequestのreadyStateプロパティが1以外の場合にrespondToReadyStateメソッドを呼び出しています。・・・ところで、なんで変数に入れているんでしょう? その後で引数にはreadyStateプロパティを使用してますし・・・。

heaaderメソッド

【抜粋】
  header: function(name) {
    try {
      return this.transport.getResponseHeader(name);
    } catch (e) {}
  },

XMLHttpRequestのgetResponseHeaderメソッドにより、受信したデータから、nameで指定したヘッダ名の値を取得します。エラー時は何もしません。

evalJSONメソッド

【抜粋】
  evalJSON: function() {
    try {
      return eval(this.header('X-JSON'));
    } catch (e) {}
  },

受信データのX-JSONヘッダの値をevalで処理して返却します。エラー時は何もしません。外部から呼び出されることはなく、Ajax.Respondersや、インスタンスのoptionsプロパティに登録されるonComlete等のメソッドの、第二引数に結果が自動的に格納されます。

前述したようにこのメソッドは素直に動いてくれない気がします。

【例】
<html>
<head>
<title></title>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script language="javascript">
function test(){
  new Ajax.Request(
    'test.jsp',
    { onComplete:function(req, json){
        $('test').innerHTML = Object.inspect($H(json)).escapeHTML();
      }
    });
}
</script>
</head>
<body>
<div id="test"></div>
<button onclick="test();">TEST</button>
</body>
</html>
[test.jsp]
<%@ page contentType="text/plain;charset=utf-8" %>
<%@ page import="java.util.*, java.io.*" %>
<%
//キャッシュ無効化
Calendar today = new GregorianCalendar();
response.setDateHeader("Last-Modified", today.getTime().getTime());
response.setDateHeader("Expires", 0);
response.setHeader("Pragma","no-cache");
response.setHeader("Cache-Control","no-cache");

//X-JSONヘッダ指定
response.setHeader("X-JSON", "{'one':'1', 'two':'2', 'three':'3'}");
%>
【上記の実行結果】
#<Hash:{}>

正しく取得できません。調べてみると、evalでエラーとなっていました。どうやら、evalの引数文字列の先頭が「{」だとエラーとなるようです(空白やコメントの後で「{」もダメ)。最後の行の"{'one':'1', 'two':'2', 'three':'3'}"を"({'one':'1', 'two':'2', 'three':'3'})"とすると以下になります。

【括弧を前後に付けた場合の実行結果】
#<Hash:{'one': '1', 'two': '2', 'three': '3'}>

これはメソッドを修正してほしい気がします。

【改修案】
  evalJSON: function() {
    try {
      return eval("(" + this.header('X-JSON') + ");");
    } catch (e) {}
  },

ところで、ヘッダで2バイト文字を扱う方法が分かりません><; どうやっても文字化けしてしまうのですけど・・・。

evalResponseメソッド

【抜粋】
  evalResponse: function() {
    try {
      return eval(this.transport.responseText);
    } catch (e) {
      this.dispatchException(e);
    }
  },

XMLHttpRequestのresponseTextプロパティの内容をeval関数で処理して返却します。エラー時はdispatchExceptionメソッドをエラーオブジェクトを引数にして呼び出します。このメソッドは、後述respondToReadyStateメソッド内で、Content-typeヘッダの値が「text/javascript」だった場合にのみ実行されます。値を返却していますが、respondToReadyStateメソッド内では使用されていません。

【例】
<html>
<head>
<title></title>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script language="javascript">
function test(){
  new Ajax.Request('test.jsp');
}
</script>
</head>
<body>
<div id="test"></div>
<button onclick="test();">TEST</button>
</body>
</html>
[test.jsp]
<%@ page contentType="text/javascript;charset=utf-8" %>
<%@ page import="java.util.*, java.io.*" %>
<%
//キャッシュ無効化
Calendar today = new GregorianCalendar();
response.setDateHeader("Last-Modified", today.getTime().getTime());
response.setDateHeader("Expires", 0);
response.setHeader("Pragma","no-cache");
response.setHeader("Cache-Control","no-cache");
%>
$("test").innerHTML = "サーバ側で作成したコードをevalで実行";

実行結果は省略します^^;
<<Ajax.Requestクラス(2)に続く>>

*1:この中にはデフォルトで設定されているAjax.activeRequestCountのインクリメント処理が含まれます(587行目)。

*2:前述Functionクラスへの追加メソッド

*3:ブラウザがKonquerorの場合は、sendの引数がnullだとエラーとなる場合があるようです。その場合はnullを空文字("")に変更すると良いようです。