連載
» 2014年07月18日 12時40分 公開

第6回 ASP.NETによるWebアプリ開発(アプリケーションの開発 3)連載:ASP.NETによる軽量業務アプリ開発(2/3 ページ)

[arton, ]

メンバー情報処理用のJavaScript

 以下に示すリスト(members.jsファイル)は、前回示したmembers.aspxファイルを呼び出したり、[login]ボタンやメンバー名のclickイベントを処理したりするJavaScriptコードだ。

(function () {                                                (1)
  var login = document.getElementById('login');
  var member_name = document.getElementById('member_name');
  var changeUI = function(f) {                                (2)
    var uielems = [['labelMember', 'inline', 'none'],
                   ['labelPsw', 'inline', 'none'],
                   ['labelName', 'none', 'inline'],
                   ['commands', 'none', 'block'],
                   ['rosters', 'none', 'block']];
    for (var i = 0; i < uielems.length; i++) {
      var elem = document.getElementById(uielems[i][0]);
      elem.style.display = f ? uielems[i][1] : uielems[i][2];
    }
    login.value = f ? 'login' : 'logout';
  };
  var nullToEmpty = function(val) {
    return (val == null) ? '' : val;
  };
  login.addEventListener('click', function() {               (3)
    if (login.value == 'login') {
      var member = document.getElementById('member');
      var psw = document.getElementById('psw');
      if (member.value != '') {
        var xtr = new XMLHttpRequest();                      (4)
        xtr.onload = function() {                            (5)
          var result = JSON.parse(xtr.responseText);         (6)
          if (result.id != null) {
            member_name.innerHTML = Util.htmlEscape(result.name);  (7)
            member_name.href = "members.aspx?q=edit";
            changeUI(false);
            document.getElementById('update_rosters').click();
          } else {
            psw.value = '';
          }
        };
        Util.postData(xtr, 'members.aspx'
          'id=' + encodeURIComponent(member.value) + '&psw=' + encodeURIComponent(psw.value)); (8)
      }
    } else { // logout
      var xtr = new XMLHttpRequest();
      Util.postData(xtr, 'members.aspx', 'logout=true');     (9)
      changeUI(true);
      document.getElementById('rosters').innerHTML = '';
    }
  });
  member_name.addEventListener('click', function(e) {
    var xtr = Util.createXtr(function() {
      var member = JSON.parse(xtr.responseText);
      if (member.name == null) {
        changeUI(true);
        document.getElementById('rosters').innerHTML = '';
      }
      Dialog.show('<div>' + member.name + '</div>'                 (10)
        + '<form method="post" id="member_form">'
        + '<label>郵便番号<input type="text" name="zip" placeholder="郵便番号" size="8"/></label><br/>'
        + '<label>住所<input type="text" name="address" placeholder="住所" size="64"/></label><br/>'
        + '<label>パスワード更新<input type="checkbox" name="update_psw" value="true"/></label>'
        + '<input type="text" name="psw" placeholder="password"/></form>',
        {
          load: function(popup) {
            var form = document.getElementById('member_form');
            var elems = ['zip', 'address'];
            for (var i = 0; i < elems.length; i++) {
              form.elements[elems[i]].value = nullToEmpty(member[elems[i]]);
            }
          },
          unload: function(popup) {
            var form = document.getElementById('member_form');
            var xtr = new XMLHttpRequest();
            var data = 'zip=' + encodeURIComponent(form.elements['zip'].value)
                   + '&address=' + encodeURIComponent(form.elements['address'].value)
                   + '&save=true';
            if (form.elements['update_psw'].checked) {
               data += '&psw=' + encodeURIComponent(form.elements['psw'].value)
            }
            Util.postData(xtr, member_name.href, data);
          }
        }
      );
    });
    xtr.open('GET', member_name.href, true);
    xtr.send(null);
    e.preventDefault();
  });
  var xtr = Util.createXtr(function() {
    var recs = JSON.parse(xtr.responseText);
    var opt = [];
    for (var i = 0; i < recs.length; i++) {
      opt[i] = '<option value="' + recs[i].id + '">' 
           + Util.htmlEscape(recs[i].name) + '</option>';
    }
    document.getElementById('member').innerHTML = opt.join('');
  });
  xtr.open('GET', 'members.aspx', true);
  xtr.send(null);
})();

