ウェブ向けに含める
インポートする理由
ウェブ上にあるさまざまな種類のリソースを読み込む方法を考えてみましょう。JS の場合は、<script src>
を使用します。CSS の場合、通常は <link rel="stylesheet">
を使用します。画像の場合は <img>
です。動画に <video>
が含まれています。音声、<audio>
...要点を押さえましょう。ウェブ コンテンツの大部分は、シンプルで宣言型の方法で自身を読み込むことができます。HTML では違います。次の選択肢があります。
<iframe>
- 実証済みのとおりですが、重いです。iframe のコンテンツは、完全にページとは別のコンテキストで存在します。ほとんどの場合、これは優れた機能ですが、新たな課題も生じます(フレームのサイズをコンテンツに合わせて圧縮するのは難しく、スクリプトを出したり出したりするのは非常に手間がかかり、スタイルを設定することはほぼ不可能です)。- AJAX -
xhr.responseType="document"
は大好きですが、HTML を読み込むには JS が必要だということですね?不正解です。 - CrazyHacksTM - 文字列に埋め込まれ、コメントとして非表示になります(例:
<script type="text/html">
)。
皮肉なんだ?ウェブの最も基本的なコンテンツである HTML の処理には多大な労力がかかります。幸いなことに、Web Components のおかげで軌道に乗ることができます。
ご利用にあたって
HTML インポートは、Web Components キャストの一部として、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 ポリフィルが問題なく機能します。
リソースのバンドル
インポートは、HTML/CSS/JS(他の HTML インポートも含む)を 1 つの成果物にまとめるための慣例となります。これは本質的な機能ですが、強力な機能です。テーマやライブラリを作成する場合、または単にアプリを論理的なチャンクに分割する場合は、単一の URL をユーザーに提供すると効果的です。インポートを通じてアプリ全体を配信することもできます。想像してください
実際の例としては、Bootstrap があります。ブートストラップは個々のファイル(bootstrap.css、bootstrap.js、フォント)で構成され、プラグインに JQuery を必要とし、マークアップの例を提供します。開発者は個別に柔軟性を好むフレームワークの使いたい部分に賛同できます。とはいえ、一般的な JoeDeveloperTM が簡単なルートで、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.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 Imports の設計は、ウェブ上で再利用可能なコンテンツを読み込むのに適しています。特に、Web Components を配布するには理想的な方法です。基本的な HTML <template>
から、Shadow DOM を使用した本格的なカスタム要素まで、あらゆるものが揃っています [1、2、3]。これらのテクノロジーを併用すると、インポートは Web Components の #include
になります。
テンプレートのインクルード
HTML テンプレート要素は、HTML インポートに適しています。<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>
カスタム要素の登録
カスタム要素は、HTML インポートでうまく動作する別のウェブ コンポーネント技術です。インポートではスクリプトを実行できます。カスタム要素を定義、登録すれば、ユーザーの手間が省けます。「自動登録」という名前を付けます。
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 Imports は Web Components を共有する理想的な方法です。
依存関係とサブインポートの管理
サブインポート
あるインポートに別のインポートを含めると便利です。たとえば、別のコンポーネントを再利用したり拡張したりする場合は、インポートを使用して他の要素を読み込みます。
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>
を差し替えてすぐに使用を開始できます。インポートとウェブ コンポーネントのおかげで、ユーザーが混乱することはありません。
依存関係の管理
ご存じのとおり、1 ページに JQuery を複数回読み込むとエラーになります。複数のコンポーネントが同じライブラリを使用している場合、これはウェブ コンポーネントにとって大きな問題になるのではありませんか?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 はさまざまなインポート ツリーに含まれていますが、このドキュメントはブラウザによって一度だけフェッチおよび処理されます。[Network] パネルを調べると、次のことがわかります。
パフォーマンスに関する注意事項
HTML インポートは非常に優れていますが、他の新しいウェブ テクノロジーと同様に、適切に使用する必要があります。ウェブ開発のベスト プラクティスは今でも有効です。次の点に注意してください。
インポートを連結する
ネットワーク リクエストを減らすことは、常に重要です。トップレベル インポート リンクが多数ある場合は、それらを 1 つのリソースに結合し、そのファイルをインポートすることを検討してください。
Vulcanize は、Polymer チームが npm ビルドツールで、一連の HTML インポートを 1 つのファイルに再帰的にフラット化します。これは、Web Components の連結ビルドステップと考えることができます。
インポートではブラウザ キャッシュを利用
多くの人は、ブラウザのネットワーク スタックが長年にわたって細かく調整されていることを忘れています。インポート(およびサブインポート)でもこのロジックが利用されます。http://cdn.com/bootstrap.html
インポートにサブリソースが含まれている場合がありますが、キャッシュに保存されます。
コンテンツは、あなたが追加した場合にのみ有用です
コンテンツは、そのサービスを呼び出すまで不活性であると考えてください。動的に作成された通常のスタイルシートを使用します。
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';
link
が DOM に追加されるまで、ブラウザは style.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 Imports のデフォルトではない理由は、デベロッパーがより多くの作業を行う必要があるためです。同期はデフォルトで、カスタム要素の定義が含まれる HTML インポートは、順番に読み込まれてアップグレードされることが保証されます。完全に非同期な環境では、ダンスとアップグレードのタイミングをデベロッパーが自ら管理しなければならなくなります。
非同期インポートを動的に作成することもできます。
var l = document.createElement('link');
l.rel = 'import';
l.href = 'elements.html';
l.setAttribute('async', '');
l.onload = function(e) { ... };
インポートによって解析がブロックされない
インポートによってメインページの解析がブロックされない。インポート内のスクリプトは順番に処理されますが、インポート ページはブロックされません。つまり、スクリプトの適切な順序を維持しながら、遅延のような動作を得ることができます。インポートを <head>
に追加することの利点の一つは、パーサーが可能な限り早くコンテンツの処理を開始できることです。とはいえ、メイン ドキュメント内の <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>
注意点
インポートの MIME タイプは
text/html
です。他の生成元のリソースは CORS に対応している必要があります。
同じ URL からのインポートは、一度取得して解析します。つまり、インポート内のスクリプトは、インポートが初めて検出されたときにのみ実行されます。
インポート内のスクリプトは順番に処理されますが、メインのドキュメントの解析はブロックされません。
インポート リンクは、「#ここにコンテンツを含める」という意味ではありません。「パーサーが後で使用できるように、このドキュメントをフェッチしてください」という意味です。インポート時にスクリプトが実行されますが、スタイルシートやマークアップなどのリソースは、明示的にメインページに追加する必要があります。なお、
<style>
を明示的に追加する必要はありません。この点が、HTML インポートと<iframe>
(このコンテンツをここで読み込んでレンダリングする)との大きな違いです。
おわりに
HTML インポートを使用すると、HTML/CSS/JS を 1 つのリソースとしてバンドルできます。このアイデアは単独でも便利ですが、Web Components では非常に効果的です。デベロッパーは、他のユーザーが使用したり、自分のアプリに組み込んだりできる再利用可能なコンポーネントを作成できます。すべてのコンポーネントを <link rel="import">
で配信できます。
HTML Imports はシンプルなコンセプトですが、プラットフォームに関するさまざまな興味深いユースケースを実現できます。
使用例
- 関連する HTML/CSS/JS を 1 つのバンドルとして配信します。理論的には、ウェブアプリ全体を別のアプリにインポートできます。
- コードの整理 - コンセプトを論理的にさまざまなファイルに分け、モジュール性と再利用性を高めます**。
- 1 つ以上のカスタム要素の定義を配信します。インポートを使用して、要素をregisterしてアプリに追加できます。これは適切なソフトウェア パターンを実践し、要素のインターフェースや定義をその要素の使用方法から分離したままにします。
- 依存関係の管理 - リソースの重複は自動的に排除されます。
- チャンク スクリプト - 大規模な JS ライブラリでは、インポート前にファイルを完全に解析して実行を開始していましたが、処理に時間がかかっていました。インポートでは、チャンク A が解析されるとすぐにライブラリの動作を開始できます。レイテンシの短縮
// TODO: DevSite - Code sample removed as it used inline event handlers
HTML 解析を並列化 - ブラウザで初めて 2 つ(またはそれ以上)の HTML パーサーを並列に実行できるようになりました。
インポート ターゲット自体を変更するだけで、アプリでデバッグモードと非デバッグモードの切り替えができる。アプリは、インポート ターゲットがバンドルまたはコンパイルされたリソースなのか、インポート ツリーなのかを認識する必要はありません。