HTML インポート

ウェブ用に含める

インポートする理由

ウェブ上でさまざまな種類のリソースを読み込む方法について考えてみましょう。JS の場合は <script src> です。CSS の場合は、<link rel="stylesheet"> が適しています。画像の場合は <img> です。動画に <video> があります。音声、<audio>… 要点を簡潔に説明しましょう。ウェブのコンテンツのほとんどは、単純で宣言的な方法で読み込むことができます。HTML の場合、そうではありません。次のいずれかを選択できます。

  1. <iframe> - 実績は十分だが重い。iframe のコンテンツは、ページとは完全に別のコンテキストに存在します。これはほとんどが優れた機能ですが、さらなる課題が発生します(フレームのサイズをコンテンツに合わせるのは大変で、スクリプトに取り込んだり取り出したりするのは非常に難しく、スタイリングはほぼ不可能です)。
  2. AJAX - xhr.responseType="document" は便利ですが、HTML を読み込むのに JS が必要だと言いますか?正常ではないようですね。
  3. CrazyHacksTM - 文字列に埋め込まれ、コメントとして非表示になります(例: <script type="text/html">)。うれしいです。

皮肉を感じますでしょうか?ウェブの最も基本的なコンテンツである HTML は、扱うのに最も手間がかかります。幸い、ウェブ コンポーネントがその解決策として登場しました。

スタートガイド

HTML インポートは、ウェブ コンポーネント キャストの一部であり、HTML ドキュメントを他の HTML ドキュメントに含める方法です。マークアップに限定されません。インポートには、CSS、JavaScript、.html ファイルに含めることができるその他のものも含めることができます。つまり、インポートは関連する HTML / CSS / JS を読み込むための優れたツールになります。

基本情報

<link rel="import"> を宣言して、ページにインポートを追加します。

<head>
    <link rel="import" href="/path/to/imports/stuff.html">
</head>

インポートの URL はインポート場所と呼ばれます。別のドメインからコンテンツを読み込むには、インポート ロケーションで CORS を有効にする必要があります。

<!-- Resources on other origins must be CORS-enabled. -->
<link rel="import" href="http://example.com/elements.html">

機能の検出とサポート

サポートを検出するには、<link> 要素に .import が存在するかどうかを確認します。

function supportsImports() {
    return 'import' in document.createElement('link');
}

if (supportsImports()) {
    // Good to go!
} else {
    // Use other libraries/require systems to load files.
}

ブラウザのサポートはまだ初期段階です。Chrome 31 が最初に実装されましたが、他のブラウザ ベンダーは ES モジュールの動作を確認してから実装する予定です。ただし、他のブラウザでは、広くサポートされるまで webcomponents.js polyfill が適切に機能します。

リソースのバンドル

インポートは、HTML/CSS/JS(他の HTML インポートを含む)を単一の成果物にバンドルするための規則です。これは組み込みの機能ですが、非常に強力な機能です。テーマやライブラリを作成する場合は、またはアプリを論理的なチャンクに分割する場合は、ユーザーに 1 つの URL を提供することをおすすめします。インポートを利用してアプリ全体を配信することも可能です。想像してください

実際の例としては、Bootstrap があります。Bootstrap は個別のファイル(bootstrap.css、bootstrap.js、フォント)で構成され、プラグインには JQuery が必要です。また、マークアップの例も用意されています。デベロッパーは、アラカルトの柔軟性を好みます。これによって、ユーザーが使用したいフレームワークの部分について承認を得ることができます。とはいえ、一般的な JoeDeveloper™ は、簡単な方法で Bootstrap をすべてダウンロードするでしょう。

Bootstrap などでは、インポートは非常に理にかなっています。これが、Bootstrap の読み込みの未来です。

<head>
    <link rel="import" href="bootstrap.html">
</head>

ユーザーは HTML インポート リンクを読み込むだけです。ファイルの散在を気にする必要はありません。代わりに、Bootstrap 全体が管理され、インポート bootstrap.html にラップされます。

