uhyohyo.net

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

十七章第七回 HTML Imports

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

今回でWeb Components関連の話題は最後です。Web Componentsに関連する技術は大別して4つに分かれるというのが定説です。ひとつはCustom Elements、もうひとつはShadow DOM、3つ目はtemplate要素です。今回紹介するのが最後のひとつ、HTML Importsです。

importsと言われると、モジュールの話を思い出しますね。モジュールは他のJavaScriptファイルをimport文によって読み込むことができるという仕組みでした。今回のHTML Importsもそれと似ています。ただし、読み込むのはJavaScriptファイルではなくHTML文書です。

link要素を使って読み込む

HTMLをご存知のかたはlink要素を使ったことがあると思います。特に、スタイルシートを読み込むために使うことが多いのではないでしょうか。

今回は、他のHTML文書を読み込むためにやはりlink要素を使います。link要素のrel属性を"import"にすることにより、他のHTML文書を読み込むことを示すことができます。


<link rel="import" href="./some_doc.html">

そして、このように読み込んだHTML文書はJavaScriptから見ることができます。そのためには、link要素のノード(HTMLLinkElement)が持つプロパティimportを参照します。これは、読みこまれた文書を表すDocumentオブジェクトです。

では、ここまでをやってみましょう。読みこまれる文書として次のようなHTMLファイルを用意しました(17_7_sample_loaded.html)。


<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>HTML Importsで読みこまれるテスト</title>
  </head>
  <body>
    <p>これは読みこまれる側の文書です。</p>
  </body>
</html>

この文書を読み込む側のファイルは次の通りです(サンプル1)。


<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>HTML Importsのテスト</title>
    <link rel="import" href="./17_7_sample_loaded.html">
  </head>
  <body>
    <p>これは読みこむ側の文書です。</p>
    <script>
      // link要素を取得
      const link = document.querySelector('link');
      // link要素が読み込んだDocumentを取得
      const doc = link.import;
      // そのbody要素の中身を見る
      console.log(doc.body);
    </script>
  </body>
</html>

まず6行目で、link要素を用いてドキュメントを読み込んでいます。そのドキュメントをJavaScriptから取得しているのが14行目です。そして、コンソールにそのドキュメントのbody要素を表示しています。コンソールには次のようなことが表示されるはずです。


  <body>
    <p>これは読みこまれる側の文書です。</p>
  </body>

link要素によって読み込んだ文書の中身を取得できていることが分かりますね。

非同期的な読み込み

なお、このようなlink要素はscript要素などでJavaScriptファイルを読み込んだ場合と同様にパーサーをブロックします。すなわち、importされるページの読み込みが完了するまでこちらの読み込みが進まないということです。モジュールの回でも述べたように、これでは嬉しくない場合もありますね。そこで、読み込みがパーサーをブロックしないようにする方法もあります。そのためには、link要素にasync属性を付けます。async属性があると読み込みが非同期的になり、指定された文書を読み込みつつ自身の文書の読み込みが進められることになります。

ただし、ではその方がいいじゃないかと言って上のサンプルのlink要素にasync属性を付けても正しく動作しないはずです。それは、link要素にasync属性を付けた場合は、14行目で取得したlinkの読み込み先の文書の読み込みがまだ終了していないからです。例えばまだbody要素が読みこまれていないためdoc.bodyがnullになるなどの挙動を示します。

では、読み込みが完了してから取得するにはどうすればいいのでしょうか。その答えを皆さんは既にしっているはずです。DOMで非同期といえば、そう、イベントです。link要素では、読み込みが完了したことを示すloadイベントと、読み込みが失敗した(読み込み先のページが存在しないなど)ことを示すerrorイベントが発生します。とりあえずloadイベントを使うことでページの読み込みが完了してから処理を進めることができるでしょう。これをやってみたのが次のサンプルです(サンプル2)。


