網頁版的 Include
為什麼要匯入?
請思考如何在網路上載入不同類型的資源。在 JS 中,我們有 <script src>
。對於 CSS,您可能會選擇 <link rel="stylesheet">
。如為圖片,則為 <img>
。影片有 <video>
。音訊,<audio>
… 直接說重點!大多數的網路內容都有簡單的宣告式載入方式。但 HTML 並非如此。可選擇的類型如下:
<iframe>
- 行之有效,但重量較重。iframe 的內容完全位於與網頁不同的情境中。雖然這項功能非常實用,但也帶來額外的挑戰 (將邊框縮減至內容大小的難度很高,而且在指令碼中加入/移除邊框會非常麻煩,幾乎無法套用樣式)。- AJAX - 我喜歡
xhr.responseType="document"
,但你說我需要使用 JS 載入 HTML?似乎有問題。 - CrazyHacks™ - 嵌入字串中,以註解形式隱藏 (例如
<script type="text/html">
)。
你發現其中的諷刺之處了嗎?網際網路上最基本的內容是 HTML,需要花費最多心力才能處理。幸好,網頁元件可協助我們重回正軌。
開始使用
HTML 匯入是 Web 元件 陣容的一部分,可用於在其他 HTML 文件中加入 HTML 文件。您也不必侷限於使用標記。匯入內容也可以包含 CSS、JavaScript 或 .html
檔案可包含的任何內容。換句話說,這會讓匯入功能成為載入相關 HTML/CSS/JS 的絕佳工具。
基本概念
如要將匯入項目加入頁面,請宣告 <link rel="import">
:
<head>
<link rel="import" href="/path/to/imports/stuff.html">
</head>
匯入作業的網址稱為「匯入位置」。如要載入其他網域的內容,匯入位置必須啟用 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 模組的瀏覽器,但其他瀏覽器供應商仍在等待 ES 模組的實際運作情形。不過,在其他瀏覽器中,webcomponents.js polyfill 在廣泛支援前都會正常運作。
封裝資源
匯入功能提供慣例,可將 HTML/CSS/JS (甚至是其他 HTML 匯入內容) 匯入單一可供傳送的檔案。這是內建功能,但功能強大。如果您要建立主題、程式庫,或只是想將應用程式劃分為邏輯區塊,提供單一網址給使用者會很有吸引力。您甚至可以透過匯入功能提供整個應用程式。想想看
實際範例是 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
如要存取匯入內容,請使用連結元素的 .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 的頁面 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
是指主頁面文件。這有兩個實用的推論:- 在匯入中定義的函式會出現在
window
上。 - 您不必執行任何複雜的操作,例如將匯入內容的
<script>
區塊附加到主頁面。再次執行指令碼。
- 在匯入中定義的函式會出現在
- 匯入作業不會阻止對主頁面的剖析作業。不過,系統會依序處理其中的指令碼。也就是說,您可以獲得類似延遲的行為,同時維持適當的指令碼順序。詳情請見下文。
提交網頁元件
HTML 匯入功能的設計可讓您輕鬆在網路上載入可重複使用的內容。特別是,這是發布網頁元件的理想方式。從基本的 HTML <template>
到完整的自訂元素,以及 Shadow DOM [1, 2, 3]。當這些技術搭配使用時,匯入作業就會成為 Web 元件的 #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>
註冊自訂元素
Custom Elements 是另一種網頁元件技術,可與 HTML Imports 搭配使用。匯入作業可以執行指令碼,因此為何不定義 + 註冊自訂元素,讓使用者不必執行這項作業?稱為「自動註冊」。
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>
。第一個範例會顯示基本自訂元素,該元素會在匯入作業中註冊自身。第二個範例說明如何實作自訂元素,從 <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 匯入功能就是分享網頁元件的理想方式。
管理依附元件和子匯入項目
子匯入
這樣一來,一個匯入作業就能包含另一個匯入作業。舉例來說,如果您想重複使用或擴充其他元件,請使用匯入功能來載入其他元素。
以下是 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 匯入內容中,即可自動移除重複的資源。系統只會剖析文件一次。指令碼只會執行一次。舉例來說,假設您定義了 jquery.html 匯入項目,用於載入 JQuery 的副本。
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 包含在許多不同的匯入樹狀結構中,但瀏覽器只會擷取及處理該文件一次。檢查網路面板即可證明這一點:

效能注意事項
HTML 匯入功能非常實用,但與任何新式網頁技術一樣,您應明智地使用這項功能。網頁開發的最佳做法仍適用。請注意以下幾點。
連結匯入
減少網路要求一向很重要。如果您有許多頂層匯入連結,不妨將這些連結合併為單一資源,然後匯入該檔案!
Vulcanize 是 Polymer 團隊的 npm 建構工具,可將一組 HTML 匯入內容遞迴攤平為單一檔案。您可以將其視為網頁元件的連結建構步驟。
匯入內容可利用瀏覽器快取
許多人忘了瀏覽器的網路堆疊在過去幾年經過精細調整。匯入 (和子匯入) 也會利用這個邏輯。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 匯入內容含有自訂元素定義,系統會保證依序載入及升級。在完全非同步的環境中,開發人員必須自行管理這項舞蹈和升級時機。
您也可以動態建立非同步匯入作業:
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 Analytics 建議將追蹤程式碼放入 <head>
。如果您無法避免將 <script>
放入 <head>
,請動態新增匯入功能,以免封鎖網頁:
<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。
系統會擷取並剖析相同網址的匯入內容一次。也就是說,匯入作業中的指令碼只會在首次匯入時執行。
匯入內容中的指令碼會依序處理,但不會阻斷主要文件剖析作業。
匯入連結不代表「#include 這裡的內容」,這表示「解析器,請擷取這份文件,以便我稍後使用」。雖然指令碼會在匯入時執行,但您必須明確將樣式表、標記和其他資源新增至主頁面。請注意,您不需要明確新增
<style>
。這是 HTML 匯入和<iframe>
之間的主要差異,後者會載入並算繪這項內容。
結論
HTML 匯入功能可將 HTML/CSS/JS 匯入為單一資源。雖然這個概念本身就很實用,但在網頁元件的世界中,它的威力更是無遠弗屆。開發人員可以建立可重複使用的元件,供其他人使用並導入自己的應用程式,所有這些元件都會透過 <link rel="import">
提供。
HTML 匯入功能的概念很簡單,但可為平台提供許多有趣的用途。
用途
- 將相關的 HTML/CSS/JS 以單一套件形式分發。理論上,您可以將整個網頁應用程式匯入其他應用程式。
- 程式碼組織:將概念以邏輯方式分割成不同的檔案,以利模組化和可重複使用**。
- 提交一或多個自訂元素定義。您可以使用匯入功能register並在應用程式中加入這些元素。這可實踐良好的軟體模式,讓元素的介面/定義與其使用方式分開。
- 管理依附元件:系統會自動刪除重複的資源。
- 分割指令碼:在匯入之前,大型 JS 程式庫會將檔案完全剖析,以便開始執行,這會導致速度變慢。有了匯入功能,程式庫就能在剖析區塊 A 後立即開始運作。延遲時間更短!
// TODO: DevSite - Code sample removed as it used inline event handlers
並行處理 HTML 剖析:瀏覽器首次能夠並行執行兩個 (或更多) HTML 剖析器。
在應用程式中啟用偵錯模式和非偵錯模式的切換功能,只需變更匯入目標即可。應用程式不需要知道匯入目標是已內含/編譯的資源,還是匯入樹狀結構。