<link rel="stylesheet" href="bootstrap.css">
<link rel="stylesheet" href="fonts.css">
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
<script src="bootstrap-tooltip.js"></script>
<script src="bootstrap-dropdown.js"></script>
...

<!-- scaffolding markup -->
<template>
    ...
</template>

しばらく待ちます。エキサイティングです

読み込み/エラー イベント

<link> 要素は、インポートが正常に読み込まれたときに load イベントを発生させ、インポートが失敗した場合(リソース 404 など)に onerror イベントを発生させます。

インポートはすぐに読み込まれようとします。onload/onerror 属性を使用すると、この問題を簡単に回避できます。

<script>
    function handleLoad(e) {
    console.log('Loaded import: ' + e.target.href);
    }
    function handleError(e) {
    console.log('Error loading import: ' + e.target.href);
    }
</script>

<link rel="import" href="file.html"
        onload="handleLoad(event)" onerror="handleError(event)">

インポートを動的に作成する場合は、次のようにします。

var link = document.createElement('link');
link.rel = 'import';
// link.setAttribute('async', ''); // make it async!
link.href = 'file.html';
link.onload = function(e) {...};
link.onerror = function(e) {...};
document.head.appendChild(link);

コンテンツの使用

ページにインポートを含めるということは、「そのファイルのコンテンツをここに配置する」という意味ではありません。これは「パーサー、このドキュメントを取得して使えるようにする」という意味です。実際にコンテンツを使用するには、アクションを起こしてスクリプトを記述する必要があります。

インポートは単なるドキュメントであることを認識することが、重要な aha! の瞬間です。実際、インポートのコンテンツはインポート ドキュメントと呼ばれます。標準の DOM API を使用してインポートの内部を操作できます。

link.import

インポートのコンテンツにアクセスするには、リンク要素の .import プロパティを使用します。

var content = document.querySelector('link[rel="import"]').import;

次の条件で、link.importnull です。

  • ブラウザが HTML インポートをサポートしていない。
  • <link>rel="import" がない。
  • <link> は DOM に追加されていません。
  • <link> が DOM から削除されました。
  • リソースが CORS 対応ではありません。

完全な例

warnings.html に次の内容が含まれているとします。

<div class="warning">
    <style>
    h3 {
        color: red !important;
    }
    </style>
    <h3>Warning!
    <p>This page is under construction
</div>

<div class="outdated">
    <h3>Heads up!
    <p>This content may be out of date
</div>

インポータは、このドキュメントの特定の部分を取得して、自分のページにクローンを作成できます。

<head>
    <link rel="import" href="warnings.html">
</head>
<body>
    ...
    <script>
    var link = document.querySelector('link[rel="import"]');
    var content = link.import;

    // Grab DOM from warning.html's document.
    var el = content.querySelector('.warning');

    document.body.appendChild(el.cloneNode(true));
    </script>
</body>

インポートでのスクリプトの作成

インポートがメイン ドキュメントに含まれていません。衛星通信だよ。ただし、メインのドキュメントが優先される場合でも、インポートはメインページに対して実行できます。インポート先の DOM、またはインポート元のページの DOM にアクセスできます。

- スタイルシートのいずれかをメインページに追加する import.html

<link rel="stylesheet" href="http://www.example.com/styles.css">
<link rel="stylesheet" href="http://www.example.com/styles2.css">

<style>
/* Note: <style> in an import apply to the main
    document by default. That is, style tags don't need to be
    explicitly added to the main document. */
#somecontainer {
color: blue;
}
</style>
...

<script>
// importDoc references this import's document
var importDoc = document.currentScript.ownerDocument;

// mainDoc references the main document (the page that's importing us)
var mainDoc = document;

// Grab the first stylesheet from this import, clone it,
// and append it to the importing document.
    var styles = importDoc.querySelector('link[rel="stylesheet"]');
    mainDoc.head.appendChild(styles.cloneNode(true));
</script>

何が行われているか見てみましょう。インポート内のスクリプトは、インポートされたドキュメント(document.currentScript.ownerDocument)を参照し、そのドキュメントの一部をインポート元のページ(mainDoc.head.appendChild(...))に追加します。

