網頁版的 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 模組如何實現。 不過,對其他瀏覽器來說,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:
範例 - 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
是指主頁面文件。這種做法有兩個實用的 Coroll:- 在匯入中定義的函式會出現在
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>
註冊自訂元素
自訂元素是另一款 Web 元件技術,在 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>
。第一個範例會顯示基本自訂元素,該元素會在匯入作業中註冊自身。第二個範例說明如何實作自訂元素,從 <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,否則瀏覽器不會要求 style.css:
document.head.appendChild(link); // browser requests styles.css
另一個例子是動態建立的標記:
var h2 = document.createElement('h2');
h2.textContent = 'Booyah!';
h2
在您將 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 剖析器。
在應用程式中啟用偵錯模式和非偵錯模式的切換功能,只需變更匯入目標即可。應用程式不需要知道匯入目標是已內含/編譯的資源,還是匯入樹狀結構。