CodeZine(コードジン)

特集ページ一覧

XMLデータアイランドをすべてのブラウザで動作させる方法

データアイランドとわずかな汎用コードを利用したクロスブラウザ実装

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/09/08 12:00

XMLデータアイランドとXMLデータソースは特に新しい考え方ではなく、もはやInternet Explorerに固有の考え方でもありません。本稿では、特定ベンダの実装にこだわらずにデータアイランドを汎用的に使用して、あらゆるブラウザで動作するデータ本位のWebページを作成する方法を説明します。

はじめに

 現在、Web標準をしっかりとサポートすることは当たり前になっています。Microsoft中心の環境を前提にしてアプリケーション開発を進める時代なら、おそらく標準にあまり気を使う必要はなかったでしょう。しかし、Microsoft以外の環境をも目指す現代の職業的Web技術者にとって、最新の標準(CSS、JavaScript、MIMEタイプ)を重視し、それをマスターすることは、最低限必要なスキルです。標準を重視するクライアントがしだいに増えており、標準に準拠しないアプリケーションはしだいに受け入れられなくなっています。ただ、標準が厳しく求められる一方で、標準外でありながら、依然重宝がられている数々のWebブラウザ機能があるのも事実です。XMLデータアイランド(HTMLコンテンツに埋め込まれたXMLコンテンツ)は、そのほんの一例にすぎません。本稿では、標準技術を磨きながら、そうした便利な機能も活用する方法を考えます。

データバインディングとは

 データソースデータバインディングの考え方は、大昔から存在します。何らかの静的コンテンツ(Webページでも、Javaウィンドウでも、4GLアプリケーション画面でもかまいません)を用意し、それをどこか別の場所に格納されているデータと結び付けるプロセスを「バインディング」と呼びます。その狙いは、不変のコンテンツを、時間とともに変化するデータで補完もしくは補強することですが、注意すべきは、そこで使う方法に一般性が求められるという点です。つまり、事前にデータが手許にあることを前提にしてはならず、ユーザからコンテンツ要求があるたびにコンテンツが自分自身を更新し、最新のデータ群を表示するようにしなければなりません。ODBC、JDBC、XULテンプレート、XBLは、いずれもバインディングシステムです。Microsoftのデータバインディングサービスもその一例です。バインディングでは、不完全なコンテンツを先に渡しておき、その後でデータを補完します。それに対して、PHPのようなテンプレート式コンテンツ生成システムでは、コンテンツとデータを混在させ、完成した最終的コンテンツを一気に引き渡します。バインディングシステムでは、普通、必要なデータが基本コンテンツとは別の場所に置かれていて、記述的アドレスによって参照されます。このアドレスは「データソース」と呼ばれます。

 XMLデータアイランドは、上記の古臭い物語に新しい展開をもたらす考え方であり、コンテンツが引き渡されるときに、そのコンテンツにデータを付随させるという手法をとります。コンテンツとデータが同じ場所にあって、その2つを結び付ければバインディングは終わりです。少数の断片的情報を共有するだけで、つまりタグ(エレメント)とタグ属性をHTMLページ内に置くだけで、すべてのバインディングロジックを指定でき、プログラミングは不要です。次に示すのは、IEがあれほど奇妙かつ特異な構造でさえなければIEで無事に動くはずのXMLデータアイランドの例です。

<html>
  <head>
    <script>
      function where() {
        var node = document.getElementById("who");
        node = node.getElementsByTagName("location");
        alert(node[0].firstChild.nodeValue);
      }
    </script>
  </head>
  <body>
    <xml id="who">
      <contributor>
        <name>Nigel McFarlane</name>
        <location>Somewhere in Cyberspace</location>
      </contributor>
    </xml>
    This article contributed by:
    <div datasrc="#who"
         onclick="where()" datafld="name">Nobody</div>
  </body>