インポート内の JavaScript のルール:

  • インポート内のスクリプトは、インポート元の document を含むウィンドウのコンテキストで実行されます。window.document はメインページのドキュメントを参照します。これには 2 つの便利な結果があります。
    • インポートで定義された関数は window に配置されます。
    • インポートの <script> ブロックをメインページに追加するなど、難しい操作を行う必要はありません。ここでもスクリプトが実行されます。
  • インポートしてもメインページの解析はブロックされません。ただし、内部のスクリプトは順番に処理されます。つまり、スクリプトの適切な順序を維持しながら、遅延のような動作を実現できます。詳しくは以下をご覧ください。

ウェブ コンポーネントの配信

HTML インポートの設計は、再利用可能なコンテンツをウェブに読み込むのに適しています。特に、Web Components を配布するのに理想的な方法です。基本的な HTML <template> から、Shadow DOM を使用した本格的な カスタム要素まで、すべてが対象です [123]。これらのテクノロジーを併用すると、インポートはウェブ コンポーネントの #include になります。

テンプレートの追加

HTML テンプレート要素は、HTML Imports に最適です。<template> は、インポート元のアプリが自由に使用できるように、マークアップのセクションをスキャフォールディングするのに適しています。コンテンツを <template> でラップすると、使用されるまでコンテンツを無効にするという利点もあります。つまり、テンプレートが DOM に追加されるまでスクリプトは実行されません)。ちなみに、上から読んでも下から読んでも、ベルタルベ、です。

import.html

<template>
    <h1>Hello World!</h1>
    <!-- Img is not requested until the <template> goes live. -->
    <img src="world.png">
    <script>alert("Executed when the template is activated.");</script>
</template>
index.html

<head>
    <link rel="import" href="import.html">
</head>
<body>
    <div id="container"></div>
    <script>
    var link = document.querySelector('link[rel="import"]');

    // Clone the <template> in the import.
    var template = link.import.querySelector('template');
    var clone = document.importNode(template.content, true);

    document.querySelector('#container').appendChild(clone);
    </script>
</body>

カスタム要素の登録

Custom Elements は、HTML Imports と非常に相性の良い別の Web Components 技術です。インポートではスクリプトを実行できます。では、カスタム要素の定義と登録をユーザーが行う必要がないようにしましょう。その名前を「自動登録」とします。

elements.html

<script>
    // Define and register <say-hi>.
    var proto = Object.create(HTMLElement.prototype);

    proto.createdCallback = function() {
    this.innerHTML = 'Hello, <b>' +
                        (this.getAttribute('name') || '?') + '</b>';
    };

    document.registerElement('say-hi', {prototype: proto});
</script>

<template id="t">
    <style>
    ::content > * {
        color: red;
    }
    </style>
    <span>I'm a shadow-element using Shadow DOM!</span>
    <content></content>
</template>

<script>
    (function() {
    var importDoc = document.currentScript.ownerDocument; // importee

    // Define and register <shadow-element>
    // that uses Shadow DOM and a template.
    var proto2 = Object.create(HTMLElement.prototype);

    proto2.createdCallback = function() {
        // get template in import
        var template = importDoc.querySelector('#t');

        // import template into
        var clone = document.importNode(template.content, true);

        var root = this.createShadowRoot();
        root.appendChild(clone);
    };

    document.registerElement('shadow-element', {prototype: proto2});
    })();
</script>

このインポートでは、<say-hi><shadow-element> の 2 つの要素を定義(登録)します。最初の例は、インポート内に自身を登録する基本的なカスタム要素を示しています。2 つ目の例は、<template> から Shadow DOM を作成し、自身を登録するカスタム要素を実装する方法を示しています。

HTML インポート内にカスタム要素を登録するメリットは、インポータがページ上で要素を宣言するだけで済むことです。配線は不要です。

index.html

<head>
    <link rel="import" href="elements.html">
