uhyohyo.net

JavaScript初級者から中級者になろう

七章第一回 複数のドキュメントを扱う

この第七章では、章立てするほど大規模ではないが役立つさまざまな機能について解説します。

まず第一回では、「複数のドキュメントを扱う」ということを解説していきます。

複数のドキュメントを扱うとは、こういう感じのことです。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p>
      <iframe src="test.html"></iframe>
    </p>
  </body>
</html>

これは、インラインフレームを使用した文書です。インラインフレームとは、そこに別の文書を表示するしくみです。この文書では、その文書の中に、test.htmlという別のページを表示しているのがわかります。

このページ全体もひとつの文書ですが、表示されるtest.htmlもまたひとつの文書です。そこで、JavaScriptではそれを扱う方法もあります。

documentの取得

JavaScriptで文書を操作するのは、documentを使うのでした。iframeで読み込んだ文書も一つの文書だから、その文書に対応するdocumentがあります。それを取得する方法を説明します。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p>
      <iframe src="test.html" id="iframe"></iframe>
    </p>

    <script type="text/javascript">
    var iframe = document.getElementById('iframe');

    var doc = iframe.contentDocument;
    </script>
  </body>
</html>

このサンプルは、iframe要素のHTMLElement(HTMLIFrameElement)のプロパティcontentDocumentをdocに代入しています。このcontentDocumentがiframe要素で読み込まれた文書のdocumentです。

もっと具体的な操作をしてみましょう。そのために、読み込むtest.htmlは、

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p>aaaaaaa</p>
    <p>bbbbbbb</p>
    <p>ccccccc</p>
  </body>
</html>

ということにしておきます。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p>
      <iframe src="test.html" id="iframe"></iframe>
    </p>

    <script type="text/javascript">
      var iframe = document.getElementById('iframe');

      var doc = iframe.contentDocument;

      console.log(doc.getElementsByTagName('p').item(0).firstChild.nodeValue);
    </script>
  </body>
</html>

console.logの文が追加されました。docには、読み込まれたほうの文書のdocumentが入っています。そのgetElementsByTagName('p')だから、読み込まれるほうの文書からpが探されます。その0番目、つまり最初だから、<p>aaaaaaa</p>になっています。そのfirstChildは唯一の子ノードであるテキストノードです。nodeValueはその値だから、"aaaaaaa"がログされます。

ちなみに、これでよそのサイトをiframeで表示してJavaScriptでカスタマイズして表示・・・なんてことを考えるかもしれませんが、それはもともとできません

試してみましょう。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p>
      <iframe src="http://google.com/" id="iframe"></iframe>
    </p>

    <script type="text/javascript">
      var iframe = document.getElementById('iframe');

      var doc = iframe.contentDocument;
      console.log(doc.getElementsByTagName('input').length);
      console.log(doc.getElementsByTagName('img').length);
    </script>
  </body>
</html>

iframeで表示しているのはGoogleです。getElementsByTagNameでinput要素とimg要素の一覧をそれぞれ取得し、lengthを表示してみています。

ブラウザによって結果は違うかもしれませんが、自分がOperaで試した場合、inputのほうは0に(明らかにinput要素はあるのに)、imgに至ってはエラーになりました。

これは、セキュリティ的に危険なことをさせないためです。これが可能であると、例えば見えないiframeで会員制サイトのログイン画面を表示してみましょう。便利なように、ユーザーIDや、最悪パスワードもクッキーで表示されるかもしれません。そうなれば、関係のないサイトからJavaScriptでそれらの情報を得ることができ、しかもそれを別の場所に送信したりすることもたやすいことです。

このように、ユーザーIDなどの大事な情報を、関係のない人が盗み出すことも可能になるのです。このようなことを防ぐために、JavaScriptでは自分のサイトのdocumentしか(正しくは、同じオリジンのサイトのdocumentしか)自由に扱えません。

documentとノードの関係

さて、基本的に、ノードはdocumentに属しています。例えば、ある文書のノードは、全てその文書のdocumentに属しています。また、上のiframeの例だと、iframeがある側のページのノードはdocumentに、iframeで読み込まれる側のページのノードはiframe.contentDocumentに属しているということになります。

ノード側から、そのノードがどのdocumentに属しているかを知ることができます。

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p>test</p>

    <script type="text/javascript">
      console.log(document.body.ownerDocument == document);
    </script>
  </body>