</html>

 このコードでは、データアイランドが<xml>タグで囲まれています。この例は、標準から外れたタグスープHTMLドキュメントであることに注意してください。このデータアイランドはHTMLコンテンツとクリックハンドラで一度ずつ使われています。HTMLコンテンツでは、プロプライエタリのdatasrc属性とdatafld属性によって<div>タグのコンテンツを<name>タグのコンテンツにバインディングしています。クリックハンドラでは、<location>タグのコンテンツをただのデータとして扱い、それをドキュメントのDOMから取り出して表示しています。<div>タグの初期コンテンツは「Nobody」ですが、ページがデータソースをロードすると、それが「Nigel McFarlane」で置き換えられます。

 Internet Explorerは、Microsoftの独自コードでバインディングを実行します(これをIEの一部と見るかWindowsの一部と見るかは人によって異なります)。このやり方は、コンテンツとデータソースが同じブラウザページにあるという簡単なXMLデータアイランドのケースにとっては明らかに大げさすぎます。おかげで移植不能であることはもちろん、不必要に複雑になっています。その代わりに、JavaScriptを使用して同じ機能を実現できるので、そのソリューションをすぐにお目にかけましょう。最近広く使われるようになったXMLHttpRequestオブジェクトを追加すれば、JavaScriptでリモートデータソースのバインディングもできますが、それはまた別の機会に譲ります。

 さきほど、上記コードはIEで動くはずと言いましたが、実は動きません。データをタグと結び付ける最初のバインディングはスムーズに実行されますが、スクリプトを使ってデータにアクセスする2番目のバインディングは失敗します。これは、IEがアイランドに対して何か標準外の奇妙なことをするからです。本稿の後の方で、この制約を迂回する方法も説明します。

標準に合わせる

 標準に合わせようと思うなら、昔風のタグスープHTMLでは通りません。標準を強く意識しているページにデータアイランドを埋め込むには、次の制約を守る必要があります。

  • 適切なHTML標準DOCTYPE(HTML 4.01 Strictなど)を使用します。
  • HTML標準では、HTMLドキュメントに新しいエレメント(タグ)を追加することを禁止していません。したがって、<xml>タグ(または同様のタグ)やその子を追加することは差し支えありません。しかし、そうしたエレメントをドキュメントのDTDに追加することは標準で禁じられており、匿名性の維持が必要です。たとえば、<location>の代わりに<address>を使用することは、<address>が既にHTMLで意味を与えられている以上、できません。
  • IEでやるように、データアイランドに<?xml?>処理命令を埋め込んではなりません。何よりも、HTMLはXMLではありませんし、そのような処理命令はユーザエージェントへのヒントにすぎず、クライアントブラウザに無視されるかもしれません。
  • <xml>タグはドキュメントについてのタグではないので、ドキュメントの<head>セクションに置いてはなりません。
  • IEのinnerHTML手法を使用してはなりません。便利ですが、標準外です。
  • 標準に準拠しているWebブラウザは、XMLデータアイランドのコンテンツをただのテキストとして表示します。つまり、「Nigel McFarlane Somewhere in Cyberspace」となります。これは望ましくないので、CSSの「display: none」スタイルルールを用いて、XMLコンテンツが表示されないようにします。
  • 最後に、ここで紹介するソリューションは、JavaScriptとCSSサポートを前提としています。このサポートが得られないときは、最初からデータソースを使用しないフォールバック手法を選んでください。行儀の良いフォールバックについての説明は別の機会に譲りますが、さして困難ではありません。

 では、データアイランドの実際の使用例を考えてみましょう。以降では、データドリブンのレポート表示と、データドリブンのフォーム記入について見ていきます。

