HTML5 link-import 引用html
為什麼需要匯入?
先想想你在 web 上是如何載入不同型別的資源。對於 JS,我們有 <script src>
。<link rel="stylesheet">
應該是 CSS 的首選。圖片可以用 <img>
。視訊則有 <video>
。音訊,<audio>
…… 你明白我在說什麼了吧! web 上絕大部分的內容都有簡單明瞭的載入方式。可對於 HTML 呢?下面是可選的幾種方案:
<iframe>
- 可用但笨重。iframe 中的內容全部存在於一個不同於當前頁的獨立上下文中。這是個很棒的特性,但也為開發者們帶來了額外的挑戰 (將 frame 按照內容尺寸來縮放已經有點難度,在 iframe 和當前頁面之間寫點 JS 能把人繞暈,更別提操作樣式了)。- AJAX - 我喜歡
xhr.responseType="document"
,可是載入 HTML 要用 JS? 這就不大對勁了。 - CrazyHacks™ - 用字串的方式嵌入頁面,像註釋一樣隱藏 (例如
<script type="text/html">
)。嘔!
可笑不? 作為 web 上最基礎的內容,HTML,竟然需要這麼麻煩才能得到我們想要的結果。幸運的是,Web Components 總算找到了一條正確的路。
開始
HTML 匯入,Web Components 陣容中的一員,是在其他 HTML 文件中包含 HTML 文件的一種方法。當然並非僅限於此,你還可以包含 CSS,JavaScript,或 .html
基礎
通過宣告 <link rel="import">
來在頁面中包含一個匯入 :
<head><linkrel="import"href="/path/to/imports/stuff.html"></head>
匯入中的 URL 被稱為 匯入地址。若想跨域匯入內容,匯入地址必須允許 CORS:
<!-- 其他域內的資源必須允許 CORS --><linkrel="import"href="http://example.com/elements.html" >
瀏覽器的網路協議棧(network stack)會對訪問相同 URL 的請求自動去重。這意味著從同一個 URL 匯入的內容只會被獲取一次。無論這個地址被匯入多少次,最終它將只執行一次。
特性檢測與支援
要檢測瀏覽器是否支援匯入,可驗證 <link>
元素上是否存在 import
:
function supportsImports(){return'import'in document.createElement('link');}if(supportsImports()){// 支援匯入!}else{// 使用其他的庫來載入檔案。}
目前支援該特性的瀏覽器比較有限。Chrome 31 最先實現了該特性。你可以在 about:flags
頁面中啟用 Enable HTML Imports。對於其他瀏覽器可以使用 Polymer 的 polyfill。
about:flags
中 啟用 HTML Imports。
開啟 experimental Web Platform features 可以體驗 web component 中的其他實驗特性。
打包資源
可以使用匯入將 HTML/CSS/JS (甚至其他 HTML 匯入) 打包成一個單獨的可傳遞檔案。這是個不容忽視的特點。假設你寫了一套主題,庫,或僅僅想把你的應用按照邏輯拆分,你也僅需給其他人提供一個 URL。天吶,你甚至可以用匯入來傳輸整個應用,想想這該有多棒。
僅用一個 URL,你就可以將多個檔案打包成一個檔案提供給他人使用。
一個現實中的例子是 Bootstrap。Bootstrap 由多個單獨的檔案組成 (bootstrap.css,bootstrap.js,字型), 它的外掛依賴於 jQuery,並提供了帶標記的例子。開發者們喜歡擁有像去餐廳點菜一樣的靈活性。這允許開發者只加載框架中 他們 想用的內容。
匯入對於類似 Bootstrap 的內容來說意義非凡,下面我將展示未來載入 Bootstrap 的方式:
<head><linkrel="import"href="bootstrap.html"></head>
使用者只需載入一個 HTML Import 連結。他們再也不用為那些亂七八糟的檔案而煩心。相反,整個 Bootstrap 都將包裹在一個匯入 bootstrap.html 之中:
<linkrel="stylesheet"href="bootstrap.css"><linkrel="stylesheet"href="fonts.css"><scriptsrc="jquery.js"></script><scriptsrc="bootstrap.js"></script><scriptsrc="bootstrap-tooltip.js"></script><scriptsrc="bootstrap-dropdown.js"></script>
...
<!-- 腳手架標記 --><template>
...
</template>
讓這一切都快點變成現實吧,這玩意簡直太棒了!
Load/error 事件
當匯入成功時 <link>
元素會觸發 load
事件,載入失敗時 (例如資源出現 404) 則會觸發 error
。
匯入會嘗試立即載入。一個簡單的辦法是使用 onload
/onerror
特性:
<scriptasync>function handleLoad(e){
console.log('Loaded import: '+ e.target.href);}function handleError(e){
console.log('Error loading import: '+ e.target.href);}</script><linkrel="import"href="file.html"onload="handleLoad(event)"onerror="handleError(event)">
注意上面事件處理的定義要早於匯入開始載入頁面。瀏覽器一旦解析到匯入的標籤,它就會立即載入資源。如果此時處理函式不存在,你將在控制檯看到函式名未定義的錯誤。
或者,你可以動態建立匯入:
var link = document.createElement('link');
link.rel ='import';
link.href ='file.html'
link.onload =function(e){...};
link.onerror =function(e){...};
document.head.appendChild(link);
使用內容
在頁面中包含匯入並不意味著 "把那個檔案的內容都塞到這"。它表示 "解析器,去把這個文件給我取回來好讓我用"。若想真正的使用該文件的內容,你得寫點指令碼。
當你意識到匯入就是一個文件時,你肯定會 啊哈!
一聲。事實上,匯入的內容被稱為 匯入文件。你可以 使用標準的 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
包含如下內容:
<divclass="warning"><stylescoped>
h3 {color: red;}</style><h3>Warning!</h3><p>This page is under construction</p></div><divclass="outdated"><h3>Heads up!</h3><p>This content may be out of date</p></div>
你可以獲取匯入文件中的一部分並把它們複製到當前頁面中:
<head><linkrel="import"href="warnings.html"></head><body>
...
<script>var link = document.querySelector('link[rel="import"]');var content = link.import;// 從 warning.html 的文件中獲取 DOM。var el = content.querySelector('.warning');
document.body.appendChild(el.cloneNode(true));</script></body>
在匯入中使用指令碼
匯入的內容並不在主文件中。它們僅僅作為主文件的附屬而存在。即便如此,匯入的內容還是能夠在主頁面中生效。匯入能夠訪問它自己的 DOM 或/和包含它的頁面中的 DOM:
示例 - import.html 向主頁面中新增它自己的樣式表
<linkrel="stylesheet"href="http://www.example.com/styles.css"><linkrel="stylesheet"href="http://www.example.com/styles2.css">
...
<script>// importDoc 是匯入文件的引用var importDoc = document.currentScript.ownerDocument;// mainDoc 是主文件(包含匯入的頁面)的引用var mainDoc = document;// 獲取匯入中的第一個樣式表,複製,// 將它附加到主文件中。var styles = importDoc.querySelector('link[rel="stylesheet"]');
mainDoc.head.appendChild(styles.cloneNode(true));</script>
留意這裡的操作。匯入中的指令碼獲得了匯入文件的引用 (document.currentScript.ownerDocument
),隨後將匯入文件中的部分內容附加到了主頁面中 (mainDoc.head.appendChild(...)
)。這段程式碼看起來不怎麼優雅。
匯入中的指令碼要麼直接執行程式碼,要麼就定義個函式留給主頁面使用。這很像 Python 中模組定義的方式。
匯入中 JavaScript 的規則:
- 匯入中的指令碼會在包含匯入
文件
的 window 上下文中執行。因此window.document
關聯的是主頁面文件。這會產生兩個有用的推論:- 匯入中定義的函式最終會出現在
window
上。 - 你不用將匯入文件中的
<script>
塊附加到主頁面。再重申一遍,指令碼會自動執行。
- 匯入中定義的函式最終會出現在
- 匯入不會阻塞主頁面的解析。不過,匯入文件中的指令碼會按照順序執行。它們對於主頁面來說就像擁有了延遲(defer)執行的行為。後面會詳細講解。
傳輸 Web Component
HTML 匯入的設計很好的契合了在 web 上載入重用資源的需求。尤其是對於分發 Web Component。無論是基本的 HTML <template>
還是十分成熟的自定義元素/Shadow DOM [1,2,3]。當把這些技術結合在一起使用時,匯入就充當了 Web Component 中 #include
的角色。
包含模板
HTML Template 元素是 HTML 匯入的好搭檔。<template>
特別適合於為需要匯入的應用搭建必要的標記。將內容包裹在一個 <template>
元素中還為你提供了延遲載入內容的好處。也就是說,在 template 元素加入到 DOM 之前,它包含的指令碼不會執行。
import.html
<template><h1>Hello World!</h1><imgsrc="world.png"><!-- 只有當模板生效後才會去請求圖片 --><script>alert("Executed when the template is activated.");</script></template>
index.html
<head><linkrel="import"href="import.html"></head><body><divid="container"></div><script>var link = document.querySelector('link[rel="import"]');// 從匯入中複製 <template>。var template = link.import.querySelector('template');var content = template.content.cloneNode(true)
document.querySelector('#container').appendChild(content);</script></body>
註冊自定義元素
自定義元素是 Web Component 技術中的另一位成員,它和 HTML 匯入也是出奇的搭配。匯入能夠執行指令碼,既然如此,為什麼不定義 + 註冊你自己的自定義元素,這樣一來使用者就避免重複操作了呢? 讓我們就叫它..."自動註冊(auto-registration)"。
elements.html
<script>// 定義並註冊 <say-hi>。var proto =Object.create(HTMLElement.prototype);
proto.createdCallback =function(){this.innerHTML ='Hello, <b>'+(this.getAttribute('name')||'?')+'</b>';};
document.register('say-hi',{prototype: proto});// 定義並註冊使用了 Shadow DOM 的 <shadow-element>。var proto2 =Object.create(HTMLElement.prototype);
proto2.createdCallback =function(){var root =this.createShadowRoot();
root.innerHTML ="<style>::content > *{color: red}</style>"+"I'm a "+this.localName +" using Shadow DOM!<content></content>";};
document.register('shadow-element',{prototype: proto2});</script>
這個匯入定義 (並註冊) 了兩個元素,<say-hi>
和 <shadow-element>
。主頁面可以直接使用它們,無需做任何額外操作。
index.html
<head><linkrel="import"href="elements.html"></head><body><say-hiname="Eric"></say-hi><shadow-element><div>( I'm in the light dom )</div></shadow-element></body>
( I'm in the light dom )
在我看來,這樣的工作流程使得 HTML 匯入成為了共享 Web Components 的理想方式。
管理依賴和子匯入
嘿。聽說你挺喜歡匯入, 所以我就在你的匯入_裡_又加了個匯入。
子匯入(Sub-imports)
若匯入能夠巢狀將會提供更多便利。例如,如果你想複用或繼承另一個元件,使用匯入載入其他元素。
下面是 Polymer 中的真例項子。通過複用佈局還有選擇器元件,我們得到了一個新的選項卡元件
(<polymer-ui-tabs>
)。它們的依賴通過 HTML 匯入來管理。
polymer-ui-tabs.html
<linkrel="import"href="polymer-selector.html"><linkrel="import"href="polymer-flex-layout.html"><polymer-elementname="polymer-ui-tabs"extends="polymer-selector" ...><template><linkrel="stylesheet"href="polymer-ui-tabs.css"><polymer-flex-layout></polymer-flex-layout><shadow></shadow></template></polymer-element>
應用開發者可以引入這個新元素:
<linkrel="import"href="polymer-ui-tabs.html"><polymer-ui-tabs></polymer-ui-tabs>
若以後出現了一個更新,更棒的 <polymer-selector2>
,你就可以毫不猶豫的用它替換 <polymer-selector>
。多虧有了匯入和 web 元件,你再也不用擔心惹惱你的使用者了。
依賴管理
我們都知道一個頁面載入多個 jQuery 會出問題。若是多個元件引用了相同的庫,對於 Web 元件來說會不會是個_嚴重_的問題? 如果使用 HTML 引用,你就完全不用擔心! 匯入可以用來管理這些依賴。
將庫放進一個 HTML 匯入中,就自動避免了重複載入問題。文件只會被解析一次。指令碼也只執行一次。來舉個例子吧,比如說你定義了一個匯入,jquery.html,它會載入 JQuery。
jquery.html
<scriptsrc="http://cdn.com/jquery.js"></script>
這個匯入可以被其他匯入複用:
import2.html
<linkrel="import"href="jquery.html"><div>Hello, I'm import 2</div>
ajax-element.html
<linkrel="import"href="jquery.html"><linkrel="import"href="import2.html"><script>var proto =Object.create(HTMLElement.prototype);
proto.makeRequest =function(url, done){return $.ajax(url).done(function(){
done();});};
document.register('ajax-element',{prototype: proto});</script>
若主頁面也需要這個庫,連它也可以包含 jquery.html:
<head><linkrel="import"href="jquery.html"><linkrel="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 is requested once效能注意事項
HTML 匯入絕對是個好東西,但就像許多其他新技術一樣,你得明智的去使用它。Web 開發的最佳實踐還是需要遵守。下面是一些需要留意的地方。
合併匯入
減少網路請求始終是重點。如果需要很多最頂層的匯入,那就考慮把它們合併在一個資源裡,然後匯入該資源!
Vulcanizer 是由 Polymer 團隊開發的 npm 構建工具,它能夠遞迴的展開一組 HTML 匯入並生成一個單獨的檔案。可以把它看成構建 Web 元件中合併的步驟。
匯入影響瀏覽器快取
許多人似乎都忘記了瀏覽器的網路協議棧經過了多年的精心調整。匯入 (包括子匯入) 也從中受益。匯入 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);// 瀏覽器請求 styles.css
另一個例子就是動態建立標籤:
var h2 = document.createElement('h2');
h2.textContent ='Booyah!';
在你把 h2
新增到 DOM 之前它沒有意義。
同樣的概念對於匯入文件也適用。在你將內容追加到 DOM 之前,它就是一個空操作。實際上,在匯入文件中直接 "執行" 的只有 <script>
。參見匯入中的指令碼操作。
優化非同步載入
匯入不會阻塞主頁面解析。匯入中的指令碼會按照順序執行,但也不會阻塞主頁面。這意味著你在維護指令碼順序時獲得了類似於延遲載入的行為。將匯入放到 <head>
的好處在於它可以讓解析器儘快的去解析匯入的內容。即便如此,你還得記得主頁面中的 <script>
仍然 會阻塞頁面:
<head><linkrel="import"href="/path/to/import_that_takes_5secs.html"><script>console.log('I block page rendering');</script></head>
根據你的應用架構和使用場景不同,有幾種方法可以優化非同步行為。下面要使用的技巧可以緩解對主頁面渲染的阻塞。
場景 #1 (推薦): <head>
中沒有指令碼或 <body>
沒有內聯指令碼
我對放置 <script>
的建議就是不要緊跟著你的匯入。把它們儘可能遠的放置...你肯定早就按照最佳實踐這麼做了,不是嗎!?;)
看個例子:
<head><linkrel="import"href="/path/to/import.html"><linkrel="import"href="/path/to/import2.html"><!-- 避免在這放指令碼 --></head><body><!-- 避免在這放指令碼 --><divid="container"></div><!-- 避免在這放指令碼 -->
...
<script>// 其他的指令碼。// 獲得匯入內容。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:
<divid="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><linkrel="import"href="/path/to/import.html"></head><body><!-- 不需要寫指令碼。匯入會自己處理 --></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');// 匯入被提前新增 :)</script><script>// 其他指令碼</script></head><body><divid="container"></div>
...
</body>
或者,將匯入放到 <body>
結束處:
<head><script>// 其他指令碼</script></head><body><divid="container"></div>
...
<script>function addImportLink(url){...}
addImportLink('/path/to/import.html');// 匯入很晚才能被新增 :(</script></body>
注意: 不推薦最後的方法。解析器在解析頁面結束之前不會去操作匯入的內容。
要點
-
匯入的 MIME 型別是
text/html
。 -
匯入跨域資源需要啟用 CORS。
-
來自相同 URL 的匯入僅獲取和解析一次。這表示匯入中的指令碼只在第一次匯入的時候執行。
-
匯入中的指令碼按順序執行,它們不會阻塞主頁面解析。
-
匯入連結不代表 "#把內容新增到這裡"。它代表 "解析器,去把這個文件取過來,我一會要用"。指令碼在匯入期間執行,而樣式,標記,還有其他資源需要明確的加入到主頁面中。這是 HTML 匯入和
<iframe>
之間的最大區別,後者表示 "在這裡載入並渲染資源"。
總結
HTML 匯入允許將 HTML/CSS/JS 打包成一個單獨資源。這個想法在 Web 元件開發世界中顯得極為重要。開發者可以建立重用的元件,其他人通過引入 <link rel="import">
就能夠在自己的應用中使用這些元件。
HTML 匯入是個簡單的概念,但卻促成了許多有趣的使用案例。
使用案例
- 將相關的HTML/CSS/JS 作為一個單獨的包 來分發。理論上來說,你可以在應用裡面匯入一個完整的 web 應用。
- 程式碼組織 - 將概念按照邏輯劃分為不同的檔案,鼓勵模組化 & 複用性**。
- 傳輸 一或多個自定義元素 的定義。可以在應用內使用匯入來註冊 和包含自定義元素。這符合良好的軟體模式,即將介面/定義與使用分離。
- 管理依賴 - 自動解決資源的重複載入。
-
指令碼塊 - 沒有匯入之前,一個大型的 JS 庫需要在使用前全部解析,這通常很慢。有了匯入,只要塊 A 解析完畢,庫就能夠立即使用。延遲更少了!
<link rel="import" href="chunks.html">
:<script>/* script chunk A goes here */</script><script>/* script chunk B goes here */</script><script>/* script chunk C goes here */</script> ...
-
並行 HTML 解析 - 這是首次能夠讓瀏覽器並行執行兩個 (或多個) HTML 解析器。
-
允許在除錯和非除錯模式下切換,只需要修改匯入的目標。你的應用無需知道匯入的目標是打包/編譯好的資源還是一棵匯入樹。