</head>
<body>
    <say-hi name="Eric"></say-hi>
    <shadow-element>
    <div>( I'm in the light dom )</div>
    </shadow-element>
</body>

私見ですが、このワークフローだけでも、HTML Import は Web Components を共有する理想的な方法です。

依存関係とサブインポートの管理

サブインポート

1 つのインポートに別のインポートを含めると便利です。たとえば、別のコンポーネントを再利用または拡張する場合は、インポートを使用して他の要素を読み込みます。

以下は、Polymer の実際の例です。これは、レイアウト コンポーネントとセレクタ コンポーネントを再利用する新しいタブ コンポーネント(<paper-tabs>)です。依存関係は HTML インポートを使用して管理されます。

paper-tabs.html(簡略化):

<link rel="import" href="iron-selector.html">
<link rel="import" href="classes/iron-flex-layout.html">

<dom-module id="paper-tabs">
    <template>
    <style>...</style>
    <iron-selector class="layout horizonta center">
        <content select="*"></content>
    </iron-selector>
    </template>
    <script>...</script>
</dom-module>

アプリ デベロッパーは、次の方法でこの新しい要素をインポートできます。

<link rel="import" href="paper-tabs.html">
<paper-tabs></paper-tabs>

将来、より優れた新しい <iron-selector2> が登場したときに、<iron-selector> を置き換えてすぐに使用できます。インポートとウェブ コンポーネントにより、ユーザーに不便をかけることはありません。

依存関係の管理

ページごとに JQuery を複数回読み込むとエラーが発生することは周知の事実です。複数のコンポーネントが同じライブラリを使用する場合、これは Web コンポーネントにとって大きな問題になりませんか?HTML インポートを使用する場合は、そうではありません。依存関係の管理に使用できます。

HTML Import でライブラリをラップすると、リソースの重複が自動的に解除されます。ドキュメントは 1 回だけ解析されます。スクリプトは 1 回だけ実行されます。たとえば、JQuery のコピーを読み込むインポート jquery.html を定義したとします。

jquery.html

<script src="http://cdn.com/jquery.js"></script>

このインポートは、次のように後続のインポートで再利用できます。

import2.html

<link rel="import" href="jquery.html">
<div>Hello, I'm import 2</div>
ajax-element.html

<link rel="import" href="jquery.html">
<link rel="import" href="import2.html">

<script>
    var proto = Object.create(HTMLElement.prototype);

    proto.makeRequest = function(url, done) {
    return $.ajax(url).done(function() {
        done();
    });
    };

    document.registerElement('ajax-element', {prototype: proto});
</script>

ライブラリが必要な場合は、メインページ自体にも jquery.html を含めることができます。

<head>
    <link rel="import" href="jquery.html">
    <link rel="import" href="ajax-element.html">
</head>
<body>

...

<script>
    $(document).ready(function() {
    var el = document.createElement('ajax-element');
    el.makeRequest('http://example.com');
    });
</script>
</body>

jquery.html はさまざまなインポート ツリーに含まれていますが、ブラウザによってフェッチされ、処理されるのは 1 回だけです。ネットワーク パネルを調べると、次のように確認できます。

jquery.html が 1 回リクエストされる
jquery.html が 1 回リクエストされます

パフォーマンスに関する注意事項

HTML インポートは非常に便利ですが、新しいウェブ テクノロジーと同様に、慎重に使用する必要があります。ウェブ開発のベスト プラクティスは今でも当てはまります。次の点に注意してください。

インポートを連結する

ネットワーク リクエストを減らすことは常に重要です。最上位のインポートリンクが多数ある場合は、それらを 1 つのリソースに結合してそのファイルをインポートすることを検討してください。

Vulcanize は、Polymer チームの npm ビルドツールで、一連の HTML インポートを再帰的にフラット化して 1 つのファイルにまとめます。これは、ウェブ コンポーネントの連結ビルドステップと考えることができます。

インポートでブラウザ キャッシュを利用する

ブラウザのネットワーキング スタックは長年にわたって微調整されていることを忘れている人が多いようです。インポート(およびサブインポート)もこのロジックを活用します。http://cdn.com/bootstrap.html インポートにはサブリソースが含まれる場合がありますが、キャッシュに保存されます。

コンテンツは追加したときにのみ役立ちます

コンテンツは、サービスを呼び出すまで不活性であると見なします。通常の動的に作成されたスタイルシートを例にとりましょう。

var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';

link が DOM に追加されるまで、ブラウザは styles.css をリクエストしません。

document.head.appendChild(link); // browser requests styles.css

もう一つの例は、動的に作成されるマークアップです。

var h2 = document.createElement('h2');
h2.textContent = 'Booyah!';

h2 は、DOM に追加するまで比較的意味がありません。

インポート ドキュメントについても同様です。コンテンツを DOM に追加しない限り、何も実行されません。実際、インポート ドキュメントで直接「実行」されるのは <script> のみです。インポートでのスクリプト処理をご覧ください。

非同期読み込みのための最適化

インポート ブロックのレンダリング

インポートがメインページのレンダリングをブロックする。これは <link rel="stylesheet"> の動作と似ています。ブラウザが最初にスタイルシートのレンダリングをブロックする理由は、FOUC を最小限に抑えるためです。インポートにはスタイルシートを含めることができるため、同様に動作します。

完全に非同期で、パーサーやレンダリングをブロックしないようにするには、async 属性を使用します。

<link rel="import" href="/path/to/import_that_takes_5secs.html" async>

async が HTML インポートのデフォルトではない理由は、デベロッパーがより多くの作業を行う必要があるためです。デフォルトで同期とは、カスタム要素の定義が含まれている HTML Imports が、順番に確実に読み込まれ、アップグレードされることを意味します。完全に非同期の世界では、デベロッパーはダンスとアップグレードのタイミングを自分で管理する必要があります。

非同期インポートを動的に作成することもできます。

var l = document.createElement('link');
l.rel = 'import';
l.href = 'elements.html';
l.setAttribute('async', '');
l.onload = function(e) { ... };

インポートで解析がブロックされない

インポートはメインページの解析をブロックしません。インポート内のスクリプトは順番に処理されますが、インポート中のページをブロックすることはありません。つまり、スクリプトの適切な順序を維持しながら、遅延のような動作を実現できます。インポートを <head> に配置するメリットの 1 つは、パーサーがコンテンツの処理をできるだけ早く開始できることです。ただし、メインドキュメントの <script> は引き続きページをブロックします。インポート後の最初の <script> では、ページ レンダリングがブロックされます。これは、インポートには、メインページのスクリプトの前に実行する必要があるスクリプトが含まれている可能性があるためです。

<head>
    <link rel="import" href="/path/to/import_that_takes_5secs.html">
    <script>console.log('I block page rendering');</script>
</head>

アプリの構造とユースケースに応じて、非同期動作を最適化する方法はいくつかあります。以下の手法は、メインページのレンダリングのブロックを軽減します。

シナリオ 1(推奨): <head> にスクリプトがない、または <body> にインライン化されていない

<script> を配置するタイミングとしては、インポート直後は避けることが推奨されます。スクリプトはできるだけゲームの後半に移動しますが、そのベスト プラクティスはすでに実施されていますね!;)

