uhyohyo.net

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

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

このページの最終更新日:

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

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

次の例を見てください。


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

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

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

なお、今回のようにJavaScriptから他のページをいじる場合は同一生成元ポリシー(SOP)の制限を受けるため、注意する必要があります。これは、簡単に言うと他のウェブサイトの内容を読むことができないということです。ウェブサイトが同じかどうかというのはオリジン(生成元)が同じかどうかで判断されます。例えばこのサイトのオリジンはhttp://uhyohyo.netです。オリジンというのは、プロトコル(http:)とホストuhyohyo.net、さらにポート番号をあわせた概念です。オリジンが同じでないと読めないということは、例えばこのサイト上のページのJavaScriptは他のオリジン(例えばhttps://google.co.jp)上のページは読めません。

また、最近はセキュリティ上の制限は厳しいため、ローカルのファイル上で実行されるJavaScriptは他のファイルを読み込むことができないようになっています。したがって、今回紹介するサンプルもローカルのファイルに書いて実行しただけだと動作しません。これに対処するには、ブラウザの設定でローカルファイルに対するSOPを無効にする方法と、仕方ないのでウェブサーバー上に置いてアクセスする方法があります。自分のPC上でウェブサーバーを動かすのもいいですね。詳しい方法は調べてみてください。

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');

    window.addEventListener('load', function(){
      var doc = iframe.contentDocument;
      console.log(doc);
    });
    </script>
  </body>
</html>

このサンプルでは以前紹介したaddEventListenerを使用しています。loadというイベントは三章第一回で紹介しました。ページの読み込みが完了したときに発生するイベントです。以前はbody要素で発生するイベントとして紹介しましたが、実はこのようにwindowでも発生します。loadイベントをJavaScriptから登録するときはwindowに登録するのがいいでしょう。

今回loadイベントを待つ理由は、iframe要素がtest.htmlを読み込むのを待つためです。

このサンプルは、loadイベントが発生したら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">
      window.addEventListener('load', function(){
        var iframe = document.getElementById('iframe');

        var doc = iframe.contentDocument;

        console.log(doc.getElementsByTagName('p')[0]);
      });
    </script>
  </body>
</html>

doc.getElementsByTagNameを呼び出すことで、読み込まれたほうの文書(test.html)からpが探されます。その0番目、つまり最初だから、<p>aaaaaaa</p>と表示されます。

以前、documentはwindowと対になっていると説明しました。iframe要素にはcontentDocumentに対応するWindowを取得するためのcontentWindowプロパティもあります。

documentとノードの関係

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

ノード側から、そのノードがどのdocumentに属しているかを知ることができます。それには、ノードが持つownerDocumentプロパティを用います。


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

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

しかし、ノードがどのdocumentに属しているかということを気にする場面は少ないかもしれません。次の例を見てください。


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

    <script type="text/javascript">
      window.addEventListener('load', function(){
        var iframe = document.getElementById('iframe');
        var doc = iframe.contentDocument;

        var p = doc.getElementsByTagName('p')[0];
        console.log(p.ownerDocument == document);
        document.body.appendChild(p);
        console.log(p.ownerDocument == document);
      });
    </script>
  </body>
</html>

このサンプルは、iframeで読みこまれた文書の中から<p>aaaaaaa</p>を取り出し、それをdocument(読み込んだ側の文書)に追加しています。

結果として、iframeの中からは<p>aaaaaaa</p>が消え、外に出現したはずです。iframeの中から消えた理由は、ノードは1箇所にしか存在できないからです。既に木構造中に存在するノードが新しい場所にappendChildされた場合はこのように古い場所から移動してきます。

興味深いのは2回のconsole.logの結果で、1回目はfalseで2回目はtrueとなっているはずです。つまり、appendChildでdocument下の木構造に追加されたことによって、このp要素が属するDocumentが変更されたということです。

この挙動は、実は昔のDOM (DOM Level 3)とは異なっています。昔は異なるDocumentに属するノードをappendChildすることはできませんでした。代わりに、他の文書に属するノードはまずdocumentのimportNodeメソッドによって自分のdocumentに属するコピーを作成し、それをappendChildする必要がありました。

importNodeは次のように使います。


    window.addEventListener('load', function(){
      var iframe = document.getElementById('iframe');
      var doc = iframe.contentDocument;
      var p = doc.getElementsByTagName('p')[0];
      var p2 = document.importNode(p, true);
      console.log(p2.ownerDocument === document); // true
      document.body.appendChild(p2);
    });
  

第1引数はコピーしたいノードです。第2引数は真偽値で、これはcloneNodeの引数と同じです。つまり、子ノードたちもコピーするかどうかを表します。