テストデータを準備する

 簡単なHTMLページで実験してみましょう。次のHTMLページは、1つのデータソースと2つのバインディングロケーションを含んでいます。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
  "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <style>
      #who { display : none; }
    </style>
    <script src="islands.js"></script>
  </head>
  <body>

    <p id="who">
      <contributor>
        <name>Nigel McFarlane</name>
        <location>Somewhere in Cyberspace</location>
      </contributor>
      <contributor>
        <name>A. Russell Jones</name>
        <location>DevX Editorial</location>
      </contributor>
      <contributor>
        <name>J. Random Hacker</name>
        <location>Everywhere</location>
      </contributor>
    </p>
 
    <h3>Reviewers of this article <h3>

    <ul datasource="who">
      <li><span datafield="name"></span>
            , Location:
          <span datafield="location"></span>
      </li>
    </ul>

    <table border="1" datasource="who">
      <tr>
        <td><input type="text" datafield="name">
        </td>
        <td><input type="text" datafield="location">
        </td>
      </tr>
    </table>

  </body>
</html>

 ドキュメントヘッドには、アイランドをユーザから隠すためのスタイルと、すべての処理を行う埋め込みスクリプトが含まれています。ドキュメントの本体には、3つの重要なコード片が含まれています。まず、<p>タグで示されるデータアイランドです。このデータアイランドには3つの<contributor>レコードが含まれ、各レコードにはnamelocationという子フィールドがあります。上の例で、データアイランドの定義に<xml>タグでなく<p>タグを用いているのは、IEのDOMサポートがお粗末なためです。IEでは、<xml>タグ(というより、HTMLタグ以外のタグ)に含まれている内容がDOMに反映されません。つまり、このようなデータにはスクリプトでアクセスできないということですが、心配は無用です。代わりに<p>タグ(または<span><div>)を使用すればいいのです。

 あと2つのコード片は末尾にあります。datasource属性を含んでいるタグが2つあるので、前述のデータアイランドコンテンツがメインページコンテンツの2か所に組み込まれることになります。datafield属性は、組み込むべきデータを指定します。datasourcedatafieldはどちらも私が勝手に作ったカスタム属性で、HTML仕様にもMicrosoftのデータアイランド仕様にもありませんが、HTML 4.01 Strictには従っています。

 こんなわずかなマークアップでも、Microsoftによるソリューションの限界を超えています。ここでは、配列不定のリストと、テーブルに埋め込まれたエレメントにデータを組み込んでいますが、どちらもMicrosoftソリューションでは実現困難または実現不可能です。

 このHTMLコンテンツは、それ自体ではほとんど何もしません。データ操作のためのスクリプトを追加しなければなりません。

スクリプトを計画する

 データバインディングを実装するには、DHTMLの手法を採用し、ドキュメントの内容を操作するスクリプトを使用するとよいでしょう。ただ、スパゲッティのように複雑怪奇なDHTMLの時代は去り、いまは確固たる標準の時代です。DHTMLコードも、すっきりわかりやすいものでなければなりません。最低限、すべてのコードがJavaScriptオブジェクトとしてカプセル化され、他のコードからきれいに分離されていることが必要です。次に示すのはデータバインディングオブジェクトのスケルトンです。「islands.js」ファイルの中身は、ほぼこれですべてです。

var islands = {
  isMSIE : ...,
  getElementsByAttribute : 
     function (node, att) { ...},
  getEBArecursive : 
    function (list, node, att) { ...},
  getFieldDataFromRecord : 
    function (rec,field) { ... },
  makeIEtree : function (island) { ... },
  merge : function (target, template, record) { ... },
  bind : function () { ... }
};

 これは、7つのプロパティから成るリテラルオブジェクトです(Perlのハッシュに似ています)。具体的には、isMSIEという1つの単純値と、6個の匿名関数が含まれています。それぞれの関数は1個のプロパティに割り当てられていて、各プロパティが1つのオブジェクトメソッドとして動作します。このように定義された匿名関数は、定義場所であるオブジェクトの中にしか現れないので、そのオブジェクトをどのWebページに組み込んでも、他者の関数名と衝突する心配がなく、便利に使用できます。ただし、アイランドの変数名だけは一意でなければなりません。

 省略記号(...)の部分をコードで置き換えれば、このオブジェクトは完成します。その前に、各プロパティについて少し説明しておきましょう。

  • isMSIE
    単なるブラウザ検査フラグです。
  • getElementsByAttribute()
    特定の属性を持つノードをドキュメントから探し出します。標準のDOMにはそのような関数がないので、新しく作成しました。
  • getEBArecursive()
    getElementsByAttribute()の再帰バージョンです。コード量を減らすのに役立ちます。
  • getFieldDataFromRecord()
    データアイランドからdatafield項目を取り出します。
  • makeIEtree()
    IEの動作の不具合を修正します。詳しくは後述します。
  • merge()
    ページコンテンツ、特殊テンプレート、データアイランドのデータから最終コンテンツを作成します。
  • bind()
    最初に呼ばれる関数で、ここからすべてが始まります。

 さて、バインディングを発生させなければなりません。それには、「islands.js」ファイルの末尾にonloadイベントハンドラを追加します。