<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>HTML Importsのテスト2</title>
    <link rel="import" href="./17_7_sample_loaded.html" async onload="loaded(event)">
  </head>
  <body>
    <p>これは読みこむ側の文書です。</p>
    <script>
      // link要素によるロードが完了したら呼び出される関数
      function loaded(ev){
        // link要素を取得
        const link = ev.target;
        // link要素が読み込んだDocumentを取得
        const doc = link.import;
        // そのbody要素の中身を見る
        console.log(doc.body);
      }
    </script>
  </body>
</html>

今回は6行目のlink要素にasync属性とonload属性が付きました。今回はひと工夫して、loaded関数にイベントオブジェントを渡し、link要素はそれを用いて取得しています(14行目)。

これがHTML Importsの基本です。他の文書を読み込みたいときにこのようにlink要素を用いて簡単に読み込むことができるのです。

HTML Importsとスクリプト

ところで、link要素によって読みこまれた文書中にスクリプトがあった場合はどうなるでしょうか。実は、その場合そのスクリプトはちゃんと実行されます。サンプル3を見てください。ソースコードは省略するので、自分で見てみましょう。

ポイントは、読みこまれる側の文書内に書かれたscript要素が実行されているということです。それどころか、読みこまれる文書内で作られた関数fooを読み込む側の文書で使うことができています。これは言い方を変えると、読み込む側のスクリプトと読みこまれる文書内で実行されるスクリプトはグローバル変数などを含む実行環境を共有しているということです。読みこまれる側のスクリプトでfooというグローバル変数が読み込む側で使われているということからもこれが分かります。

まとめると、link要素で読みこまれた文書に書かれたスクリプトは、普通にscript要素で読みこまれたのと同様に、同じ実行環境で実行されます。ある意味script要素で外部JavaScriptを読み込む代わりになるわけですね。

また、もうひとつ驚くべきポイントがあります。読みこまれる側のスクリプトにおいてdocument.body.innerHTMLを表示していますが、ここで表示されるHTMLは読み込む側のものであるはずです。つまり、link要素によって読みこまれたHTML文書内のJavaScriptが実行される場合、document(あるいはwindow)などはその文書自身ではなく、読み込む側の文書を指しているのです。ただし、このことも前述の、実行環境を共有するという特徴から説明することができます。documentというのはグローバル変数として存在しているので、読み込む側も読みこまれる側も同じグローバル変数を共有するとなれば、たとえ読みこまれる側でもdocumentというグローバル変数が読み込む側の文書を指しているのも理解できます。

このことにより、link要素を用いて読みこまれた文書はもとの文書に影響を与えることができます。

ところで、読みこまれる側のスクリプトを実行中にdocumentを参照すると読み込む側の文書となるなら、読みこまれる側の文書を得るにはどうすればよいのでしょうか。

それには、document.currentScript.ownerDocumentとします。まず、document.currentScriptというのは今回初めて登場したもので、現在スクリプトが実行されているscript要素を指します。これはHTML Importsと関係なしに便利に使えるプロパティですね。今回このdocumentというのはやはり読み込む側のdocumentなのですが、現在実行されているのはその読み込む側の文書からlink要素によって読みこまれた文書内に存在するscript要素ということになります。

そして、ownerDocumentは以前紹介しましたね。そのノードが属するDocumentを示すプロパティです。

このようにして、読みこまれる側の文書のDocumentを取得することができます。なんだか回りくどいですが仕方ありません。一応これを試してみたのがサンプル4です。

Web Componentsにおける利用

今まで解説したことを利用すると、カスタム要素の定義を別の文書に独立させることができます。

まず、読みこまれる側の文書は次のようにします。