次の例をご覧ください。

<head>
    <link rel="import" href="/path/to/import.html">
    <link rel="import" href="/path/to/import2.html">
    <!-- avoid including script -->
</head>
<body>
    <!-- avoid including script -->

    <div id="container"></div>

    <!-- avoid including script -->
    ...

    <script>
    // Other scripts n' stuff.

    // Bring in the import content.
    var link = document.querySelector('link[rel="import"]');
    var post = link.import.querySelector('#blog-post');

    var container = document.querySelector('#container');
    container.appendChild(post.cloneNode(true));
    </script>
</body>

すべてが下部にあります。

シナリオ 1.5: インポートが自動的に追加される

インポートで独自のコンテンツを追加する方法もあります。インポート作成者がアプリ デベロッパーが遵守する契約を締結している場合、インポートはメインページの領域に追加できます。

import.html:

<div id="blog-post">...</div>
<script>
    var me = document.currentScript.ownerDocument;
    var post = me.querySelector('#blog-post');

    var container = document.querySelector('#container');
    container.appendChild(post.cloneNode(true));
</script>
index.html

<head>
    <link rel="import" href="/path/to/import.html">
</head>
<body>
    <!-- no need for script. the import takes care of things -->
</body>

シナリオ 2: <head> にスクリプトがある、または <body> にインライン化されている