members.jsファイル
  (1) 1回しか呼び出さない関数は無名関数として定義し、その場で実行する。members.jsファイルはロード時に必要なイベントハンドラーなどを全てDOMへ組み込むため、これに該当する。
  (2) 関数内で複数回呼び出される関数は、関数をローカル変数(ここではchangeUI変数)に設定して利用する。
  (3) イベントハンドラーは直接HTMLに属性として記述するのではなく、要素オブジェクトの「addEventListener(イベント名, ハンドラー);」を利用する。
  (4) XMLHttpRequestオブジェクトは直接new演算子を使って生成する。
  (5) レスポンスハンドラーはXMLHttpRequestオブジェクトのonloadプロパティに設定する。
  (6) ここで参照している「xtr」は、(4)の行で宣言した変数「xtr」だという点に着目する。JavaScriptの関数は「クロージャ」(関数閉包)として実現されているため、その関数が定義されたスコープ上で参照できる変数を利用できる。「JSON.parse」はJSON文字列からオブジェクトを生成するためのJSON型のメソッド。ここで示したように、ASPXがJSONを返すと、JavaScript側ではこの1行だけでオブジェクトとしてアクセスできるようになる。
  (7) 前回のレスポンスデータ選択の長短所で示したように、サーバー側はJSONの文字列にプレーンなテキストを設定すべきである。その結果としてクライアント側でHTMLエスケープが必要となる。ここで呼び出しているUtilオブジェクトはutility.jsファイルで定義したオブジェクトである(後述)。
  (8) ここではPOSTするフォームデータを自力で「id=value&psw=value」という形式で作成している。このため、「value」に該当する箇所ではJavaScript組み込みのencodeURIComponent関数を使ってエンコードを行っている。
  (9) (8)と異なり、ここではフォームデータの「value」部がエンコード不要な文字列(true)なのでencodeURIComponent関数を呼び出していない。
  (10) Dialogはdialog.jsファイルで定義したオブジェクト。Dialog.showメソッドは、ダイアログの内容のHTMLと、ロード/アンロード時に実行する関数を定義したオブジェクトの2つの引数を取る。第2引数の「{ load: function(popup) {...}, unlaod: function(popup) { ... } }」が、「はじめに」で示した「オブジェクトに定義した関数」の例である。

 ここで示したmembers.jsファイルは、ロードされると同時に、定義した無名関数がそのまま実行される。

 実行した結果、いくつかのローカル関数を定義し、DOMのイベントハンドラーに関数を追加する。これらはローカル関数ではあるが、HTMLと同じ寿命を持つDOMにイベントハンドラーとして組み込まれるので、HTMLと同じ寿命を持つこととなる。

 このような方法を利用することで、グローバルなネームスペースを汚染せずに、HTMLの表示期間中持続する関数やオブジェクトを定義する。

[コラム]JavaScriptのバグパターン

 以下は筆者自身が経験したJavaScriptのバグパターンであって、他の開発者が必ずしも同じパターンにはまり込むことはないかもしれないが、参考のために示す。

  • var付け忘れによるグローバル変数の破壊
     − 変数の初出時に「var」を使った宣言を忘れると、それはそのままグローバル変数(正確にはwindowオブジェクトのプロパティ)となる。1つのHTMLファイルにロードされる複数のJavaScriptファイルに、varの付け忘れによる名前の衝突があると、全く理解不能なバグとなるため初出時には必ず「var」を付けて自衛する必要がある。
  • 型の自動変換
     − 型の自動変換は非常に便利であるが、言語仕様をきちんと把握していないとバグの元となる。

var result = document.getElementById('number_data').value * 32; // 文字列の数字データと32の乗算
// 上から文字列が数値の場合、数値との演算が行われると想定すると+は異なる
result = document.getElementById('number_data').value + 32; // 文字列と文字列化された'32'の連結となる
if ('0' == some_variable) { // some_variableが文字列の'0'でなくともtrueとなることがある。例えば数値の0やfalseである*1

型の自動変換の例

*1 文字列と文字列の比較を行う場合は、===を利用する。'0' == false → true、'0' === false → false。


  • thisが分からなくなる
     − イベントハンドラーにおいて顕著だが、クラスベースのオブジェクト指向言語の考え方からは想定外のthisが与えられる(多くの場合、windowオブジェクトがthisである)。オブジェクトのプロパティやメソッドには明示的に変数を経由してアクセスする方がよい。筆者はこれもあって、JavaScriptでクラスもどきを実現する仕組みの利用をやめた。クラスのインスタンスという意識が働くと、どうしても暗黙のうちにthisがそのインスタンスそのものをポイントしているように感じてしまうためである。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。