<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>カスタム要素の定義用文書</title>
  </head>
  <body>
    <template id="tmp">
      <style>
        div.wrapper {
          border: 10px dashed black;
          padding: 10px;
        }
      </style>
      <div class="wrapper">
        <slot></slot>
      </div>
    </template>
    <script>
      (()=>{
        // template要素を取得しておく
        const tmp = document.currentScript.ownerDocument.getElementById('tmp');

        class MyElement extends HTMLElement {
          constructor(){
            super();

            // template要素の中身をコピー
            const content = tmp.content.cloneNode(true);

            // ShadowRootを作って中身を追加
            this.attachShadow({
              mode: 'open',
            }).appendChild(content);
          }
        }

        customElements.define('my-element', MyElement);
      })();
    </script>
  </body>
</html>

読み込む側はこれを次のように読み込みます(サンプル5)。


<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>カスタム要素を読み込むテスト</title>
    <link rel="import" href="./17_7_sample_loaded4.html" async>
  </head>
  <body>
    <my-element>
      <p>カスタム要素を使いました。</p>
    </my-element>
  </body>
</html>

読み込む側が非常にすっきりしましたね。6行目でHTMLファイルをひとつ読み込むだけで、自動的にmy-elementというカスタム要素が利用可能になります。これは、読みこまれる文書がカスタム要素の登録をやってくれるからです。

読みこまれる側は今までどおりのコードですね。template要素とShadow DOMを利用しつつカスタム要素を登録しています。ただし、今回は全体が無名関数に囲まれています(20–39行目)。この目的は、変数tmpやMyElementクラスを関数の中のローカル変数としてしまうことで外から隠蔽することです。今回、script要素が実行された時点でtemplate要素を取得して変数tmpに入れてしまっています。これは、document.currentScriptを使って読みこまれる側の文書にアクセスできるのはこのscript要素が実行されたときだけだからです。変数を使うとなると、グローバル変数を作るのは好ましくありませんね。読み込む側の文書とか、あるいはさらに別の文書が読みこまれた場合に偶然同じtmpという変数を使っていたらエラーになってしまうからです。

この例にはHTML Importsを使う利点がよく現れています。この例では、カスタム要素を別のHTMLファイルにパッケージ化することに成功しました。カスタム要素を使いたい側は該当するHTMLファイルを読み込むだけでいいのです。

さらに、これはscript要素を使ってJavaScriptファイルを読み込むだけではできません。なぜなら、読みこまれる側でtemplate要素を使っているからです。template要素はHTMLの概念なので、JavaScriptファイルだけでなくHTML文書の形で読み込めるようにする必要があったのです。これを可能にしてくれるのがHTML Importsというわけですね。

特に、読みこまれる側の文書ではtemplate要素にtmpというid属性をつけている点に注目してください。id属性は文書の中で重複が発生してはいけませんが、HTML Importsで読み込むならばそれは別の文書なので、読み込む側とか他の文書との被りを気にせずに自由にid属性をつけることができるのです。このように、上で述べたようにグローバル変数さえ気をつければ、読み込む側の文書に余計な影響を与えない独立したパッケージを作ることができるようになっています。

逆に、グローバル変数に気をつけなければいけないのはHTML Importsの弱点と言えるかもしれません。モジュールの場合、モジュール間のやり取りはグローバル変数などではなくimport文とexport文により行い、それぞれは独立した実行環境を持っていました。そのため、各モジュールが同じ名前の変数を使っていても問題なく動作します。


以上がHTML Importsの解説です。HTML Importsは外部スクリプトだけでなく文書の形でまとめてパッケージを読み込むことができます。また、そのような利用法に限らず、単純に複数のページから使いたい構造をtemplate要素に入れておいたページを読み込むというような利用法も可能でしょう。

最初に述べたようにHTML Importsはまだブラウザベンダ間の合意が固まったとは言いがたい状況であり、まだ実際のウェブサイトで実用可能になるには時間がかかるかもしれません。特に、類似した機能を持つES Modulesが先に安定したためES Modulesのほうに注目が集まっているようです。

とはいえ、HTML Importsもまたこれからのウェブページを支える技術のひとつですので、覚えておいて損はないでしょう。