window.onload = function () { islands.bind(); };

 この行には、スコープチェーンというJavaScriptの便利な機能が使われています。次の構文の方が簡単ですが、こちらの構文は使わないのが無難でしょう。

window.onload = islands.bind();

 この簡単な構文を使う場合、bind()メソッドが実行されるときの現ウィンドウはwindowオブジェクトとなります。一方、匿名関数の内部で、bind()メソッドをそれ自身のオブジェクトから呼ぶと、アイランドのオブジェクトが「スコープ内」に置かれます。これなら、特殊なthisプロパティを用いることでislandsオブジェクト内の他メソッドを簡単に参照できますし、そのオブジェクトメソッドをislands変数への参照で汚染することもありません。

オブジェクトメソッドを計画する

 バインディングの実装方法は次のとおりです。まず、ブラウザ検査を片づけておきましょう。

isMSIE:(window.navigator.userAgent.search('MSIE')!=-1)

 bind()メソッドがすべての力仕事を担います。計画を立てやすいよう、このメソッドで行う作業のリストを示しておきます。

  1. すべてのデータソースターゲットを探します。各ターゲットについて次の処理を行います。
  2. そのターゲットの既存のコンテンツを別テンプレートに取り出します。
  3. それがIEなら、対応するデータアイランドを修正します。
  4. 対応するデータアイランド中のレコードを探します。各レコードについて次の処理を行います。
  5. そのレコードとテンプレートをマージします。
  6. マージしたテンプレートをページ中にコピーします。

 必要とされるメソッドのいくつかはきわめて一般的な性格のもので、Webを探してみればあちこちで見つかります。以下は実装方法の一例です。まず、getElementsByAttribute()関数から見ていきましょう。これは再帰バージョンを包むラッパーにすぎません。

getElementsByAttribute : function (node, att) {
  var rv = [];
  this.getEBArecursive(rv, node, att);
  return rv;
},

 DOMノードと属性名が与えられると、そのノードの下(内部)にあるすべての該当DOMノードを配列として返します。それを行うのが次の関数です。

getEBArecursive : function (list, node, att) {
  for (var i=node.childNodes.length-1; i>=0; i--)
  {
    var child = node.childNodes.item(i);
    if ( child.nodeType == 1 ) {
      if ( child.getAttribute(att) ) {
        list.push(child);
      }
      this.getEBArecursive(list, child, att);
    }
  }
},

 この再帰バージョンは、見つかったノードのリストと現ノードを自分自身に渡します。そして、エレメントノード(タグ)の子ごとに属性の有無を調べ、見つかればそのノードを記録します。見つからなければ、その子に関して再度自分自身を呼びます。こうして、探す対象が何もなくなると、forループは何も行わず、リストの完成とともに再帰が終了します。

 もう1つ、やはり一般的な性格を持つ関数にgetFieldDataFromRecord()があります。これはノードとタグ名が与えられると、そのノードのサブツリーを降りていって、与えられた名前を持つタグのコンテンツを取り出してきます。

