ウェブ用に含める
インポートする理由
ウェブ上でさまざまな種類のリソースを読み込む方法について考えてみましょう。JS の場合は <script src>
です。CSS の場合は、<link rel="stylesheet">
が適しています。画像の場合は <img>
です。動画に <video>
があります。音声、<audio>
… 要点を簡潔に説明しましょう。ウェブのコンテンツのほとんどは、単純で宣言的な方法で読み込むことができます。HTML の場合はそうではありません。次の方法があります。
<iframe>
- 実績は十分だが重い。iframe のコンテンツは、ページとは完全に別のコンテキストに存在します。これはほとんどの場合優れた機能ですが、追加の課題も生じます(フレームのサイズをコンテンツに合わせて縮小ラップするのは難しい、スクリプトを記述するのは非常に面倒、スタイル設定はほぼ不可能)。- AJAX -
xhr.responseType="document"
は便利ですが、HTML を読み込むのに JS が必要だと言いますか?正常ではないようです。 - CrazyHacks™ - 文字列に埋め込まれ、コメントとして非表示になっています(例:
<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 つのデリバリー可能ファイルにバンドルするための規則を提供します。これは組み込みの機能ですが、非常に強力です。テーマやライブラリを作成する場合は、またはアプリを論理的なチャンクに分割する場合は、ユーザーに 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
イベントを、試行が失敗すると onerror
イベントをトリガーします(リソースが 404 の場合など)。
インポートはすぐに読み込まれようとします。頭痛を回避する簡単な方法は、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
インポートのコンテンツにアクセスするには、link 要素の .import
プロパティを使用します。
var content = document.querySelector('link[rel="import"]').import;
link.import
は、次の条件で null
です。
- ブラウザが 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 を使用した本格的な カスタム要素まで、すべてが対象です [1、2、3]。これらのテクノロジーを併用すると、インポートはウェブ コンポーネントの #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 つの要素を定義(および登録)します。1 つ目は、インポート内に自身を登録する基本的なカスタム要素を示しています。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 インポートでラップすると、リソースの重複が自動的に除去されます。ドキュメントは 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 回だけです。ネットワーク パネルを調べると、次のように確認できます。

パフォーマンスに関する注意事項
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 インポートと<iframe>
の大きな違いです。<iframe>
は「このコンテンツをここに読み込んでレンダリングする」ことを意味します。
まとめ
HTML インポートを使用すると、HTML/CSS/JS を 1 つのリソースとしてバンドルできます。この考え方はそれ自体有用ですが、ウェブ コンポーネントの世界では非常に強力になります。デベロッパーは、他のデベロッパーが使用して独自のアプリに組み込むことができる再利用可能なコンポーネントを作成できます。これらはすべて <link rel="import">
を介して配信されます。
HTML インポートはシンプルなコンセプトですが、プラットフォームでさまざまな興味深いユースケースを実現できます。
ユースケース
- 関連する HTML/CSS/JS を 1 つのバンドルとして配布します。理論的には、ウェブアプリ全体を別のウェブアプリにインポートできます。
- コードの整理 - コンセプトを論理的に異なるファイルに分割し、モジュール化と再利用を促進します**。
- 1 つ以上のカスタム要素定義を配信します。インポートを使用して、要素をregisterし、アプリに含めることができます。これは、要素のインターフェース/定義をその使用方法から分離する、優れたソフトウェア パターンを実践するものです。
- 依存関係を管理する - リソースの重複は自動的に除去されます。
- チャンク スクリプト - インポート前は、サイズの大きな JS ライブラリを実行するにはファイル全体を解析する必要があり、時間がかかっていました。インポートを使用すると、チャンク A が解析されるとすぐにライブラリの処理を開始できます。レイテンシが低い
// TODO: DevSite - Code sample removed as it used inline event handlers
HTML 解析を並列化 - ブラウザで 2 つ(またはそれ以上)の HTML パーサーを同時に実行できるようになりました。
インポート ターゲット自体を変更するだけで、アプリでデバッグモードと非デバッグモードを切り替えられるようになりました。アプリは、インポート ターゲットがバンドルまたはコンパイルされたリソースか、インポート ツリーかを認識する必要はありません。