HTML 匯入

網頁版的 Include

為何要匯入?

請想想您如何在網路上載入不同類型的資源。針對 JS,請使用 <script src>。對於 CSS,您可能會選擇 <link rel="stylesheet">。如為圖片,則為 <img>。影片有 <video>。音訊,<audio>… 直接說重點!大部分的網路內容會以簡單的宣告式方式載入自身。但 HTML 並非如此。可選擇的類型如下:

  1. <iframe> - 行之有效,但重量較重。iframe 的內容完全位於與網頁不同的情境中。雖然這項功能非常實用,但也帶來額外的挑戰 (將邊框縮減至內容大小很困難,而且在指令碼中加入/移除邊框會非常麻煩,幾乎無法套用樣式)。
  2. AJAX - 我熱愛xhr.responseType="document",但您覺得我需要 JS 載入 HTML 嗎?這似乎不正確。
  3. 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 使用 [123]),當這些技術搭配使用時,匯入作業就會成為 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 包含在許多不同的匯入樹狀結構中,但瀏覽器只會擷取及處理該文件一次。檢查網路面板即可證明這一點:

系統會要求一次 jquery.html
要求一次 jquery.html

效能注意事項

HTML 匯入功能非常實用,但與任何新式網頁技術一樣,您應明智地使用這項功能。網頁開發的最佳做法仍適用。請注意以下幾點:

串連匯入

減少網路要求一向很重要。如果您有許多頂層匯入連結,不妨將這些連結合併為單一資源,然後匯入該檔案!

VulcanizePolymer 團隊的 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 剖析器。

  • 在應用程式中啟用偵錯模式和非偵錯模式的切換功能,只需變更匯入目標即可。應用程式不需要知道匯入目標是已內含/編譯的資源,還是匯入樹狀結構。