</html>

これはtrueが表示されることから、その文書のノード(今回はbody要素)が持つownerDocumentというプロパティが、そのノードが属するdocumentを表しているということがわかります。

さて、これがどんなことに役立つかですが、実は、何かのノードを使ってある文書(document)を操作する際、そのノードがそのdocumentに属していないといけないのです。逆に言うと、documentを操作するとき、違う文書に属するノードは使えないということです。例えば、

<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p>
      <iframe src="test.html" id="iframe"></iframe>
    </p>

    <script type="text/javascript">
      var iframe = document.getElementById('iframe');
      var doc = iframe.contentDocument;

      var p = doc.getElementsByTagName('p').item(0);
      document.body.appendChild(p);
    </script>
  </body>
</html>

このサンプルは、変数docに代入するところまでは同じです。その後、読み込まれた側の0番目のP、つまりさっきと同じ<p>aaaaaaa</p>を取得し、それをdocument(読み込む側)に追加しようとしています。

しかし、上記のように、このpは違う文書に属しているためにうまくいきません。

そこで、このようなことを行う方法はちゃんとあります。

var iframe = document.getElementById('iframe');
var doc = iframe.contentDocument;
var p = doc.getElementsByTagName('p').item(0);
document.body.appendChild( document.importNode(p, true) );
            

このようにします。今度はちゃんと正しく動作します。ここで、importNodeという新しいメソッドが出てきました。これが何かをしたのです。

これはdocumentが持つメソッドです。このメソッドは、他の文書に属するノードを自分に属するノードにするというものです。

つまり、このimportNodeによって、他の文書に属していたPがこのdocumentに属するようになり、その結果appendChildが成功したのです。

ただし、実は、このメソッドはもとのノードに影響を与えません。今回の場合、このp要素は読み込む側に属したから読み込まれる側から消えてしまったかというと、そうではありません。ちゃんと残っています。実は、正確にはこのメソッドは「他の文書に属するノードを見て、自分に属するそれと同じノードを作成する」というものなのです。つまり動作としては、cloneNode(二章第七回)と少し似ています。できたノードは戻り値で返されます。

それでは引数をみてみます。2つありますね。ひとつめの引数がもとのノードです。今回の場合、docに属するpですね。2つめの引数は、その要素の子以下もまとめて処理するかどうかです。これもcloneNodeと同じで、これがfalseだと<p></p>だけのノードができます。

文書に依存しない処理をつくる

次に、文書に依存しない処理をつくることについて考えてみます。次のような関数を考えます。

function wrapDiv(node){
    var wrapdiv = document.createElement('div');
    wrapdiv.appendChild(node);
    if(node.parentNode){
        //親がある場合
        node.parentNode.replaceChild(wrapdiv, node);
    }
    return wrapdiv;
}

これは、あるノードを引数とって、それをDIV要素で囲むという関数です。

BODY
  |
  └―― P

のPが引数に渡されたら、

BODY
  |
  └―― DIV
    |
    └―― P
          

というようになります。

しかし、これにはひとつ問題があります。それは何だか分かりますか。

この関数では、包むdiv要素を作るときにdocumentが使われています。

何がまずいかというと、引数のノードがそのdocumentに所属しているとは限らないのです。上のiframeの例で、フレーム内の文書のあるノードを読み込む側が持つ関数wrapDivで加工したいという場合があるかもしれません。

しかし、その場合でも、document.createElementを使うとdocumentに所属する要素ができるので、もとのノードはifame内の文書に所属しているのに、それを包むdivはべつの文書に所属しているということが起きます。これだとうまくいきません。

どうすればいいかというと、囲まれるノードと同じ文書に所属させればいいのです。それはつまり、そのノードが所属するdocumentのcreateElementを呼び出せばいいから、こうなります。

function wrapDiv(node){
    var wrapdiv = node.ownerDocument.createElement('div');
    wrapdiv.appendChild(node);
    if(node.parentNode){
        //親がある場合
        node.parentNode.replaceChild(wrapdiv, node);
    }
    return wrapdiv;
}

これで、他の文書のノードが引数に来ても正しく処理できます。つまり、この関数は文書に依存しなくなったのです。どの文書のノードも処理できます。

こうすると、汎用性が高くなりいいです。こういう関数は、普段から意識して作るといいでしょう。