getFieldDataFromRecord : function (rec,field) {
  var found;
  for (var i=0; i < rec.childNodes.length; i++) {
    if (rec.childNodes.item(i).nodeName.
      toLowerCase()==field) {
      found = rec.childNodes.item(i).firstChild;
    if (found == null )
      return "";
    else
      return found.nodeValue;
    }
  }
},

 この関数は、レコードが3層の階層に配列されていることを前提しています。3層とは、「レコードのタグ」「データ項目のタグ」「データ項目の開始タグと終了タグの間に置かれているデータ」です。

 以上の準備を終えると、いよいよ核心部分です。

バインディングを実装する

 少し先取りをして、最終的なバインディングロジックをお見せします。これがbind()関数です。

 コメントに付いている番号は、前述の計画リストの番号に対応しています。
 
bind : function () {
  // 1. Find all the datasources in the page.
  var targets = 
    this.getElementsByAttribute(
    document,'datasource');
  if (!targets || targets.length == 0) return;

  // Do it for each data binding 'target' in 
  // the page 
  for (var i=0; i < targets.length; i++) {
    var iid = targets[i].getAttribute('datasource');
    var island = document.getElementById(iid);
    if (!island) return;

    // 2. Extract a copy of the current content 
    // for this target
    var template = targets[i].cloneNode(true);

    // ... and delete the real copy
    for (var j = targets[i].childNodes.length-1; 
       j>=0; j--) {
       targets[i].removeChild(
          targets[i].childNodes.item(j));
    }

    if ( this.isMSIE ) { island = 
       this.makeIEtree(island); }

    // 4. Apply the template once for each 
    // XML record
    for (j = 0; j < island.childNodes.length; j++)
    {
      // children that are text nodes aren't 
      // real records
      var record = island.childNodes.item(j);
      if ( record.nodeName == '#text' )
        continue;

      // 5, 6. Combine page, template and data
      this.merge(targets[i], template, record);
    }
      
    if ( this.isMSIE ) { delete island; }
  }
}

 データソースごとに外部ループがあり、レコードごとに内部ループがあります。datasourceタグが見つかったら、その中身を入れ替えます。具体的な手順としては、ページから当該コンテンツを取り出し、処理してから戻します。最終コンテンツは、テンプレートで指し示されたDOMサブツリーとアイランドを組み合わせたものとなります。データソースごとに複数のレコードがありえるので、その場合は、数回の戻しが必要となります。

 ここで問題となるのが、標準の観点からは眉をひそめたくなるIEの動作です。データアイランドを取り出すとき、IEはノードを返してきますが、ここで返されるのは開始タグ、テキスト、終了タグを平坦に並べただけのリストであり、ノードの下のサブツリーはたどれません。そこで、makeIEtree()関数を使い、あるべきDOMツリーをそのリストから作成します。ステップ4~6の前後で、DOMツリーを作成、破棄していることに注目してください。makeIEtree()関数のコードは次のとおりです。

makeIEtree : function (island) {
  var subtree = 
    document.createElement(island.nodeName);
  var current = subtree;
  var next;

  for (var j = 0; j < island.childNodes.length; j++)
  {
    var record = island.childNodes.item(j);
    if ( record.nodeName == '#text' )
    {
      current.appendChild(
        document.createTextNode(record.nodeValue));
    }
    else if ( record.nodeName.charAt(0) == '/' )
    {
      current = current.parentNode;
    }
    else
    {
      next = document.createElement(
        record.nodeName);
      current.appendChild(next);
      current = next;
    }
  }
  return subtree;
},

 この関数は、IEが返してくる平坦リストを単純なループで処理します。subtree変数が、作成されるDOMツリーの最上部です。currentは、そのサブツリーにおいて現在作成中の部分を指し示し、nextはノード作成に一時的に使用される変数です。コードは新しいタグを見つけるたびにノードを1つ作成し、currentがそのノードを下っていきます。終了タグを見つけると、currentは1ノード引き返します(上へ戻ります)が、それ以外は、見つかったものを現ノードの子ノード群に追加していきます。

 これでIEがらみの問題は解決されました。残るは、見つかった情報の処理です。この処理はmerge()関数で行います。