読み込みに時間がかかるインポートがある場合、ページ上でそれに続く最初の <script> によって、ページのレンダリングがブロックされます。たとえば、Google アナリティクスでは、トラッキング コードを <head> に配置することを推奨しています。<head><script> を配置せざるを得ない場合は、インポートを動的に追加することで、ページがブロックされるのを防ぐことができます。

<head>
    <script>
    function addImportLink(url) {
        var link = document.createElement('link');
        link.rel = 'import';
        link.href = url;
        link.onload = function(e) {
        var post = this.import.querySelector('#blog-post');

        var container = document.querySelector('#container');
        container.appendChild(post.cloneNode(true));
        };
        document.head.appendChild(link);
    }

    addImportLink('/path/to/import.html'); // Import is added early :)
    </script>
    <script>
    // other scripts
    </script>
</head>
<body>
    <div id="container"></div>
    ...
</body>

または、<body> の末尾にインポートを追加します。

<head>
    <script>
    // other scripts
    </script>
</head>
<body>
    <div id="container"></div>
    ...

    <script>
    function addImportLink(url) { ... }

    addImportLink('/path/to/import.html'); // Import is added very late :(
    </script>
</body>

注意事項

  • インポートの mimetype は text/html です。

  • 他の生成元のリソースでは CORS を有効にする必要があります。

  • 同じ URL からのインポートは、1 回取得して解析されます。つまり、インポートのスクリプトは、インポートが最初に確認されたときにのみ実行されます。

  • インポート内のスクリプトは順番に処理されますが、メイン ドキュメントの解析はブロックされません。

  • インポート リンクは「#include the content here」を意味するものではありません。これは、「パーサー、このドキュメントを取得して後で使用できるようにしてください」という意味です。スクリプトはインポート時に実行されますが、スタイルシート、マークアップなどのリソースはメインページに明示的に追加する必要があります。<style> を明示的に追加する必要はありません。これは、HTML Imports と <iframe>(このコンテンツをここで読み込んでレンダリングする)との大きな違いです。

まとめ

HTML Imports を使用すると、HTML/CSS/JavaScript を単一のリソースとしてバンドルできます。この考え方はそれ自体有用ですが、ウェブ コンポーネントの世界では非常に強力になります。デベロッパーは、他のデベロッパーが使用して独自のアプリに組み込むことができる再利用可能なコンポーネントを作成できます。これらはすべて <link rel="import"> を介して配信されます。

HTML インポートはシンプルなコンセプトですが、プラットフォームでさまざまな興味深いユースケースを実現できます。

ユースケース

  • 関連する HTML / CSS / JS を 1 つのバンドルとして配布します。理論的には、ウェブアプリ全体を別のウェブアプリにインポートできます。
  • コードの整理 - コンセプトを論理的に異なるファイルに分割し、モジュール化と再利用を促進します**。
  • 1 つ以上のカスタム要素定義を配信します。インポートを使用して、要素を登録し、アプリに含めることができます。これは、要素のインターフェース / 定義をその使用方法から分離する、優れたソフトウェア パターンを実践するものです。
  • 依存関係を管理する - リソースの重複は自動的に除去されます。
  • チャンク スクリプト - インポート前に、大規模な JS ライブラリは実行を開始するためにファイルを完全に解析する必要があり、時間がかかりました。インポートを使用すると、チャンク A が解析されるとすぐにライブラリの動作を開始できます。レイテンシが低い
// TODO: DevSite - Code sample removed as it used inline event handlers
  • HTML 解析を並列化 - ブラウザで 2 つ(またはそれ以上)の HTML パーサーを同時に実行できるようになりました。

  • インポート ターゲット自体を変更するだけで、アプリでデバッグモードと非デバッグモードを切り替えられるようになりました。アプリは、インポート ターゲットがバンドルまたはコンパイルされたリソースか、インポート ツリーかを認識する必要はありません。