1. 程式人生 > 實用技巧 >HTML5 link-import 引用html

HTML5 link-import 引用html

為什麼需要匯入?

先想想你在 web 上是如何載入不同型別的資源。對於 JS,我們有 <script src><link rel="stylesheet"> 應該是 CSS 的首選。圖片可以用 <img>。視訊則有 <video>。音訊,<audio>…… 你明白我在說什麼了吧! web 上絕大部分的內容都有簡單明瞭的載入方式。可對於 HTML 呢?下面是可選的幾種方案:

  1. <iframe> - 可用但笨重。iframe 中的內容全部存在於一個不同於當前頁的獨立上下文中。這是個很棒的特性,但也為開發者們帶來了額外的挑戰 (將 frame 按照內容尺寸來縮放已經有點難度,在 iframe 和當前頁面之間寫點 JS 能把人繞暈,更別提操作樣式了)。
  2. AJAX - 我喜歡 xhr.responseType="document",可是載入 HTML 要用 JS? 這就不大對勁了。
  3. CrazyHacks™ - 用字串的方式嵌入頁面,像註釋一樣隱藏 (例如 <script type="text/html">)。嘔!

可笑不? 作為 web 上最基礎的內容,HTML,竟然需要這麼麻煩才能得到我們想要的結果。幸運的是,Web Components 總算找到了一條正確的路。

開始

HTML 匯入Web Components 陣容中的一員,是在其他 HTML 文件中包含 HTML 文件的一種方法。當然並非僅限於此,你還可以包含 CSS,JavaScript,或 .html

檔案中能包含的任何內容。換句話說,這使得匯入成為了載入相關 HTML/CSS/JS 的神器

基礎

通過宣告 <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 [123]。當把這些技術結合在一起使用時,匯入就充當了 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 解析器。

  • 允許在除錯和非除錯模式下切換,只需要修改匯入的目標。你的應用無需知道匯入的目標是打包/編譯好的資源還是一棵匯入樹。