merge : function (target, template, record) {
  // dig out the fields requiring update
  var fields =
  this.getElementsByAttribute(template,'datafield');

  if (!fields || fields.length == 0) next;

  // update text for target fields in the template
  for (var k=fields.length-1; k>=0; k--) {
    var thetag   = fields[k];
    var thefield = thetag.getAttribute('datafield');
    var newtext  = 
      this.getFieldDataFromRecord(record,thefield);

    if (thetag.firstChild)  // replace existing text
    {
      thetag.firstChild.nodeValue = newtext;
    }
    else if ( thetag.value == null ) // not form tag
    {
      thetag.appendChild(
        document.createTextNode(newtext));
    }
    else                           // a form element
    {
      thetag.value = newtext;
    }
  }

  // put the updated content back into the page.
  for (k=0; k < template.childNodes.length; k++) {
    target.appendChild(
      template.childNodes.item(k).cloneNode(true));
  }
},

 merge()関数は、まず、ターゲット中からdatafield属性を持つすべてのタグを見つけ出し、それの配列を作ります。次に、タグごとに、与えられたレコードの中から対応するコンテンツを見つけ出します。見つかったコンテンツをテンプレート(基本ページコンテンツ)に入れるとき、3つのケースが考えられます。すなわち、コンテンツタグに既にテキストが含まれている場合(入れ替え)、含まれていない場合(追加)、そしてコンテンツタグがフォームエレメントである場合です。3つ目のケースでは、データをvalueプロパティに入れなければなりません。最後に、更新されたテンプレートの中身をそっくりページに戻します。これが終われば、あとはページをロードするだけで、すべてが実行されます。ダウンロードサンプル内のページをお気に入りのブラウザで試してみてください。

 このDHTMLコードは、従来の心配事の多くを排除しているという点で、ずっとプロ的な仕事と言えます。ページにイベントハンドラを埋め込むこともありません。これでブラウザごとのテストの必要性が減少し、やむを得ない場合にだけテストを行えば済みます。得られる結果も単なる遊びや新しがりでなく、本格的で意義ある成果です。最近、AJAXを始めとするDHTML関連技術の影響が爆発的に広まっています。今後は、Web技術者の実際の仕事でも、クライアントからの要求でも、その比重が高まっていくことになるでしょう。

 データアイランドは、わずかな汎用コードを使うだけでクロスブラウザ実装を可能にします。本稿では、データアイランドに基づくコンテンツの表示方法を考え、そのコンテンツをフォーム、テーブル、または非テーブル要素(リストなど)に統合する方法について説明しました。少し努力すれば、洗練されたデータ管理システムに発展させることもできるでしょうし、実際、その方面でいくつかの実験的な仕事がなされてもいます。IE 4.0やNetscape 4.xといった古びたブラウザが姿を消し、MozillaとFirefoxが台頭してきた現在、DHTMLは以前にも増して強力で、汎用性の高い技術に育ちつつあります。IEに拘束されてきたアプリケーションをDHTMLで解き放ってください。

編集者から一言
 DevXへの長年の寄稿者であり友人でもあったNigel McFarlane氏が、2005年6月、永眠されました。Web上にあふれている多くの嘆きの言葉に添えて、DevX編集スタッフからも一言哀悼の意を捧げます。ここに掲げた記事は、氏がわれわれのために生前に書き残した2編の原稿のうちの1編で、家族の同意を得て公開させていただいています。Nigel McFarlane氏は2冊の単行本の著者であり、DevXのほか、Mozillaとオープンソース運動にも多くの寄稿をされました。――Lori Piquet
 


  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

あなたにオススメ

著者プロフィール

  • japan.internet.com(ジャパンインターネットコム)

    japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.com や EarthWeb.c...

  • Nigel McFarlane(Nigel McFarlane)

    故人。フリーランスの科学技術ライターであり、MozillaおよびFirefoxの技術に関するオピニオンリーダー。著書に『Firefox Hacks』(OReilly Media社)と『Rapid Application Development with Mozilla』(Prentice Hall...

バックナンバー

連載:japan.internet.com翻訳記事

もっと読む

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5